@messenger-box/platform-mobile 10.0.3-alpha.36 → 10.0.3-alpha.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/screens/inbox/components/CachedImage/index.js +125 -93
  3. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  4. package/lib/screens/inbox/components/DialogsListItem.js +74 -288
  5. package/lib/screens/inbox/components/DialogsListItem.js.map +1 -1
  6. package/lib/screens/inbox/components/ServiceDialogsListItem.js +184 -415
  7. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +1 -1
  8. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +0 -2
  9. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  10. package/lib/screens/inbox/containers/ConversationView.js +621 -951
  11. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  12. package/lib/screens/inbox/containers/Dialogs.js +212 -628
  13. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  14. package/lib/screens/inbox/containers/ThreadConversationView.js +409 -1364
  15. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  16. package/package.json +3 -3
  17. package/src/screens/inbox/components/CachedImage/index.tsx +191 -140
  18. package/src/screens/inbox/components/DialogsListItem.tsx +122 -386
  19. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +69 -377
  20. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +2 -4
  21. package/src/screens/inbox/containers/ConversationView.tsx +793 -1064
  22. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  23. package/src/screens/inbox/containers/Dialogs.tsx +301 -763
  24. package/src/screens/inbox/containers/ThreadConversationView.tsx +661 -1887
  25. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js +0 -175
  26. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js.map +0 -1
  27. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js +0 -191
  28. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js.map +0 -1
  29. package/lib/screens/inbox/containers/workflow/conversation-xstate.js +0 -380
  30. package/lib/screens/inbox/containers/workflow/conversation-xstate.js.map +0 -1
  31. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +0 -211
  32. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +0 -1
  33. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js +0 -438
  34. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js.map +0 -1
@@ -11,6 +11,7 @@ import {
11
11
  Image,
12
12
  Spinner,
13
13
  Text,
14
+ Skeleton,
14
15
  } from '@admin-layout/gluestack-ui-mobile';
15
16
  import { Platform, TouchableHighlight, SafeAreaView, View } from 'react-native';
16
17
  import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native';
@@ -21,7 +22,7 @@ import * as ImagePicker from 'expo-image-picker';
21
22
  import { encode as atob } from 'base-64';
22
23
  import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
23
24
  import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
24
- import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo } from 'common';
25
+ import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo, FileRefType, PostTypeEnum } from 'common';
25
26
  import {
26
27
  OnChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
27
28
  useMessagesQuery,
@@ -29,6 +30,7 @@ import {
29
30
  useSendMessagesMutation,
30
31
  useViewChannelDetailQuery,
31
32
  useAddDirectChannelMutation,
33
+ MessagesDocument,
32
34
  } from 'common/graphql';
33
35
  import { useUploadFilesNative } from '@messenger-box/platform-client';
34
36
  import { objectId } from '@messenger-box/core';
@@ -37,13 +39,8 @@ import { format, isToday, isYesterday } from 'date-fns';
37
39
  import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
38
40
  import CachedImage from '../components/CachedImage';
39
41
  import { config } from '../config';
40
- import {
41
- conversationXstate,
42
- Actions as ConversationActions,
43
- BaseState,
44
- MainState,
45
- } from './workflow/conversation-xstate';
46
42
  import colors from 'tailwindcss/colors';
43
+ import { v4 as uuidv4 } from 'uuid';
47
44
 
48
45
  // Define an extended interface for ImagePickerAsset with url property
49
46
  interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
@@ -90,516 +87,268 @@ export interface AlertMessageAttachmentsInterface {
90
87
  };
91
88
  }
92
89
 
93
- // Create a safer version of useMachine to handle potential errors
94
- function useSafeMachine(machine) {
95
- // Define the state type
96
- interface SafeStateType {
97
- context: {
98
- channelId: any;
99
- channelMessages: any[];
100
- totalCount: number;
101
- skip: number;
102
- loading: boolean;
103
- loadingOldMessages: boolean;
104
- error: any;
105
- selectedImage: string;
106
- files: any[];
107
- images: any[];
108
- messageText: string;
109
- imageLoading: boolean;
110
- };
111
- value: string;
112
- matches?: (stateValue: string) => boolean;
113
- }
114
-
115
- // Initialize with default state
116
- const [state, setState] = useState<SafeStateType>({
117
- context: {
118
- channelId: null,
119
- channelMessages: [],
120
- totalCount: 0,
121
- skip: 0,
122
- loading: false,
123
- loadingOldMessages: false,
124
- error: null,
125
- selectedImage: '',
126
- files: [],
127
- images: [],
128
- messageText: '',
129
- imageLoading: false,
130
- },
131
- value: 'idle',
132
- });
133
-
134
- // Create a safe send function
135
- const send = useCallback((event) => {
136
- try {
137
- // Log event for debugging
138
- console.log('Event received:', event.type);
139
-
140
- // Handle specific events manually
141
- if (event.type === ConversationActions.INITIAL_CONTEXT) {
142
- setState((prev) => ({
143
- ...prev,
144
- context: {
145
- ...prev.context,
146
- channelId: event.data?.channelId || null,
147
- },
148
- value: BaseState.FetchMessages,
149
- }));
150
- } else if (event.type === ConversationActions.SET_CHANNEL_MESSAGES) {
151
- setState((prev) => ({
152
- ...prev,
153
- context: {
154
- ...prev.context,
155
- channelMessages: event.data?.messages || [],
156
- totalCount: event.data?.totalCount || 0,
157
- loading: false,
158
- loadingOldMessages: false,
159
- },
160
- value: 'active',
161
- }));
162
- } else if (event.type === ConversationActions.CLEAR_MESSAGES) {
163
- setState((prev) => ({
164
- ...prev,
165
- context: {
166
- ...prev.context,
167
- channelMessages: [],
168
- totalCount: 0,
169
- },
170
- }));
171
- } else if (event.type === ConversationActions.SET_MESSAGE_TEXT) {
172
- setState((prev) => ({
173
- ...prev,
174
- context: {
175
- ...prev.context,
176
- messageText: event.data?.messageText || '',
177
- },
178
- }));
179
- } else if (event.type === ConversationActions.FETCH_MORE_MESSAGES) {
180
- setState((prev) => ({
181
- ...prev,
182
- context: {
183
- ...prev.context,
184
- loadingOldMessages: true,
185
- },
186
- value: MainState.FetchMoreMessages,
187
- }));
188
- } else if (event.type === ConversationActions.SET_IMAGE) {
189
- setState((prev) => ({
190
- ...prev,
191
- context: {
192
- ...prev.context,
193
- selectedImage: event.data?.image || '',
194
- images: event.data?.images || [],
195
- imageLoading: false,
196
- },
197
- }));
198
- } else if (event.type === ConversationActions.CLEAR_IMAGE) {
199
- setState((prev) => ({
200
- ...prev,
201
- context: {
202
- ...prev.context,
203
- selectedImage: '',
204
- images: [],
205
- },
206
- }));
207
- } else if (event.type === ConversationActions.START_LOADING) {
208
- setState((prev) => ({
209
- ...prev,
210
- context: {
211
- ...prev.context,
212
- loading: true,
213
- },
214
- }));
215
- } else if (event.type === ConversationActions.STOP_LOADING) {
216
- setState((prev) => ({
217
- ...prev,
218
- context: {
219
- ...prev.context,
220
- loading: false,
221
- },
222
- }));
223
- } else if (event.type === ConversationActions.SEND_MESSAGE) {
224
- setState((prev) => ({
225
- ...prev,
226
- context: {
227
- ...prev.context,
228
- loading: true,
229
- },
230
- value: MainState.SendMessage,
231
- }));
232
- } else if (event.type === ConversationActions.SEND_MESSAGE_WITH_FILE) {
233
- setState((prev) => ({
234
- ...prev,
235
- context: {
236
- ...prev.context,
237
- loading: true,
238
- },
239
- value: MainState.SendMessageWithFile,
240
- }));
241
- } else if (event.type === ConversationActions.CREATE_DIRECT_CHANNEL) {
242
- setState((prev) => ({
243
- ...prev,
244
- context: {
245
- ...prev.context,
246
- loading: true,
247
- },
248
- value: MainState.CreateDirectChannel,
249
- }));
250
- } else if (event.type === 'SEND_MESSAGE_SUCCESS' || event.type === 'SEND_MESSAGE_WITH_FILE_SUCCESS') {
251
- setState((prev) => ({
252
- ...prev,
253
- context: {
254
- ...prev.context,
255
- loading: false,
256
- messageText: '',
257
- images: [],
258
- selectedImage: '',
259
- },
260
- value: 'active',
261
- }));
262
- } else if (event.type === 'CREATE_DIRECT_CHANNEL_SUCCESS') {
263
- setState((prev) => ({
264
- ...prev,
265
- context: {
266
- ...prev.context,
267
- loading: false,
268
- channelId: event.data?.channelId || prev.context.channelId,
269
- messageText: '',
270
- },
271
- value: BaseState.FetchMessages,
272
- }));
273
- } else if (event.type === 'FETCH_MORE_MESSAGES_SUCCESS') {
274
- setState((prev) => {
275
- const newMessages = event.data?.messages || [];
276
- return {
277
- ...prev,
278
- context: {
279
- ...prev.context,
280
- loadingOldMessages: false,
281
- channelMessages: uniqBy([...prev.context.channelMessages, ...newMessages], ({ id }) => id),
282
- },
283
- value: 'active',
284
- };
285
- });
286
- } else if (event.type === 'ERROR') {
287
- setState((prev) => ({
288
- ...prev,
289
- context: {
290
- ...prev.context,
291
- loading: false,
292
- loadingOldMessages: false,
293
- error: event.data?.message || 'Unknown error',
294
- },
295
- value: 'error',
296
- }));
297
- }
298
- } catch (error) {
299
- console.error('Error in send function:', error);
300
- }
301
- }, []);
302
-
303
- // Add a custom matches function to the state
304
- const stateWithMatches = useMemo(() => {
305
- return {
306
- ...state,
307
- matches: (checkState) => {
308
- return state.value === checkState;
309
- },
310
- };
311
- }, [state]);
312
-
313
- // Return as a tuple to match useMachine API
314
- return [stateWithMatches, send] as const;
315
- }
316
-
317
- const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMessage, ...rest }: any) => {
318
- const [channelToTop, setChannelToTop] = useState(0);
319
-
320
- // Create a ref to track if component is mounted
321
- const isMountedRef = useRef(true);
322
-
323
- // Use our safer custom implementation instead of the problematic useMachine
324
- const [state, send] = useSafeMachine(conversationXstate);
325
-
326
- // Define safe functions first to avoid "used before declaration" errors
327
- const safeContext = useCallback(() => {
328
- try {
329
- return state?.context || {};
330
- } catch (error) {
331
- console.error('Error accessing state.context:', error);
332
- return {};
333
- }
334
- }, [state]);
335
-
336
- const safeContextProperty = useCallback(
337
- (property, defaultValue = null) => {
338
- try {
339
- return state?.context?.[property] ?? defaultValue;
340
- } catch (error) {
341
- console.error(`Error accessing state.context.${property}:`, error);
342
- return defaultValue;
343
- }
344
- },
345
- [state],
346
- );
347
-
348
- const safeMatches = useCallback(
349
- (stateValue) => {
350
- try {
351
- return state?.matches?.(stateValue) || false;
352
- } catch (error) {
353
- console.error(`Error calling state.matches with ${stateValue}:`, error);
354
- return false;
355
- }
356
- },
357
- [state],
358
- );
359
-
360
- const safeSend = useCallback(
361
- (event) => {
362
- try {
363
- console.log('🛠️ Safe send event:', event.type);
364
- send(event);
365
-
366
- // For important events like SET_CHANNEL_MESSAGES, verify they worked
367
- if (event.type === ConversationActions.SET_CHANNEL_MESSAGES) {
368
- // Add a verification step to ensure state was updated
369
- setTimeout(() => {
370
- // Don't log for every check to avoid console spam
371
- if (state.context.channelMessages.length === 0 && event.data?.messages?.length > 0) {
372
- console.warn('🛠️ SET_CHANNEL_MESSAGES did not update state - messages still empty');
373
- }
374
- }, 100);
375
- }
376
- } catch (error) {
377
- console.error('🛠️ Error in safeSend:', error, 'Event type:', event.type);
378
- // Try direct state update as fallback for critical events
379
- if (event.type === ConversationActions.SET_CHANNEL_MESSAGES && event.data?.messages) {
380
- console.log('🛠️ Attempting fallback for SET_CHANNEL_MESSAGES');
381
- try {
382
- // Direct update as fallback
383
- send({
384
- type: 'DIRECT_STATE_UPDATE',
385
- data: event.data,
386
- });
387
- } catch (fallbackError) {
388
- console.error('🛠️ Even fallback failed:', fallbackError);
389
- }
390
- }
391
- }
392
- },
393
- [send, state.context.channelMessages.length],
394
- );
395
-
396
- // Immediately set initial context if needed
397
- useEffect(() => {
398
- if (ChannelId) {
399
- console.log('Setting initial channel ID on mount:', ChannelId);
400
- try {
401
- send({
402
- type: ConversationActions.INITIAL_CONTEXT,
403
- data: { channelId: ChannelId },
404
- });
405
- } catch (error) {
406
- console.error('Error sending initial context:', error);
407
- }
408
- }
409
- }, []);
410
-
411
- // Use a ref to track the current machine snapshot for safer access
412
- const stateRef = useRef(state);
413
-
414
- // Keep the ref updated with the latest snapshot
415
- useEffect(() => {
416
- stateRef.current = state;
417
- }, [state]);
90
+ // Fix for the optimistic response types
91
+ type OptimisticPropsConfig = {
92
+ __typename: 'MachineConfiguration';
93
+ resource: string;
94
+ };
418
95
 
419
- // Avoid referencing state.context directly in places that might cause undefined errors
420
- const safeGetContext = useCallback(() => {
421
- if (stateRef.current && stateRef.current.context) {
422
- return stateRef.current.context;
423
- }
424
- // Return default values if context is undefined
425
- return {
426
- channelId: null,
427
- channelMessages: [],
428
- totalCount: 0,
429
- skip: 0,
430
- loading: false,
431
- loadingOldMessages: false,
432
- error: null,
433
- selectedImage: '',
434
- files: [],
435
- images: [],
436
- messageText: '',
437
- imageLoading: false,
438
- };
439
- }, []);
96
+ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowThreadMessage, ...rest }: any) => {
97
+ // Core state management using React hooks instead of XState
98
+ const [channelId, setChannelId] = useState<string | null>(initialChannelId || null);
99
+ const [messageText, setMessageText] = useState('');
100
+ const [skip, setSkip] = useState(0);
101
+ const [loading, setLoading] = useState(false);
102
+ const [loadingOldMessages, setLoadingOldMessages] = useState(false);
103
+ const [error, setError] = useState<string | null>(null);
104
+ const [selectedImage, setSelectedImage] = useState<string>('');
105
+ const [images, setImages] = useState<any[]>([]);
106
+ const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
107
+ const [imageObject, setImageObject] = useState<any>({});
440
108
 
441
- // Use cleanup function to prevent setting state after unmount
442
- useEffect(() => {
443
- return () => {
444
- isMountedRef.current = false;
445
- };
446
- }, []);
109
+ // Create refs for various operations
110
+ const messageRootListRef = useRef<any>(null);
111
+ const isMounted = useRef(true);
112
+ const fetchOldDebounceRef = useRef(false);
447
113
 
114
+ // Navigation and auth
448
115
  const auth: any = useSelector(userSelector);
449
116
  const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
450
117
  const navigation = useNavigation<any>();
451
- const [selectedImage, setImage] = useState<string>('');
452
- const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
453
- const [imageObject, setImageObject] = useState<any>({});
454
- const messageRootListRef = useRef<any>(null);
455
118
  const isFocused = useIsFocused();
456
119
 
120
+ // Apollo mutations
457
121
  const [addDirectChannel] = useAddDirectChannelMutation();
458
122
  const { startUpload } = useUploadFilesNative();
459
123
  const [sendMsg] = useSendMessagesMutation();
460
124
  const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
461
125
 
126
+ // Apollo query for messages
462
127
  const {
463
128
  data,
464
129
  loading: messageLoading,
465
130
  refetch,
466
131
  fetchMore: fetchMoreMessages,
467
132
  subscribeToMore,
468
- }: any = useMessagesQuery({
133
+ } = useMessagesQuery({
469
134
  variables: {
470
- channelId: state.context.channelId?.toString(),
135
+ channelId: channelId?.toString(),
471
136
  parentId: null,
472
137
  limit: MESSAGES_PER_PAGE,
473
- skip: state.context.skip,
138
+ skip: skip,
474
139
  },
475
- skip: !state.context.channelId,
140
+ skip: !channelId,
476
141
  fetchPolicy: 'cache-and-network',
477
142
  nextFetchPolicy: 'cache-first',
478
143
  refetchWritePolicy: 'merge',
479
- onCompleted: (queryData) => {
480
- console.log('MESSAGE QUERY COMPLETED:', queryData);
481
- if (queryData?.messages?.data) {
482
- console.log(
483
- 'Raw message data from query:',
484
- JSON.stringify(queryData.messages.data).substring(0, 100) + '...',
485
- );
486
- console.log('Message count from query:', queryData.messages.data.length);
487
- console.log('Total count from query:', queryData.messages.totalCount);
488
-
489
- // DIRECT STATE UPDATE: Immediately update state to ensure messages are displayed
490
- if (queryData.messages.data.length > 0) {
491
- console.log('🔄 IMMEDIATE STATE UPDATE with messages:', queryData.messages.data.length);
492
- safeSend({
493
- type: ConversationActions.SET_CHANNEL_MESSAGES,
494
- data: {
495
- messages: queryData.messages.data,
496
- totalCount: queryData.messages.totalCount,
497
- },
498
- });
499
- }
500
- }
501
- },
144
+ notifyOnNetworkStatusChange: true,
502
145
  onError: (error) => {
503
- console.error('MESSAGE QUERY ERROR:', error);
504
- // Report error to state machine
505
- safeSend({ type: 'ERROR', data: { message: String(error) } });
146
+ setError(String(error));
506
147
  },
507
148
  });
508
- // console.log('🔄 MESSAGE QUERY COMPLETED:', JSON.stringify(data));
509
- // Modify the fetchMessagesDirectly function to use safe access
510
- const fetchMessagesDirectly = useCallback(async () => {
511
- const channelId = safeGetContext().channelId;
512
- if (!channelId) {
513
- console.warn('Cannot fetch messages: No channel ID');
514
- return;
515
- }
516
149
 
517
- try {
518
- console.log('💫 FETCHING messages for channel:', channelId);
150
+ // Extract messages from the query data
151
+ const channelMessages = useMemo(() => {
152
+ return data?.messages?.data || [];
153
+ }, [data?.messages?.data]);
519
154
 
520
- // Use loading state to prevent duplicate fetches
521
- send({ type: ConversationActions.START_LOADING });
155
+ // Get total message count
156
+ const totalCount = useMemo(() => {
157
+ return data?.messages?.totalCount || 0;
158
+ }, [data?.messages?.totalCount]);
522
159
 
523
- const response = await refetch({
524
- channelId: channelId.toString(),
525
- parentId: null,
526
- limit: MESSAGES_PER_PAGE,
527
- skip: 0,
528
- });
160
+ // Clear messages when component unmounts
161
+ useEffect(() => {
162
+ return () => {
163
+ isMounted.current = false;
164
+ };
165
+ }, []);
529
166
 
530
- if (response?.data?.messages) {
531
- const { data: messages, totalCount } = response.data.messages;
532
-
533
- if (messages && messages.length > 0) {
534
- // Batch update to reduce renders
535
- safeSend({
536
- type: ConversationActions.SET_CHANNEL_MESSAGES,
537
- data: { messages, totalCount },
538
- });
539
- } else {
540
- console.warn('No messages found for channel', channelId);
541
- // Still clear loading state
542
- send({ type: ConversationActions.STOP_LOADING });
543
- }
544
- } else {
545
- console.warn('Query returned no messages data');
546
- send({ type: ConversationActions.STOP_LOADING });
167
+ // Update channelId from props or navigation params
168
+ useEffect(() => {
169
+ const currentChannelId = initialChannelId || currentRoute?.params?.channelId;
170
+ if (currentChannelId) {
171
+ setChannelId(currentChannelId);
172
+ }
173
+ }, [initialChannelId, currentRoute]);
174
+
175
+ // Focus/unfocus behavior
176
+ useFocusEffect(
177
+ React.useCallback(() => {
178
+ if (channelId) {
179
+ // Refresh messages when screen comes into focus
180
+ refetch();
547
181
  }
548
- } catch (error) {
549
- console.error('ERROR fetching messages:', error);
550
- send({ type: ConversationActions.STOP_LOADING });
182
+ return () => {
183
+ // Nothing needed on unfocus
184
+ };
185
+ }, [channelId, isFocused, refetch]),
186
+ );
187
+
188
+ // Loading state for image selection
189
+ useEffect(() => {
190
+ if (selectedImage) {
191
+ setLoading(false);
551
192
  }
552
- }, [safeGetContext, refetch, safeSend]);
193
+ }, [selectedImage]);
553
194
 
195
+ // Fetch more messages function
554
196
  const fetchMoreMessagesImpl = useCallback(async () => {
555
197
  try {
198
+ setLoadingOldMessages(true);
556
199
  const response = await fetchMoreMessages({
557
200
  variables: {
558
- channelId: state.context.channelId?.toString(),
201
+ channelId: channelId?.toString(),
559
202
  parentId: null,
560
- skip: state.context.channelMessages.length,
203
+ skip: channelMessages.length,
561
204
  },
205
+ // Let type policy handle the merge
562
206
  });
563
207
 
208
+ setLoadingOldMessages(false);
564
209
  if (!response?.data?.messages?.data) {
565
210
  return { error: 'No messages returned' };
566
211
  }
567
212
 
568
213
  return { messages: response.data.messages.data };
569
214
  } catch (error) {
215
+ setLoadingOldMessages(false);
216
+ setError(String(error));
570
217
  return { error: String(error) };
571
218
  }
572
- }, [state.context.channelId, state.context.channelMessages.length, fetchMoreMessages]);
219
+ }, [channelId, channelMessages.length, fetchMoreMessages]);
573
220
 
221
+ // Send message function
574
222
  const sendMessageImpl = useCallback(async () => {
575
223
  try {
224
+ // Store the current message text and clear input immediately for better UX
225
+ const currentMessageText = messageText;
226
+ setMessageText('');
227
+
576
228
  const notificationData: IExpoNotificationData = {
577
229
  url: config.INBOX_MESSEGE_PATH,
578
- params: { channelId: state.context.channelId, hideTabBar: true },
230
+ params: { channelId, hideTabBar: true },
579
231
  screen: 'DialogMessages',
580
232
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
581
233
  };
582
234
 
235
+ // Create optimistic message with minimal structure required for UI rendering
236
+ const messageId = objectId();
237
+ const optimisticMessage = {
238
+ __typename: 'Post' as const,
239
+ id: messageId,
240
+ message: currentMessageText,
241
+ createdAt: new Date().toISOString(),
242
+ updatedAt: new Date().toISOString(),
243
+ author: {
244
+ __typename: 'UserAccount' as const,
245
+ id: auth?.id,
246
+ picture: auth?.picture || '',
247
+ givenName: auth?.givenName || '',
248
+ familyName: auth?.familyName || '',
249
+ email: auth?.email || '',
250
+ username: auth?.username || '',
251
+ alias: [] as string[],
252
+ tokens: [],
253
+ },
254
+ isDelivered: true,
255
+ isRead: false,
256
+ type: 'TEXT' as PostTypeEnum,
257
+ parentId: null,
258
+ fromServer: false,
259
+ channel: {
260
+ __typename: 'Channel' as const,
261
+ id: channelId,
262
+ },
263
+ // Required fields that Apollo expects in the cache
264
+ propsConfiguration: {
265
+ __typename: 'MachineConfiguration' as const,
266
+ resource: '' as any,
267
+ },
268
+ props: {},
269
+ files: {
270
+ __typename: 'FilesInfo' as const,
271
+ data: [],
272
+ totalCount: 0,
273
+ },
274
+ replies: {
275
+ __typename: 'Messages' as const,
276
+ data: [],
277
+ totalCount: 0,
278
+ },
279
+ };
280
+
583
281
  const response = await sendMsg({
584
282
  variables: {
585
- channelId: state.context.channelId,
586
- content: state.context.messageText,
283
+ channelId,
284
+ content: currentMessageText,
587
285
  notificationParams: notificationData,
588
286
  },
287
+ optimisticResponse: {
288
+ __typename: 'Mutation',
289
+ sendMessage: optimisticMessage,
290
+ },
291
+ update: (cache, { data }) => {
292
+ if (data?.sendMessage) {
293
+ try {
294
+ // Read the existing messages from the cache
295
+ const existingData = cache.readQuery<{
296
+ messages: {
297
+ __typename: string;
298
+ messagesRefId?: string;
299
+ data: any[];
300
+ totalCount: number;
301
+ };
302
+ }>({
303
+ query: MessagesDocument,
304
+ variables: {
305
+ channelId: channelId?.toString(),
306
+ parentId: null,
307
+ limit: MESSAGES_PER_PAGE,
308
+ skip: 0,
309
+ },
310
+ });
311
+
312
+ // If we don't have data yet in the cache, don't try to update
313
+ if (!existingData) return;
314
+
315
+ // Let the type policy handle the merging
316
+ cache.writeQuery({
317
+ query: MessagesDocument,
318
+ variables: {
319
+ channelId: channelId?.toString(),
320
+ parentId: null,
321
+ limit: MESSAGES_PER_PAGE,
322
+ skip: 0,
323
+ },
324
+ data: {
325
+ messages: {
326
+ ...existingData.messages,
327
+ data: [data.sendMessage, ...existingData.messages.data],
328
+ totalCount: (existingData.messages.totalCount || 0) + 1,
329
+ },
330
+ },
331
+ });
332
+ } catch (error) {
333
+ console.error('Error updating cache:', error);
334
+ }
335
+ }
336
+ },
589
337
  });
590
338
 
591
339
  return { message: response.data?.sendMessage };
592
340
  } catch (error) {
341
+ setLoading(false);
342
+ setError(String(error));
593
343
  return { error: String(error) };
594
344
  }
595
- }, [state.context.channelId, state.context.messageText, sendMsg]);
345
+ }, [channelId, messageText, sendMsg, auth]);
596
346
 
597
- // Fix the image selection process to ensure proper format for upload
347
+ // Image selection handler
598
348
  const onSelectImages = async () => {
599
- safeSend({ type: ConversationActions.START_LOADING });
349
+ setLoading(true);
600
350
 
601
351
  try {
602
- console.log('Starting image picker...');
603
352
  let imageSource = await ImagePicker.launchImageLibraryAsync({
604
353
  mediaTypes: ImagePicker.MediaTypeOptions.Images,
605
354
  allowsEditing: true,
@@ -610,22 +359,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
610
359
  });
611
360
 
612
361
  if (!imageSource?.canceled) {
613
- console.log(
614
- 'Image selected. Asset details:',
615
- JSON.stringify({
616
- uri: imageSource?.assets?.[0]?.uri?.substring(0, 30) + '...',
617
- width: imageSource?.assets?.[0]?.width,
618
- height: imageSource?.assets?.[0]?.height,
619
- hasBase64: !!imageSource?.assets?.[0]?.base64,
620
- hasUri: !!imageSource?.assets?.[0]?.uri,
621
- }),
622
- );
623
-
624
362
  // Get the asset
625
363
  const selectedAsset = imageSource?.assets?.[0];
626
364
  if (!selectedAsset) {
627
- console.error('No asset found in selected image');
628
- safeSend({ type: ConversationActions.STOP_LOADING });
365
+ setLoading(false);
629
366
  return;
630
367
  }
631
368
 
@@ -641,58 +378,49 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
641
378
  mimeType: 'image/jpeg',
642
379
  };
643
380
 
644
- console.log('Prepared image asset for upload:', {
645
- hasUrl: !!asset.url,
646
- hasFileName: !!asset.fileName,
647
- hasMimeType: !!asset.mimeType,
648
- previewAvailable: !!previewImage,
649
- });
650
-
651
381
  // Update state with the new image
652
- safeSend({
653
- type: ConversationActions.SET_IMAGE,
654
- data: {
655
- image: previewImage,
656
- images: [asset], // Replace existing images with the new one
657
- },
658
- });
659
-
660
- console.log('Image state updated successfully');
382
+ setSelectedImage(previewImage);
383
+ setImages([asset]);
661
384
  } else {
662
- console.log('Image selection cancelled');
663
- safeSend({ type: ConversationActions.STOP_LOADING });
385
+ setLoading(false);
664
386
  }
665
387
  } catch (error) {
666
- console.error('Error selecting image:', error);
667
- safeSend({ type: ConversationActions.STOP_LOADING });
388
+ setLoading(false);
668
389
  }
669
390
  };
670
391
 
671
- // Update the sendMessageWithFileImpl function to fix image uploads
392
+ // Add a state variable to track which message should show the skeleton
393
+ const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
394
+
395
+ // Send message with file function - update to set and clear uploadingMessageId
672
396
  const sendMessageWithFileImpl = useCallback(async () => {
673
397
  try {
674
- console.log('Executing sendMessageWithFileImpl');
398
+ // For file uploads, we still need loading state since we need to wait for the file upload
399
+ setLoading(true);
675
400
 
676
401
  // Generate a unique post ID for the message
677
402
  const postId = objectId();
678
- console.log('Generated postId for file upload:', postId);
403
+
404
+ // Set the message ID that should show the skeleton
405
+ setUploadingMessageId(postId);
679
406
 
680
407
  // Prepare notification data
681
408
  const notificationData: IExpoNotificationData = {
682
409
  url: config.INBOX_MESSEGE_PATH,
683
- params: { channelId: state.context.channelId, hideTabBar: true },
410
+ params: { channelId, hideTabBar: true },
684
411
  screen: 'DialogMessages',
685
412
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
686
413
  };
687
414
 
688
415
  // Safety check for images
689
- if (!state.context.images || state.context.images.length === 0) {
690
- console.error('No images found in state');
416
+ if (!images || images.length === 0) {
417
+ setLoading(false);
418
+ setUploadingMessageId(null);
691
419
  return { error: 'No images available to upload' };
692
420
  }
693
421
 
694
422
  // Format the images for upload if needed
695
- const imagesToUpload = state.context.images.map((img) => {
423
+ const imagesToUpload = images.map((img) => {
696
424
  // Ensure the image has all required properties
697
425
  return {
698
426
  ...img,
@@ -702,20 +430,73 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
702
430
  };
703
431
  });
704
432
 
705
- console.log(
706
- 'Formatted images for upload:',
707
- imagesToUpload.map((img) => ({
708
- hasUri: !!img.uri,
709
- hasUrl: !!img.url,
710
- hasName: !!img.name,
711
- hasType: !!img.type,
712
- hasFileName: !!img.fileName,
713
- uri: img.uri?.substring(0, 30) + '...',
714
- })),
715
- );
433
+ // Store current message text and clear inputs immediately for better UX
434
+ const currentMessageText = messageText;
435
+ setMessageText('');
436
+
437
+ // Create a unique file ID for the optimistic response
438
+ const fileId = objectId();
439
+
440
+ // Create minimal optimistic message with file
441
+ const optimisticMessage = {
442
+ __typename: 'Post' as const,
443
+ id: postId,
444
+ message: currentMessageText || ' ',
445
+ createdAt: new Date().toISOString(),
446
+ updatedAt: new Date().toISOString(),
447
+ author: {
448
+ __typename: 'UserAccount' as const,
449
+ id: auth?.id,
450
+ picture: auth?.picture || '',
451
+ givenName: auth?.givenName || '',
452
+ familyName: auth?.familyName || '',
453
+ email: auth?.email || '',
454
+ username: auth?.username || '',
455
+ alias: [] as string[],
456
+ tokens: [],
457
+ },
458
+ isDelivered: true,
459
+ isRead: false,
460
+ type: 'TEXT' as PostTypeEnum,
461
+ parentId: null,
462
+ fromServer: false,
463
+ channel: {
464
+ __typename: 'Channel' as const,
465
+ id: channelId,
466
+ },
467
+ // Required fields that Apollo expects in the cache
468
+ propsConfiguration: {
469
+ __typename: 'MachineConfiguration' as const,
470
+ resource: '' as any,
471
+ },
472
+ props: {},
473
+ files: {
474
+ __typename: 'FilesInfo' as const,
475
+ data: [
476
+ {
477
+ __typename: 'FileInfo' as const,
478
+ id: fileId,
479
+ url: selectedImage,
480
+ name: imagesToUpload[0]?.name || 'image.jpg',
481
+ extension: 'jpg',
482
+ mimeType: 'image/jpeg',
483
+ height: imagesToUpload[0]?.height || 0,
484
+ width: imagesToUpload[0]?.width || 0,
485
+ size: imagesToUpload[0]?.fileSize || 0,
486
+ refType: 'Post' as FileRefType,
487
+ ref: postId,
488
+ },
489
+ ],
490
+ totalCount: 1,
491
+ },
492
+ replies: {
493
+ __typename: 'Messages' as const,
494
+ data: [],
495
+ totalCount: 0,
496
+ },
497
+ };
716
498
 
717
499
  // Upload the files
718
- console.log('Starting file upload...');
719
500
  const uploadResponse = await startUpload({
720
501
  file: imagesToUpload,
721
502
  saveUploadedFile: {
@@ -726,89 +507,154 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
726
507
  },
727
508
  });
728
509
 
729
- console.log(
730
- 'Upload response received:',
731
- uploadResponse?.data ? 'Has data' : 'No data',
732
- 'Error:',
733
- uploadResponse?.error ? uploadResponse.error : 'None',
734
- );
735
-
736
510
  if (uploadResponse?.error) {
737
- console.error('Upload error:', uploadResponse.error);
511
+ setLoading(false);
512
+ setUploadingMessageId(null);
738
513
  return { error: String(uploadResponse.error) };
739
514
  }
740
515
 
741
516
  // Get uploaded file IDs
742
517
  const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
743
- console.log(
744
- 'Uploaded files:',
745
- uploadedFiles
746
- ? JSON.stringify(uploadedFiles.map((f) => ({ id: f.id, url: f.url?.substring(0, 30) + '...' })))
747
- : 'null',
748
- );
749
-
750
518
  const files = uploadedFiles?.map((f: any) => f.id) ?? null;
751
519
 
752
- console.log('Files uploaded successfully. File IDs:', files);
520
+ // Create a real version of the message with the actual file data
521
+ const realMessage = {
522
+ ...optimisticMessage,
523
+ files: {
524
+ __typename: 'FilesInfo' as const,
525
+ data: uploadedFiles.map((file) => ({
526
+ __typename: 'FileInfo' as const,
527
+ id: file.id,
528
+ url: file.url,
529
+ name: file.name,
530
+ extension: file.extension,
531
+ mimeType: file.mimeType,
532
+ height: file.height,
533
+ width: file.width,
534
+ size: file.size,
535
+ refType: file.refType,
536
+ ref: postId,
537
+ })),
538
+ totalCount: uploadedFiles.length,
539
+ },
540
+ };
753
541
 
754
542
  // Send the message with the uploaded files
755
- console.log('Sending message with files:', {
756
- postId,
757
- channelId: state.context.channelId,
758
- content: state.context.messageText || ' ',
759
- hasFiles: !!files,
760
- fileCount: files?.length || 0,
761
- });
762
-
763
543
  const response = await sendMsg({
764
544
  variables: {
765
545
  postId,
766
- channelId: state.context.channelId,
767
- content: state.context.messageText || ' ', // Use a space if no text
546
+ channelId,
547
+ content: currentMessageText || ' ', // Use a space if no text
768
548
  files,
769
549
  notificationParams: notificationData,
770
550
  },
771
- });
551
+ optimisticResponse: {
552
+ __typename: 'Mutation',
553
+ sendMessage: realMessage, // Use the message with real file data
554
+ },
555
+ update: (cache, { data }) => {
556
+ if (data?.sendMessage) {
557
+ try {
558
+ // Read the existing messages from the cache
559
+ const existingData = cache.readQuery<{
560
+ messages: {
561
+ __typename: string;
562
+ messagesRefId?: string;
563
+ data: any[];
564
+ totalCount: number;
565
+ };
566
+ }>({
567
+ query: MessagesDocument,
568
+ variables: {
569
+ channelId: channelId?.toString(),
570
+ parentId: null,
571
+ limit: MESSAGES_PER_PAGE,
572
+ skip: 0,
573
+ },
574
+ });
772
575
 
773
- if (response?.data?.sendMessage) {
774
- console.log('Message with file sent successfully:', response.data.sendMessage.id);
775
-
776
- // Log the file data from the response to verify it's being returned correctly
777
- if (response.data.sendMessage.files?.data) {
778
- console.log(
779
- '📷 Message response file data:',
780
- JSON.stringify({
781
- fileCount: response.data.sendMessage.files.data.length,
782
- fileUrl: response.data.sendMessage.files.data[0]?.url?.substring(0, 30) + '...',
783
- }),
784
- );
785
- }
576
+ // If we don't have data yet in the cache, don't try to update
577
+ if (!existingData) return;
578
+
579
+ // Ensure the message has files data
580
+ const messageWithFiles = {
581
+ ...data.sendMessage,
582
+ files: data.sendMessage.files || {
583
+ __typename: 'FilesInfo',
584
+ data: uploadedFiles.map((file) => ({
585
+ __typename: 'FileInfo',
586
+ id: file.id,
587
+ url: file.url,
588
+ name: file.name,
589
+ extension: file.extension,
590
+ mimeType: file.mimeType,
591
+ height: file.height,
592
+ width: file.width,
593
+ size: file.size,
594
+ refType: file.refType,
595
+ ref: postId,
596
+ })),
597
+ totalCount: uploadedFiles.length,
598
+ },
599
+ };
600
+
601
+ // Let the type policy handle the merging
602
+ cache.writeQuery({
603
+ query: MessagesDocument,
604
+ variables: {
605
+ channelId: channelId?.toString(),
606
+ parentId: null,
607
+ limit: MESSAGES_PER_PAGE,
608
+ skip: 0,
609
+ },
610
+ data: {
611
+ messages: {
612
+ ...existingData.messages,
613
+ data: [messageWithFiles, ...existingData.messages.data],
614
+ totalCount: (existingData.messages.totalCount || 0) + 1,
615
+ },
616
+ },
617
+ });
786
618
 
787
- // Clear the images after successful send
788
- setTimeout(() => {
789
- safeSend({ type: ConversationActions.CLEAR_IMAGE });
790
- }, 100);
791
- } else {
792
- console.error('Failed to send message with file:', response?.errors);
793
- }
619
+ // Clear the images after successful send
620
+ setSelectedImage('');
621
+ setImages([]);
622
+ } catch (error) {
623
+ console.error('Error updating cache:', error);
624
+ }
625
+ }
626
+ },
627
+ });
794
628
 
629
+ setLoading(false);
630
+ setUploadingMessageId(null);
795
631
  return { message: response.data?.sendMessage };
796
632
  } catch (error) {
797
- console.error('Error in sendMessageWithFileImpl:', error);
633
+ setLoading(false);
634
+ setUploadingMessageId(null);
635
+ setError(String(error));
798
636
  return { error: String(error) };
799
637
  }
800
- }, [state.context.channelId, state.context.messageText, state.context.images, startUpload, sendMsg, safeSend]);
638
+ }, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth]);
801
639
 
640
+ // Create direct channel implementation
802
641
  const createDirectChannelImpl = useCallback(async () => {
803
642
  try {
643
+ setLoading(true);
804
644
  if (
805
645
  !rest?.isCreateNewChannel ||
806
646
  rest?.newChannelData?.type !== RoomType?.Direct ||
807
647
  !rest?.newChannelData?.userIds?.length
808
648
  ) {
649
+ setLoading(false);
809
650
  return { error: 'Invalid channel data' };
810
651
  }
811
652
 
653
+ // Store current message text
654
+ const currentMessageText = messageText;
655
+ // Clear message text immediately for better UX
656
+ setMessageText('');
657
+
812
658
  const response = await addDirectChannel({
813
659
  variables: {
814
660
  receiver: [...(rest?.newChannelData?.userIds ?? [])],
@@ -817,10 +663,12 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
817
663
  });
818
664
 
819
665
  if (!response?.data?.createDirectChannel?.id) {
666
+ setLoading(false);
820
667
  return { error: 'Failed to create channel' };
821
668
  }
822
669
 
823
670
  const newChannelId = response.data.createDirectChannel.id;
671
+ setChannelId(newChannelId);
824
672
 
825
673
  const notificationData: IExpoNotificationData = {
826
674
  url: config.INBOX_MESSEGE_PATH,
@@ -829,113 +677,102 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
829
677
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
830
678
  };
831
679
 
680
+ // Create unique message ID for optimistic response
681
+ const messageId = objectId();
682
+
683
+ // Create minimal optimistic message
684
+ const optimisticMessage = {
685
+ __typename: 'Post' as const,
686
+ id: messageId,
687
+ message: currentMessageText,
688
+ createdAt: new Date().toISOString(),
689
+ updatedAt: new Date().toISOString(),
690
+ author: {
691
+ __typename: 'UserAccount' as const,
692
+ id: auth?.id,
693
+ picture: auth?.picture || '',
694
+ givenName: auth?.givenName || '',
695
+ familyName: auth?.familyName || '',
696
+ email: auth?.email || '',
697
+ username: auth?.username || '',
698
+ alias: [] as string[],
699
+ tokens: [],
700
+ },
701
+ isDelivered: true,
702
+ isRead: false,
703
+ type: 'TEXT' as PostTypeEnum,
704
+ parentId: null,
705
+ fromServer: false,
706
+ channel: {
707
+ __typename: 'Channel' as const,
708
+ id: newChannelId,
709
+ },
710
+ // Required fields that Apollo expects in the cache
711
+ propsConfiguration: {
712
+ __typename: 'MachineConfiguration' as const,
713
+ resource: '' as any,
714
+ },
715
+ props: {},
716
+ files: {
717
+ __typename: 'FilesInfo' as const,
718
+ data: [],
719
+ totalCount: 0,
720
+ },
721
+ replies: {
722
+ __typename: 'Messages' as const,
723
+ data: [],
724
+ totalCount: 0,
725
+ },
726
+ };
727
+
728
+ // Send message in the new channel
832
729
  await sendMsg({
833
730
  variables: {
834
731
  channelId: newChannelId,
835
- content: state.context.messageText,
732
+ content: currentMessageText,
836
733
  notificationParams: notificationData,
837
734
  },
735
+ optimisticResponse: {
736
+ __typename: 'Mutation',
737
+ sendMessage: optimisticMessage,
738
+ },
739
+ update: (cache, { data }) => {
740
+ if (data?.sendMessage) {
741
+ try {
742
+ // For a new channel, we don't need to read existing data
743
+ // Just write the new message to the cache
744
+ cache.writeQuery({
745
+ query: MessagesDocument,
746
+ variables: {
747
+ channelId: newChannelId,
748
+ parentId: null,
749
+ limit: MESSAGES_PER_PAGE,
750
+ skip: 0,
751
+ },
752
+ data: {
753
+ messages: {
754
+ __typename: 'Messages',
755
+ messagesRefId: newChannelId,
756
+ data: [data.sendMessage],
757
+ totalCount: 1,
758
+ },
759
+ },
760
+ });
761
+ } catch (error) {
762
+ console.error('Error updating cache:', error);
763
+ }
764
+ }
765
+ },
838
766
  });
839
767
 
768
+ setLoading(false);
840
769
  return { channelId: newChannelId };
841
770
  } catch (error) {
771
+ setLoading(false);
772
+ setError(String(error));
842
773
  return { error: String(error) };
843
774
  }
844
- }, [rest, state.context.messageText, addDirectChannel, sendMsg]);
845
-
846
- // Remove the implementation inside this effect
847
- useEffect(() => {
848
- // We've moved these implementations to useCallback hooks above
849
- }, [state.value, sendMsg, refetch, fetchMoreMessages, addDirectChannel, startUpload, rest, state.context]);
850
-
851
- React.useEffect(() => {
852
- return () => {
853
- send({ type: ConversationActions.CLEAR_MESSAGES });
854
- };
855
- }, []);
856
-
857
- useFocusEffect(
858
- React.useCallback(() => {
859
- if (state.context.channelId) {
860
- send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: state.context.channelId } });
861
- }
862
- return () => {
863
- send({ type: ConversationActions.CLEAR_MESSAGES });
864
- };
865
- }, [state.context.channelId, isFocused]),
866
- );
867
-
868
- React.useEffect(() => {
869
- const currentChannelId = ChannelId || currentRoute?.params?.channelId;
870
- if (currentChannelId) {
871
- console.log('Setting initial channel ID:', currentChannelId);
872
- send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: currentChannelId } });
873
- }
874
- }, [ChannelId, currentRoute]);
875
-
876
- React.useEffect(() => {
877
- if (state.context.selectedImage) {
878
- send({ type: ConversationActions.STOP_LOADING });
879
- }
880
- }, [state.context.selectedImage]);
881
-
882
- useEffect(() => {
883
- if (data?.messages?.data) {
884
- console.log('📩 QUERY DATA CHANGED - Messages received:', data.messages.data.length);
885
- const { data: messages, totalCount: messageTotalCount } = data.messages;
886
-
887
- if (messages && messages.length > 0) {
888
- console.log('📩 QUERY DATA - Setting channel messages, count:', messages.length);
889
-
890
- // Use timeout to ensure this doesn't conflict with other state updates
891
- setTimeout(() => {
892
- // Directly update the state machine with new messages
893
- safeSend({
894
- type: ConversationActions.SET_CHANNEL_MESSAGES,
895
- data: {
896
- messages: uniqBy([...messages, ...state.context.channelMessages], ({ id }) => id),
897
- totalCount: messageTotalCount,
898
- },
899
- });
900
-
901
- // Verify the update was processed properly
902
- setTimeout(() => {
903
- const currentMessagesCount = state.context.channelMessages?.length || 0;
904
- console.log('📩 STATE UPDATE VERIFICATION - Current message count:', currentMessagesCount);
905
-
906
- if (currentMessagesCount === 0) {
907
- console.warn('⚠️ STATE NOT UPDATED after message data received - triggering fallback');
908
- // Direct fallback approach
909
- safeSend({
910
- type: ConversationActions.SET_CHANNEL_MESSAGES,
911
- data: {
912
- messages: messages,
913
- totalCount: messageTotalCount,
914
- },
915
- });
916
- }
917
- }, 500);
918
- }, 0);
919
-
920
- // Debug: Log the first message to verify data format
921
- if (messages[0]) {
922
- const sample = messages[0];
923
- console.log(
924
- '📩 SAMPLE MESSAGE:',
925
- JSON.stringify({
926
- id: sample.id,
927
- message: sample.message,
928
- author: {
929
- id: sample.author?.id,
930
- name: `${sample.author?.givenName} ${sample.author?.familyName}`,
931
- },
932
- createdAt: sample.createdAt,
933
- }),
934
- );
935
- }
936
- }
937
- }
938
- }, [data, safeSend]);
775
+ }, [rest, messageText, addDirectChannel, sendMsg, auth]);
939
776
 
940
777
  // Optimize onFetchOld by adding debounce logic
941
778
  const onFetchOld = useCallback(() => {
@@ -943,49 +780,86 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
943
780
  if (fetchOldDebounceRef.current) return;
944
781
 
945
782
  // Check if we need to fetch more messages
946
- if (
947
- state?.context?.totalCount > state?.context?.channelMessages?.length &&
948
- !state?.context?.loadingOldMessages
949
- ) {
783
+ if (totalCount > channelMessages.length && !loadingOldMessages) {
950
784
  // Set debounce
951
785
  fetchOldDebounceRef.current = true;
952
786
 
953
- // Send fetch event
954
- send({ type: ConversationActions.FETCH_MORE_MESSAGES });
787
+ // Fetch more messages
788
+ fetchMoreMessagesImpl();
955
789
 
956
790
  // Clear debounce after a timeout
957
791
  setTimeout(() => {
958
792
  fetchOldDebounceRef.current = false;
959
793
  }, 1000);
960
794
  }
961
- }, [state?.context?.totalCount, state?.context?.channelMessages, state?.context?.loadingOldMessages]);
962
-
963
- // Add debounce ref
964
- const fetchOldDebounceRef = useRef(false);
795
+ }, [totalCount, channelMessages.length, loadingOldMessages, fetchMoreMessagesImpl]);
965
796
 
966
797
  const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
967
798
  const paddingToTop = 60;
968
799
  return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
969
800
  };
970
801
 
971
- const dataURLtoFile = (dataurl: any, filename: any) => {
972
- var arr = dataurl.split(','),
973
- mime = arr[0].match(/:(.*?);/)[1],
974
- bstr = atob(arr[1]),
975
- n = bstr.length,
976
- u8arr = new Uint8Array(n);
977
- while (n--) {
978
- u8arr[n] = bstr.charCodeAt(n);
802
+ // Transform the message data for GiftedChat
803
+ const messageList = useMemo(() => {
804
+ // Short-circuit if no messages to process
805
+ if (!channelMessages || channelMessages.length === 0) {
806
+ return [];
807
+ }
808
+
809
+ // Use a more efficient approach - pre-filter messages once
810
+ const filteredMessages = uniqBy(channelMessages, ({ id }) => id);
811
+
812
+ // Skip processing if no filtered messages
813
+ if (filteredMessages.length === 0) {
814
+ return [];
979
815
  }
980
- return new File([u8arr], filename, { type: mime });
981
- };
982
816
 
983
- // Fix the render send function to ensure it works for image-only messages
817
+ // Transform messages only once and return
818
+ return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
819
+ const date = new Date(msg.createdAt);
820
+
821
+ // Extract image URL from files data
822
+ let imageUrl = null;
823
+ if (msg.files && typeof msg.files === 'object') {
824
+ // Handle both cases where files might be directly present or via files.data
825
+ const filesData = msg.files.data || (Array.isArray(msg.files) ? msg.files : null);
826
+
827
+ if (filesData && filesData.length > 0) {
828
+ const fileData = filesData[0];
829
+ // Make sure we have valid file data with a URL
830
+ if (fileData && typeof fileData === 'object' && fileData.url) {
831
+ imageUrl = fileData.url;
832
+ }
833
+ }
834
+ }
835
+
836
+ // Create message in a more direct way
837
+ return {
838
+ _id: msg.id,
839
+ text: msg.message,
840
+ createdAt: date,
841
+ user: {
842
+ _id: msg.author?.id || '',
843
+ name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
844
+ avatar: msg.author?.picture || '',
845
+ },
846
+ image: imageUrl,
847
+ sent: msg?.isDelivered,
848
+ received: msg?.isRead,
849
+ type: msg?.type,
850
+ propsConfiguration: msg?.propsConfiguration,
851
+ replies: msg?.replies ?? [],
852
+ isShowThreadMessage,
853
+ };
854
+ });
855
+ }, [channelMessages, isShowThreadMessage]);
856
+
857
+ // Render the send button
984
858
  const renderSend = useCallback(
985
859
  (props) => {
986
860
  // Enable the send button if there's text OR we have images
987
- const hasContent = !!props.text || state?.context?.images?.length > 0;
988
- const canSend = (state?.context?.channelId || rest?.isCreateNewChannel) && hasContent;
861
+ const hasContent = !!props.text || images?.length > 0;
862
+ const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
989
863
 
990
864
  return (
991
865
  <Send
@@ -1011,157 +885,58 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1011
885
  </Send>
1012
886
  );
1013
887
  },
1014
- [state?.context?.channelId, state?.context?.images, rest?.isCreateNewChannel],
888
+ [channelId, images, rest?.isCreateNewChannel],
1015
889
  );
1016
890
 
1017
- // Fix the handleSend function to properly handle image-only messages
891
+ // Handle send for messages
1018
892
  const handleSend = useCallback(
1019
893
  async (messages) => {
1020
894
  // Extract message text from GiftedChat messages array
1021
- const messageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
1022
- console.log('Sending message:', messageText);
1023
- console.log('Images:', state.context.images?.length);
895
+ const newMessageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
1024
896
 
1025
897
  // Check if we can send a message (channel exists or we're creating one)
1026
- if (!state.context.channelId && !rest?.isCreateNewChannel) {
1027
- console.log('Cannot send - no channel');
898
+ if (!channelId && !rest?.isCreateNewChannel) {
1028
899
  return;
1029
900
  }
1030
901
 
1031
902
  // Allow sending if we have text OR images (image-only messages are valid)
1032
- const hasText = !!messageText && messageText !== ' ';
1033
- const hasImages = state.context.images && state.context.images.length > 0;
903
+ const hasText = !!newMessageText && newMessageText !== ' ';
904
+ const hasImages = images && images.length > 0;
1034
905
 
1035
906
  if (!hasText && !hasImages) {
1036
- console.log('Nothing to send - no text or images');
1037
907
  return;
1038
908
  }
1039
909
 
1040
- // Set the message text in the state (even if empty for image-only messages)
1041
- safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText } });
910
+ // Update the message text state - now handled in send functions for better UX
911
+ setMessageText(newMessageText);
1042
912
 
1043
913
  // Handle direct channel creation if needed
1044
- if (rest?.isCreateNewChannel && !state.context.channelId) {
914
+ if (rest?.isCreateNewChannel && !channelId) {
1045
915
  if (rest?.newChannelData?.type === RoomType?.Direct) {
1046
- safeSend({ type: ConversationActions.CREATE_DIRECT_CHANNEL });
916
+ createDirectChannelImpl();
1047
917
  }
1048
918
  return;
1049
919
  }
1050
920
 
1051
921
  // Send message with or without image based on state
1052
922
  if (hasImages) {
1053
- console.log('Sending message with file');
1054
- safeSend({ type: ConversationActions.SEND_MESSAGE_WITH_FILE });
923
+ sendMessageWithFileImpl();
1055
924
  } else {
1056
- console.log('Sending text-only message');
1057
- safeSend({ type: ConversationActions.SEND_MESSAGE });
925
+ sendMessageImpl();
1058
926
  }
1059
927
  },
1060
- [state.context.channelId, state.context.images, rest?.isCreateNewChannel, rest?.newChannelData?.type, safeSend],
928
+ [
929
+ channelId,
930
+ images,
931
+ rest?.isCreateNewChannel,
932
+ rest?.newChannelData?.type,
933
+ createDirectChannelImpl,
934
+ sendMessageWithFileImpl,
935
+ sendMessageImpl,
936
+ ],
1061
937
  );
1062
938
 
1063
- // Update fetchMessagesWithFallback to not use fallback state
1064
- const fetchMessagesWithFallback = useCallback(async () => {
1065
- if (!state.context.channelId) return;
1066
-
1067
- try {
1068
- console.log('🔄 DIRECT FETCH: Using direct approach for channel:', state.context.channelId);
1069
-
1070
- const response = await refetch({
1071
- channelId: state.context.channelId?.toString(),
1072
- parentId: null,
1073
- limit: MESSAGES_PER_PAGE,
1074
- skip: 0,
1075
- });
1076
-
1077
- if (response?.data?.messages?.data) {
1078
- const messages = response.data.messages.data;
1079
- console.log('✅ DIRECT FETCH: Got messages:', messages.length);
1080
-
1081
- // Skip fallback and send directly to state machine
1082
- send({
1083
- type: ConversationActions.SET_CHANNEL_MESSAGES,
1084
- data: {
1085
- messages,
1086
- totalCount: response.data.messages.totalCount,
1087
- },
1088
- });
1089
- }
1090
- } catch (error) {
1091
- console.error('❌ DIRECT FETCH ERROR:', error);
1092
- }
1093
- }, [state.context.channelId, refetch]);
1094
-
1095
- // Auto-trigger fallback if needed
1096
- useEffect(() => {
1097
- let timeoutId: NodeJS.Timeout;
1098
-
1099
- if (state.context.channelId && state.context.channelMessages.length === 0) {
1100
- timeoutId = setTimeout(() => {
1101
- console.log('⚠️ ACTIVATING FALLBACK - XState not updating after timeout');
1102
- fetchMessagesWithFallback();
1103
- }, 3000); // Wait 3 seconds for normal flow to work
1104
- }
1105
-
1106
- return () => {
1107
- if (timeoutId) clearTimeout(timeoutId);
1108
- };
1109
- }, [state.context.channelId, state.context.channelMessages, fetchMessagesWithFallback]);
1110
-
1111
- // Optimize the messageList calculation for better performance
1112
- const messageList = useMemo(() => {
1113
- // Only recalculate when these dependencies change
1114
- console.log('🔄 CALCULATING MESSAGE LIST - Optimized version');
1115
-
1116
- // Short-circuit if no messages to process
1117
- if (!state?.context?.channelMessages || state.context.channelMessages.length === 0) {
1118
- console.log('No messages to process');
1119
- return [];
1120
- }
1121
-
1122
- // Use a more efficient approach - pre-filter messages once
1123
- const filteredMessages = uniqBy(state.context.channelMessages, ({ id }) => id);
1124
-
1125
- // Skip processing if no filtered messages
1126
- if (filteredMessages.length === 0) {
1127
- return [];
1128
- }
1129
-
1130
- // Transform messages only once and return
1131
- return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
1132
- const date = new Date(msg.createdAt);
1133
-
1134
- // Extract image URL from files data
1135
- let imageUrl = null;
1136
- if (msg.files?.data && msg.files.data.length > 0) {
1137
- const fileData = msg.files.data[0];
1138
- if (fileData && fileData.url) {
1139
- imageUrl = fileData.url;
1140
- }
1141
- }
1142
-
1143
- // Create message in a more direct way
1144
- return {
1145
- _id: msg.id,
1146
- text: msg.message,
1147
- createdAt: date,
1148
- user: {
1149
- _id: msg.author?.id || '',
1150
- name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
1151
- avatar: msg.author?.picture || '',
1152
- },
1153
- image: imageUrl,
1154
- sent: msg?.isDelivered,
1155
- received: msg?.isRead,
1156
- type: msg?.type,
1157
- propsConfiguration: msg?.propsConfiguration,
1158
- replies: msg?.replies ?? [],
1159
- isShowThreadMessage,
1160
- };
1161
- });
1162
- }, [state?.context?.channelMessages, isShowThreadMessage]);
1163
-
1164
- // Memoize the renderMessageText function
939
+ // Render message text with customizations for alerts and replies
1165
940
  const renderMessageText = useCallback(
1166
941
  (props: any) => {
1167
942
  const { currentMessage } = props;
@@ -1226,7 +1001,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1226
1001
  onPress={() => {
1227
1002
  if (currentMessage?.isShowThreadMessage)
1228
1003
  navigation.navigate(config.THREAD_MESSEGE_PATH, {
1229
- channelId: state?.context?.channelId,
1004
+ channelId: channelId,
1230
1005
  title: 'Message',
1231
1006
  postParentId: currentMessage?._id,
1232
1007
  isPostParentIdThread: true,
@@ -1291,7 +1066,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1291
1066
  onPress={() => {
1292
1067
  if (currentMessage?.isShowThreadMessage)
1293
1068
  navigation.navigate(config.THREAD_MESSEGE_PATH, {
1294
- channelId: state?.context?.channelId,
1069
+ channelId: channelId,
1295
1070
  title: 'Message',
1296
1071
  postParentId: currentMessage?._id,
1297
1072
  isPostParentIdThread: true,
@@ -1343,9 +1118,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1343
1118
  );
1344
1119
  }
1345
1120
  },
1346
- [navigation, state?.context?.channelId, role],
1121
+ [navigation, channelId, role],
1347
1122
  );
1348
1123
 
1124
+ // Render action buttons (including image upload)
1349
1125
  const renderActions = (props) => {
1350
1126
  return (
1351
1127
  <Actions
@@ -1379,79 +1155,115 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1379
1155
  };
1380
1156
 
1381
1157
  // Create a more visible and reliable image preview with cancel button
1382
- const renderAccessory = useCallback(
1383
- (props) => {
1384
- const selectedImage = safeContextProperty('selectedImage', '');
1385
-
1386
- if (!selectedImage) {
1387
- return null;
1388
- }
1158
+ const renderAccessory = useCallback(() => {
1159
+ if (!selectedImage) {
1160
+ return null;
1161
+ }
1389
1162
 
1390
- return (
1163
+ return (
1164
+ <View
1165
+ style={{
1166
+ height: 70,
1167
+ backgroundColor: 'white',
1168
+ borderTopWidth: 1,
1169
+ borderTopColor: '#e0e0e0',
1170
+ flexDirection: 'row',
1171
+ alignItems: 'center',
1172
+ margin: 0,
1173
+ padding: 0,
1174
+ paddingVertical: 0,
1175
+ position: 'absolute',
1176
+ bottom: Platform.OS === 'ios' ? 105 : 95, // Position well above the input area
1177
+ left: 0,
1178
+ right: 0,
1179
+ zIndex: 1,
1180
+ elevation: 3,
1181
+ shadowColor: '#000',
1182
+ shadowOffset: { width: 0, height: -1 },
1183
+ shadowOpacity: 0.05,
1184
+ shadowRadius: 2,
1185
+ }}
1186
+ >
1391
1187
  <View
1392
1188
  style={{
1393
- height: 50,
1394
- padding: 3,
1395
- backgroundColor: 'white',
1396
- borderTopWidth: 1,
1397
- borderTopColor: '#e0e0e0',
1189
+ flex: 1,
1398
1190
  flexDirection: 'row',
1399
1191
  alignItems: 'center',
1400
- margin: 0,
1401
- paddingBottom: 0,
1402
- paddingTop: 5,
1403
- position: 'absolute',
1404
- bottom: 0,
1405
- left: 0,
1406
- right: 0,
1407
- zIndex: 999,
1192
+ paddingLeft: 15,
1193
+ paddingRight: 5,
1408
1194
  }}
1409
1195
  >
1410
1196
  <View
1411
1197
  style={{
1412
- flex: 1,
1413
- flexDirection: 'row',
1414
- alignItems: 'center',
1415
- paddingHorizontal: 15,
1198
+ width: 56,
1199
+ height: 56,
1200
+ marginRight: 15,
1201
+ borderRadius: 4,
1202
+ backgroundColor: colors.gray[200],
1203
+ overflow: 'hidden',
1204
+ borderWidth: 1,
1205
+ borderColor: '#e0e0e0',
1416
1206
  }}
1417
1207
  >
1418
1208
  <Image
1419
- key={state?.context?.selectedImage}
1209
+ key={selectedImage}
1420
1210
  alt={'selected image'}
1421
- source={{ uri: state?.context?.selectedImage }}
1211
+ source={{ uri: selectedImage }}
1422
1212
  style={{
1423
- width: 36,
1424
- height: 36,
1425
- borderRadius: 5,
1426
- marginRight: 15,
1213
+ width: '100%',
1214
+ height: '100%',
1427
1215
  }}
1428
- size={'xs'}
1216
+ size={'md'}
1429
1217
  />
1218
+ {loading && (
1219
+ <View
1220
+ style={{
1221
+ position: 'absolute',
1222
+ top: 0,
1223
+ left: 0,
1224
+ right: 0,
1225
+ bottom: 0,
1226
+ backgroundColor: 'rgba(255, 255, 255, 0.7)',
1227
+ justifyContent: 'center',
1228
+ alignItems: 'center',
1229
+ }}
1230
+ >
1231
+ <Spinner size="small" color={colors.blue[500]} />
1232
+ </View>
1233
+ )}
1234
+ </View>
1430
1235
 
1431
- <TouchableHighlight
1432
- underlayColor="#dddddd"
1433
- onPress={() => safeSend({ type: ConversationActions.CLEAR_IMAGE })}
1434
- style={{
1435
- backgroundColor: '#f44336',
1436
- paddingVertical: 2,
1437
- paddingHorizontal: 5,
1438
- borderRadius: 5,
1439
- marginLeft: 10,
1440
- elevation: 3,
1441
- shadowColor: '#000',
1442
- shadowOffset: { width: 0, height: 1 },
1443
- shadowOpacity: 0.3,
1444
- shadowRadius: 2,
1445
- }}
1446
- >
1447
- <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1448
- </TouchableHighlight>
1236
+ <View style={{ flex: 1 }}>
1237
+ <Text style={{ fontSize: 14, fontWeight: '400', color: colors.gray[800] }}>
1238
+ {images[0]?.fileName || 'image_' + new Date().getTime() + '.jpg'}
1239
+ </Text>
1240
+ <Text style={{ fontSize: 12, color: colors.gray[500], marginTop: 2 }}>
1241
+ {loading ? 'Preparing...' : 'Ready to send'}
1242
+ </Text>
1449
1243
  </View>
1244
+
1245
+ <TouchableHighlight
1246
+ underlayColor={'rgba(0,0,0,0.1)'}
1247
+ onPress={() => {
1248
+ setSelectedImage('');
1249
+ setImages([]);
1250
+ }}
1251
+ style={{
1252
+ backgroundColor: colors.red[500],
1253
+ borderRadius: 24,
1254
+ width: 36,
1255
+ height: 36,
1256
+ alignItems: 'center',
1257
+ justifyContent: 'center',
1258
+ marginRight: 10,
1259
+ }}
1260
+ >
1261
+ <Ionicons name="close" size={20} color="white" />
1262
+ </TouchableHighlight>
1450
1263
  </View>
1451
- );
1452
- },
1453
- [state?.context?.selectedImage, safeSend],
1454
- );
1264
+ </View>
1265
+ );
1266
+ }, [selectedImage, loading, images]);
1455
1267
 
1456
1268
  const setImageViewerObject = (obj: any, v: boolean) => {
1457
1269
  setImageObject(obj);
@@ -1459,13 +1271,14 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1459
1271
  };
1460
1272
 
1461
1273
  const modalContent = React.useMemo(() => {
1462
- if (!imageObject) return <></>;
1274
+ if (!imageObject || !imageObject.image) return null;
1463
1275
  const { image, _id } = imageObject;
1276
+
1464
1277
  return (
1465
1278
  <CachedImage
1466
1279
  style={{ width: '100%', height: '100%' }}
1467
1280
  resizeMode={'cover'}
1468
- cacheKey={`${_id}-slack-bubble-imageKey`}
1281
+ cacheKey={`${_id}-modal-imageKey`}
1469
1282
  source={{
1470
1283
  uri: image,
1471
1284
  expiresIn: 86400,
@@ -1475,14 +1288,60 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1475
1288
  );
1476
1289
  }, [imageObject]);
1477
1290
 
1291
+ // Create a skeleton component for message bubbles with images
1478
1292
  const renderMessage = useCallback(
1479
1293
  (props: any) => {
1294
+ // Check if this message ID matches the uploading message ID
1295
+ const isUploading = props.currentMessage._id === uploadingMessageId && loading;
1296
+
1297
+ if (isUploading && props.currentMessage.image) {
1298
+ // Return a custom message skeleton during upload
1299
+ return (
1300
+ <View
1301
+ style={{
1302
+ padding: 10,
1303
+ marginBottom: 10,
1304
+ marginRight: 10,
1305
+ alignSelf: 'flex-end',
1306
+ borderRadius: 15,
1307
+ backgroundColor: colors.gray[100],
1308
+ maxWidth: '80%',
1309
+ }}
1310
+ >
1311
+ {props.currentMessage.text && props.currentMessage.text.trim() !== '' && (
1312
+ <Box
1313
+ style={{
1314
+ height: 15,
1315
+ borderRadius: 4,
1316
+ backgroundColor: colors.gray[200],
1317
+ overflow: 'hidden',
1318
+ marginBottom: 8,
1319
+ }}
1320
+ >
1321
+ <Skeleton variant="rounded" style={{ flex: 1 }} />
1322
+ </Box>
1323
+ )}
1324
+ <Box
1325
+ style={{
1326
+ height: 150,
1327
+ width: 150,
1328
+ borderRadius: 10,
1329
+ backgroundColor: colors.gray[200],
1330
+ overflow: 'hidden',
1331
+ }}
1332
+ >
1333
+ <Skeleton variant="rounded" style={{ flex: 1 }} />
1334
+ </Box>
1335
+ </View>
1336
+ );
1337
+ }
1338
+
1480
1339
  // Use memo to prevent unnecessary re-renders of each message
1481
1340
  return (
1482
1341
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1483
1342
  );
1484
1343
  },
1485
- [isShowImageViewer],
1344
+ [isShowImageViewer, uploadingMessageId, loading],
1486
1345
  );
1487
1346
 
1488
1347
  let onScroll = false;
@@ -1499,211 +1358,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1499
1358
  };
1500
1359
 
1501
1360
  const onEndReached = () => {
1502
- console.log('on end reached');
1503
1361
  if (!onScroll) return;
1504
1362
  onScroll = false;
1505
1363
  };
1506
1364
 
1507
- // Add debug logging to help diagnose the issue
1508
- useEffect(() => {
1509
- console.log('Current channel ID:', state.context.channelId);
1510
- console.log('Current state:', state.value);
1511
- console.log('Channel messages count:', state.context.channelMessages.length);
1512
- }, [state.context.channelId, state.value, state.context.channelMessages]);
1513
-
1514
- // Fix the infinite update loop in useEffect monitoring state changes
1515
- useEffect(() => {
1516
- // Only trigger effect if we have a specific state to handle
1517
- // Check if function exists and if we're in a valid state before calling implementation functions
1518
- if (state && typeof state.matches === 'function') {
1519
- if (state.matches(BaseState.FetchMessages)) {
1520
- console.log('In FetchMessages state, attempting to fetch messages');
1521
- // Use a ref to track if we've already fetched for this state update
1522
- if (!fetchInProgressRef.current) {
1523
- fetchInProgressRef.current = true;
1524
- fetchMessagesDirectly().finally(() => {
1525
- fetchInProgressRef.current = false;
1526
- });
1527
- }
1528
- } else if (state.matches(MainState.FetchMoreMessages)) {
1529
- if (!fetchMoreInProgressRef.current) {
1530
- fetchMoreInProgressRef.current = true;
1531
- fetchMoreMessagesImpl().then((result) => {
1532
- if (result.error) {
1533
- console.error('Error fetching more messages:', result.error);
1534
- safeSend({ type: 'ERROR', data: { message: result.error } });
1535
- } else {
1536
- safeSend({ type: 'FETCH_MORE_MESSAGES_SUCCESS', data: result });
1537
- }
1538
- fetchMoreInProgressRef.current = false;
1539
- });
1540
- }
1541
- } else if (state.matches(MainState.SendMessage)) {
1542
- if (!sendInProgressRef.current) {
1543
- sendInProgressRef.current = true;
1544
- sendMessageImpl().then((result) => {
1545
- if (result.error) {
1546
- console.error('Error sending message:', result.error);
1547
- safeSend({ type: 'ERROR', data: { message: result.error } });
1548
- } else {
1549
- safeSend({ type: 'SEND_MESSAGE_SUCCESS', data: result });
1550
- }
1551
- sendInProgressRef.current = false;
1552
- });
1553
- }
1554
- } else if (state.matches(MainState.SendMessageWithFile)) {
1555
- if (!sendFileInProgressRef.current) {
1556
- sendFileInProgressRef.current = true;
1557
- sendMessageWithFileImpl().then((result) => {
1558
- if (result.error) {
1559
- console.error('Error sending message with file:', result.error);
1560
- safeSend({ type: 'ERROR', data: { message: result.error } });
1561
- } else {
1562
- safeSend({ type: 'SEND_MESSAGE_WITH_FILE_SUCCESS', data: result });
1563
- }
1564
- sendFileInProgressRef.current = false;
1565
- });
1566
- }
1567
- } else if (state.matches(MainState.CreateDirectChannel)) {
1568
- if (!createChannelInProgressRef.current) {
1569
- createChannelInProgressRef.current = true;
1570
- createDirectChannelImpl().then((result) => {
1571
- if (result.error) {
1572
- console.error('Error creating direct channel:', result.error);
1573
- safeSend({ type: 'ERROR', data: { message: result.error } });
1574
- } else {
1575
- safeSend({ type: 'CREATE_DIRECT_CHANNEL_SUCCESS', data: result });
1576
- }
1577
- createChannelInProgressRef.current = false;
1578
- });
1579
- }
1580
- }
1581
- }
1582
- }, [
1583
- state?.value,
1584
- fetchMessagesDirectly,
1585
- fetchMoreMessagesImpl,
1586
- sendMessageImpl,
1587
- sendMessageWithFileImpl,
1588
- createDirectChannelImpl,
1589
- safeSend,
1590
- ]);
1591
-
1592
- // Add refs to prevent duplicate operations
1593
- const fetchInProgressRef = useRef(false);
1594
- const fetchMoreInProgressRef = useRef(false);
1595
- const sendInProgressRef = useRef(false);
1596
- const sendFileInProgressRef = useRef(false);
1597
- const createChannelInProgressRef = useRef(false);
1598
-
1599
- // Fix subscription handler to prevent infinite updates
1600
- const renderChatFooter = useCallback(() => {
1601
- return (
1602
- <>
1603
- <ImageViewerModal
1604
- isVisible={isShowImageViewer}
1605
- setVisible={setImageViewer}
1606
- modalContent={modalContent}
1607
- />
1608
- <SubscriptionHandler
1609
- channelId={state?.context?.channelId?.toString()}
1610
- subscribeToNewMessages={() =>
1611
- subscribeToMore({
1612
- document: CHAT_MESSAGE_ADDED,
1613
- variables: {
1614
- channelId: state?.context?.channelId?.toString(),
1615
- },
1616
- updateQuery: (prev, { subscriptionData }: any) => {
1617
- try {
1618
- // Log for debugging
1619
- console.log(
1620
- '🔔 Subscription data received for channel:',
1621
- state?.context?.channelId,
1622
- );
1623
-
1624
- // Check if we have valid subscription data
1625
- if (!subscriptionData?.data?.chatMessageAdded) {
1626
- console.log('🔔 No valid message in subscription data');
1627
- return prev;
1628
- }
1629
-
1630
- const newMessage = subscriptionData.data.chatMessageAdded;
1631
- console.log('🔔 New message received:', newMessage.id);
1632
-
1633
- // Check if we already have this message to avoid duplicates
1634
- const currentMessages = prev?.messages?.data || [];
1635
- if (currentMessages.some((msg) => msg.id === newMessage.id)) {
1636
- console.log('🔔 Message already in cache, skipping update');
1637
- return prev;
1638
- }
1639
-
1640
- // Capture the new message in state directly
1641
- setTimeout(() => {
1642
- try {
1643
- // Update state machine directly
1644
- console.log('🔔 Directly updating state machine with new message');
1645
- safeSend({
1646
- type: ConversationActions.SET_CHANNEL_MESSAGES,
1647
- data: {
1648
- messages: [...state.context.channelMessages, newMessage],
1649
- totalCount: (state.context.totalCount || 0) + 1,
1650
- },
1651
- });
1652
- } catch (error) {
1653
- console.error('🔔 Error updating state with subscription data:', error);
1654
- }
1655
- }, 0);
1656
-
1657
- // Update Apollo cache
1658
- const updatedData = {
1659
- ...prev,
1660
- messages: {
1661
- ...prev.messages,
1662
- data: [...currentMessages, newMessage],
1663
- totalCount: (prev?.messages?.totalCount || 0) + 1,
1664
- },
1665
- };
1666
-
1667
- console.log('🔔 Returning updated query data with new messages');
1668
- return updatedData;
1669
- } catch (error) {
1670
- console.error('🔔 Error in subscription updateQuery:', error);
1671
- return prev;
1672
- }
1673
- },
1674
- })
1675
- }
1676
- />
1677
- </>
1678
- );
1679
- }, [
1680
- isShowImageViewer,
1681
- modalContent,
1682
- state?.context?.channelId,
1683
- state?.context?.channelMessages,
1684
- subscribeToMore,
1685
- safeSend,
1686
- ]);
1687
-
1688
- // Add optimized listViewProps to reduce re-renders and improve list performance
1689
- const listViewProps = useMemo(
1690
- () => ({
1691
- onEndReached: onEndReached,
1692
- onEndReachedThreshold: 0.5,
1693
- onMomentumScrollBegin: onMomentumScrollBegin,
1694
- removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1695
- initialNumToRender: 10, // Reduce initial render amount
1696
- maxToRenderPerBatch: 7, // Reduce number in each render batch
1697
- windowSize: 7, // Reduce the window size
1698
- updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
1699
- keyExtractor: (item) => item._id, // Add explicit key extractor
1700
- }),
1701
- [onEndReached, onMomentumScrollBegin],
1702
- );
1703
-
1704
1365
  // Add a loader for when more messages are being loaded
1705
1366
  const renderLoadEarlier = useCallback(() => {
1706
- return state?.context?.loadingOldMessages ? (
1367
+ return loadingOldMessages ? (
1707
1368
  <View
1708
1369
  style={{
1709
1370
  padding: 10,
@@ -1715,7 +1376,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1715
1376
  <Spinner size="small" color="#3b82f6" />
1716
1377
  </View>
1717
1378
  ) : null;
1718
- }, [state?.context?.loadingOldMessages]);
1379
+ }, [loadingOldMessages]);
1719
1380
 
1720
1381
  // Add renderInputToolbar function
1721
1382
  const renderInputToolbar = useCallback((props) => {
@@ -1739,15 +1400,95 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1739
1400
  );
1740
1401
  }, []);
1741
1402
 
1403
+ // Create a memoized ImageViewerModal component
1404
+ const imageViewerModal = useMemo(
1405
+ () => (
1406
+ <ImageViewerModal isVisible={isShowImageViewer} setVisible={setImageViewer} modalContent={modalContent} />
1407
+ ),
1408
+ [isShowImageViewer, modalContent],
1409
+ );
1410
+
1411
+ // Create a memoized subscription handler component
1412
+ const subscriptionHandler = useMemo(
1413
+ () => (
1414
+ <SubscriptionHandler
1415
+ channelId={channelId?.toString()}
1416
+ subscribeToNewMessages={() =>
1417
+ subscribeToMore({
1418
+ document: CHAT_MESSAGE_ADDED,
1419
+ variables: {
1420
+ channelId: channelId?.toString(),
1421
+ },
1422
+ // Let type policy handle the merge
1423
+ })
1424
+ }
1425
+ />
1426
+ ),
1427
+ [channelId, subscribeToMore, auth?.id],
1428
+ );
1429
+
1430
+ // Create a memoized renderChatFooter function
1431
+ const renderChatFooter = useCallback(() => {
1432
+ return (
1433
+ <>
1434
+ {imageViewerModal}
1435
+ {subscriptionHandler}
1436
+ </>
1437
+ );
1438
+ }, [imageViewerModal, subscriptionHandler]);
1439
+
1440
+ // Add optimized listViewProps to reduce re-renders and improve list performance
1441
+ const listViewProps = useMemo(
1442
+ () => ({
1443
+ onEndReached: onEndReached,
1444
+ onEndReachedThreshold: 0.5,
1445
+ onMomentumScrollBegin: onMomentumScrollBegin,
1446
+ removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1447
+ initialNumToRender: 10, // Reduce initial render amount
1448
+ maxToRenderPerBatch: 7, // Reduce number in each render batch
1449
+ windowSize: 7, // Reduce the window size
1450
+ updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
1451
+ keyExtractor: (item) => item._id, // Add explicit key extractor
1452
+ }),
1453
+ [onEndReached, onMomentumScrollBegin],
1454
+ );
1455
+
1456
+ // Debug helper function to inspect files in messages
1457
+ const debugFileData = useCallback((message: any, prefix: string = 'Message') => {
1458
+ if (__DEV__) {
1459
+ console.log(
1460
+ `${prefix} ID: ${message?.id}, ` +
1461
+ `Has files object: ${!!message?.files}, ` +
1462
+ `Files typename: ${message?.files?.__typename}, ` +
1463
+ `Files data exists: ${!!message?.files?.data}, ` +
1464
+ `Files count: ${message?.files?.data?.length || 0}`,
1465
+ );
1466
+
1467
+ if (message?.files?.data && message?.files?.data?.length > 0) {
1468
+ const file = message?.files?.data[0];
1469
+ console.log(
1470
+ `File[0] ID: ${file?.id}, ` +
1471
+ `URL: ${file?.url?.substring(0, 30)}..., ` +
1472
+ `Name: ${file?.name}, ` +
1473
+ `Type: ${file?.mimeType}`,
1474
+ );
1475
+ }
1476
+ }
1477
+ }, []);
1478
+
1742
1479
  // Return optimized component with performance improvements
1743
1480
  return (
1744
1481
  <View
1745
1482
  style={{
1746
1483
  flex: 1,
1747
1484
  backgroundColor: 'white',
1485
+ position: 'relative',
1748
1486
  }}
1749
1487
  >
1750
- {state?.matches && state.matches(BaseState.FetchMessages) && <Spinner color={'#3b82f6'} />}
1488
+ {messageLoading && <Spinner color={'#3b82f6'} />}
1489
+
1490
+ {/* Render the image preview directly in the container so it's properly positioned */}
1491
+ {selectedImage ? renderAccessory() : null}
1751
1492
 
1752
1493
  <GiftedChat
1753
1494
  ref={messageRootListRef}
@@ -1757,20 +1498,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1757
1498
  listViewProps={{
1758
1499
  ...listViewProps,
1759
1500
  contentContainerStyle: {
1760
- paddingBottom: 10,
1501
+ paddingBottom: selectedImage ? 90 : 0, // Add padding at the bottom when image is selected
1761
1502
  },
1762
- keyboardShouldPersistTaps: 'handled',
1763
1503
  }}
1764
1504
  onSend={handleSend}
1765
- text={safeContextProperty('messageText', ' ') || ' '}
1766
- onInputTextChanged={(text) =>
1767
- safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText: text } })
1768
- }
1769
- renderFooter={() =>
1770
- safeContextProperty('loading') || safeContextProperty('imageLoading') ? (
1771
- <Spinner color={'#3b82f6'} />
1772
- ) : null
1773
- }
1505
+ text={messageText || ' '}
1506
+ onInputTextChanged={(text) => setMessageText(text)}
1507
+ renderFooter={() => (loading && !images.length ? <Spinner color={'#3b82f6'} /> : null)}
1774
1508
  scrollToBottom
1775
1509
  user={{
1776
1510
  _id: auth?.id || '',
@@ -1781,14 +1515,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1781
1515
  renderMessageText={renderMessageText}
1782
1516
  renderInputToolbar={renderInputToolbar}
1783
1517
  minInputToolbarHeight={50}
1784
- renderActions={safeContextProperty('channelId') && renderActions}
1785
- renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
1518
+ renderActions={channelId && renderActions}
1786
1519
  renderMessage={renderMessage}
1787
1520
  renderChatFooter={renderChatFooter}
1788
1521
  renderLoadEarlier={renderLoadEarlier}
1789
- loadEarlier={state?.context?.totalCount > state?.context?.channelMessages?.length}
1790
- isLoadingEarlier={state?.context?.loadingOldMessages}
1791
- bottomOffset={Platform.OS === 'ios' ? 10 : 0} // Reduce bottom offset
1522
+ loadEarlier={totalCount > channelMessages.length}
1523
+ isLoadingEarlier={loadingOldMessages}
1524
+ bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0} // Adjust bottom offset based on image preview
1792
1525
  textInputProps={{
1793
1526
  style: {
1794
1527
  borderWidth: 1,
@@ -1832,12 +1565,9 @@ const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptio
1832
1565
  useEffect(() => {
1833
1566
  // Don't set up subscription if there's no channel ID
1834
1567
  if (!channelId) {
1835
- console.log('⚠️ No channel ID for subscription');
1836
1568
  return;
1837
1569
  }
1838
1570
 
1839
- console.log('🔄 Setting up NEW subscription for channel:', channelId);
1840
-
1841
1571
  // Call the subscribe function and store the unsubscribe function
1842
1572
  const unsubscribe = subscribeToNewMessages();
1843
1573
 
@@ -1847,7 +1577,6 @@ const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptio
1847
1577
  // Return cleanup function
1848
1578
  return () => {
1849
1579
  if (unsubscribe && typeof unsubscribe === 'function') {
1850
- console.log('🔄 Cleaning up subscription for channel:', channelIdRef.current);
1851
1580
  unsubscribe();
1852
1581
  }
1853
1582
  };