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

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 +4 -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 +75 -271
  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 +478 -944
  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 +104 -368
  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 +660 -1060
  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,241 @@ 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',
144
+ notifyOnNetworkStatusChange: true,
479
145
  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
- }
146
+ // MESSAGE QUERY COMPLETED
501
147
  },
502
148
  onError: (error) => {
503
- console.error('MESSAGE QUERY ERROR:', error);
504
- // Report error to state machine
505
- safeSend({ type: 'ERROR', data: { message: String(error) } });
149
+ setError(String(error));
506
150
  },
507
151
  });
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
152
 
517
- try {
518
- console.log('💫 FETCHING messages for channel:', channelId);
153
+ // Extract messages from the query data
154
+ const channelMessages = useMemo(() => {
155
+ return data?.messages?.data || [];
156
+ }, [data?.messages?.data]);
519
157
 
520
- // Use loading state to prevent duplicate fetches
521
- send({ type: ConversationActions.START_LOADING });
158
+ // Get total message count
159
+ const totalCount = useMemo(() => {
160
+ return data?.messages?.totalCount || 0;
161
+ }, [data?.messages?.totalCount]);
522
162
 
523
- const response = await refetch({
524
- channelId: channelId.toString(),
525
- parentId: null,
526
- limit: MESSAGES_PER_PAGE,
527
- skip: 0,
528
- });
163
+ // Clear messages when component unmounts
164
+ useEffect(() => {
165
+ return () => {
166
+ isMounted.current = false;
167
+ };
168
+ }, []);
529
169
 
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 });
170
+ // Update channelId from props or navigation params
171
+ useEffect(() => {
172
+ const currentChannelId = initialChannelId || currentRoute?.params?.channelId;
173
+ if (currentChannelId) {
174
+ setChannelId(currentChannelId);
175
+ }
176
+ }, [initialChannelId, currentRoute]);
177
+
178
+ // Focus/unfocus behavior
179
+ useFocusEffect(
180
+ React.useCallback(() => {
181
+ if (channelId) {
182
+ // Refresh messages when screen comes into focus
183
+ refetch();
547
184
  }
548
- } catch (error) {
549
- console.error('ERROR fetching messages:', error);
550
- send({ type: ConversationActions.STOP_LOADING });
185
+ return () => {
186
+ // Nothing needed on unfocus
187
+ };
188
+ }, [channelId, isFocused, refetch]),
189
+ );
190
+
191
+ // Loading state for image selection
192
+ useEffect(() => {
193
+ if (selectedImage) {
194
+ setLoading(false);
551
195
  }
552
- }, [safeGetContext, refetch, safeSend]);
196
+ }, [selectedImage]);
553
197
 
198
+ // Fetch more messages function
554
199
  const fetchMoreMessagesImpl = useCallback(async () => {
555
200
  try {
201
+ setLoadingOldMessages(true);
556
202
  const response = await fetchMoreMessages({
557
203
  variables: {
558
- channelId: state.context.channelId?.toString(),
204
+ channelId: channelId?.toString(),
559
205
  parentId: null,
560
- skip: state.context.channelMessages.length,
206
+ skip: channelMessages.length,
561
207
  },
208
+ // updateQuery: (prev, { fetchMoreResult }) => {
209
+ // if (!fetchMoreResult || !fetchMoreResult.messages) return prev;
210
+
211
+ // // Create a new array of all messages deduped by ID
212
+ // const combinedMessages = [...prev.messages.data, ...fetchMoreResult.messages.data].filter(
213
+ // (message, index, self) => index === self.findIndex((m) => m.id === message.id),
214
+ // );
215
+
216
+ // return {
217
+ // ...prev,
218
+ // messages: {
219
+ // ...prev.messages,
220
+ // data: combinedMessages,
221
+ // totalCount: fetchMoreResult.messages.totalCount,
222
+ // __typename: prev.messages.__typename,
223
+ // },
224
+ // };
225
+ // },
562
226
  });
563
227
 
228
+ setLoadingOldMessages(false);
564
229
  if (!response?.data?.messages?.data) {
565
230
  return { error: 'No messages returned' };
566
231
  }
567
232
 
568
233
  return { messages: response.data.messages.data };
569
234
  } catch (error) {
235
+ setLoadingOldMessages(false);
236
+ setError(String(error));
570
237
  return { error: String(error) };
571
238
  }
572
- }, [state.context.channelId, state.context.channelMessages.length, fetchMoreMessages]);
239
+ }, [channelId, channelMessages.length, fetchMoreMessages]);
573
240
 
241
+ // Send message function
574
242
  const sendMessageImpl = useCallback(async () => {
575
243
  try {
244
+ // Store the current message text and clear input immediately for better UX
245
+ const currentMessageText = messageText;
246
+ setMessageText('');
247
+
576
248
  const notificationData: IExpoNotificationData = {
577
249
  url: config.INBOX_MESSEGE_PATH,
578
- params: { channelId: state.context.channelId, hideTabBar: true },
250
+ params: { channelId, hideTabBar: true },
579
251
  screen: 'DialogMessages',
580
252
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
581
253
  };
582
254
 
255
+ // Create optimistic message with consistent structure
256
+ const messageId = objectId();
257
+ const optimisticMessage = {
258
+ __typename: 'Post' as const,
259
+ id: messageId,
260
+ message: currentMessageText,
261
+ createdAt: new Date().toISOString(),
262
+ updatedAt: new Date().toISOString(),
263
+ author: {
264
+ __typename: 'UserAccount' as const,
265
+ id: auth?.id,
266
+ givenName: auth?.givenName || '',
267
+ familyName: auth?.familyName || '',
268
+ picture: auth?.picture || '',
269
+ username: auth?.username || '',
270
+ email: auth?.email || '',
271
+ alias: [] as string[],
272
+ tokens: [],
273
+ },
274
+ isDelivered: true,
275
+ isRead: false,
276
+ type: 'TEXT' as any,
277
+ parentId: null,
278
+ fromServer: false,
279
+ channel: {
280
+ __typename: 'Channel' as const,
281
+ id: channelId,
282
+ },
283
+ propsConfiguration: {
284
+ __typename: 'MachineConfiguration' as const,
285
+ resource: '' as any, // Cast to any to bypass the URI type check
286
+ },
287
+ props: {},
288
+ files: {
289
+ __typename: 'FilesInfo' as const,
290
+ data: [],
291
+ totalCount: 0,
292
+ },
293
+ replies: {
294
+ __typename: 'Messages' as const,
295
+ data: [],
296
+ totalCount: 0,
297
+ },
298
+ };
299
+
583
300
  const response = await sendMsg({
584
301
  variables: {
585
- channelId: state.context.channelId,
586
- content: state.context.messageText,
302
+ channelId,
303
+ content: currentMessageText,
587
304
  notificationParams: notificationData,
588
305
  },
306
+ optimisticResponse: {
307
+ __typename: 'Mutation',
308
+ sendMessage: optimisticMessage,
309
+ },
589
310
  });
590
311
 
591
312
  return { message: response.data?.sendMessage };
592
313
  } catch (error) {
314
+ setLoading(false);
315
+ setError(String(error));
593
316
  return { error: String(error) };
594
317
  }
595
- }, [state.context.channelId, state.context.messageText, sendMsg]);
318
+ }, [channelId, messageText, sendMsg, auth]);
596
319
 
597
- // Fix the image selection process to ensure proper format for upload
320
+ // Image selection handler
598
321
  const onSelectImages = async () => {
599
- safeSend({ type: ConversationActions.START_LOADING });
322
+ setLoading(true);
600
323
 
601
324
  try {
602
- console.log('Starting image picker...');
603
325
  let imageSource = await ImagePicker.launchImageLibraryAsync({
604
326
  mediaTypes: ImagePicker.MediaTypeOptions.Images,
605
327
  allowsEditing: true,
@@ -610,22 +332,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
610
332
  });
611
333
 
612
334
  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
335
  // Get the asset
625
336
  const selectedAsset = imageSource?.assets?.[0];
626
337
  if (!selectedAsset) {
627
- console.error('No asset found in selected image');
628
- safeSend({ type: ConversationActions.STOP_LOADING });
338
+ setLoading(false);
629
339
  return;
630
340
  }
631
341
 
@@ -641,58 +351,49 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
641
351
  mimeType: 'image/jpeg',
642
352
  };
643
353
 
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
354
  // 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');
355
+ setSelectedImage(previewImage);
356
+ setImages([asset]);
661
357
  } else {
662
- console.log('Image selection cancelled');
663
- safeSend({ type: ConversationActions.STOP_LOADING });
358
+ setLoading(false);
664
359
  }
665
360
  } catch (error) {
666
- console.error('Error selecting image:', error);
667
- safeSend({ type: ConversationActions.STOP_LOADING });
361
+ setLoading(false);
668
362
  }
669
363
  };
670
364
 
671
- // Update the sendMessageWithFileImpl function to fix image uploads
365
+ // Add a state variable to track which message should show the skeleton
366
+ const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
367
+
368
+ // Send message with file function - update to set and clear uploadingMessageId
672
369
  const sendMessageWithFileImpl = useCallback(async () => {
673
370
  try {
674
- console.log('Executing sendMessageWithFileImpl');
371
+ // For file uploads, we still need loading state since we need to wait for the file upload
372
+ setLoading(true);
675
373
 
676
374
  // Generate a unique post ID for the message
677
375
  const postId = objectId();
678
- console.log('Generated postId for file upload:', postId);
376
+
377
+ // Set the message ID that should show the skeleton
378
+ setUploadingMessageId(postId);
679
379
 
680
380
  // Prepare notification data
681
381
  const notificationData: IExpoNotificationData = {
682
382
  url: config.INBOX_MESSEGE_PATH,
683
- params: { channelId: state.context.channelId, hideTabBar: true },
383
+ params: { channelId, hideTabBar: true },
684
384
  screen: 'DialogMessages',
685
385
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
686
386
  };
687
387
 
688
388
  // Safety check for images
689
- if (!state.context.images || state.context.images.length === 0) {
690
- console.error('No images found in state');
389
+ if (!images || images.length === 0) {
390
+ setLoading(false);
391
+ setUploadingMessageId(null);
691
392
  return { error: 'No images available to upload' };
692
393
  }
693
394
 
694
395
  // Format the images for upload if needed
695
- const imagesToUpload = state.context.images.map((img) => {
396
+ const imagesToUpload = images.map((img) => {
696
397
  // Ensure the image has all required properties
697
398
  return {
698
399
  ...img,
@@ -702,20 +403,82 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
702
403
  };
703
404
  });
704
405
 
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
- );
406
+ // Store current message text and clear inputs immediately for better UX
407
+ const currentMessageText = messageText;
408
+ setMessageText('');
409
+
410
+ // Create file info for optimistic response
411
+ const optimisticFileInfo = {
412
+ __typename: 'FileInfo' as const,
413
+ id: objectId(),
414
+ url: selectedImage,
415
+ name: imagesToUpload[0]?.name || 'image.jpg',
416
+ extension: 'jpg',
417
+ mimeType: 'image/jpeg',
418
+ size: 0,
419
+ refType: FileRefType.Post,
420
+ height: imagesToUpload[0]?.height || 0,
421
+ width: imagesToUpload[0]?.width || 0,
422
+ };
423
+
424
+ // Create optimistic message with file
425
+ const optimisticMessage = {
426
+ __typename: 'Post' as const,
427
+ id: postId,
428
+ message: currentMessageText || ' ',
429
+ createdAt: new Date().toISOString(),
430
+ updatedAt: new Date().toISOString(),
431
+ author: {
432
+ __typename: 'UserAccount' as const,
433
+ id: auth?.id,
434
+ givenName: auth?.givenName || '',
435
+ familyName: auth?.familyName || '',
436
+ picture: auth?.picture || '',
437
+ username: auth?.username || '',
438
+ email: auth?.email || '',
439
+ alias: [] as string[],
440
+ tokens: [],
441
+ },
442
+ isDelivered: true,
443
+ isRead: false,
444
+ type: 'TEXT' as any,
445
+ parentId: null,
446
+ fromServer: false,
447
+ channel: {
448
+ __typename: 'Channel' as const,
449
+ id: channelId,
450
+ },
451
+ propsConfiguration: {
452
+ __typename: 'MachineConfiguration' as const,
453
+ resource: '' as any, // Cast to any to bypass the URI type check
454
+ },
455
+ props: {},
456
+ files: {
457
+ __typename: 'FilesInfo' as const,
458
+ data: [
459
+ {
460
+ __typename: 'FileInfo' as const,
461
+ id: objectId(),
462
+ url: selectedImage,
463
+ name: imagesToUpload[0]?.name || 'image.jpg',
464
+ extension: 'jpg',
465
+ mimeType: 'image/jpeg',
466
+ size: 0,
467
+ refType: FileRefType.Post,
468
+ height: imagesToUpload[0]?.height || 0,
469
+ width: imagesToUpload[0]?.width || 0,
470
+ },
471
+ ] as any,
472
+ totalCount: 1,
473
+ },
474
+ replies: {
475
+ __typename: 'Messages' as const,
476
+ data: [],
477
+ totalCount: 0,
478
+ },
479
+ };
716
480
 
717
481
  // Upload the files
718
- console.log('Starting file upload...');
719
482
  const uploadResponse = await startUpload({
720
483
  file: imagesToUpload,
721
484
  saveUploadedFile: {
@@ -726,89 +489,66 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
726
489
  },
727
490
  });
728
491
 
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
492
  if (uploadResponse?.error) {
737
- console.error('Upload error:', uploadResponse.error);
493
+ setLoading(false);
494
+ setUploadingMessageId(null);
738
495
  return { error: String(uploadResponse.error) };
739
496
  }
740
497
 
741
498
  // Get uploaded file IDs
742
499
  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
500
  const files = uploadedFiles?.map((f: any) => f.id) ?? null;
751
501
 
752
- console.log('Files uploaded successfully. File IDs:', files);
753
-
754
502
  // 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
503
  const response = await sendMsg({
764
504
  variables: {
765
505
  postId,
766
- channelId: state.context.channelId,
767
- content: state.context.messageText || ' ', // Use a space if no text
506
+ channelId,
507
+ content: currentMessageText || ' ', // Use a space if no text
768
508
  files,
769
509
  notificationParams: notificationData,
770
510
  },
511
+ optimisticResponse: {
512
+ __typename: 'Mutation',
513
+ sendMessage: optimisticMessage,
514
+ },
771
515
  });
772
516
 
773
517
  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
- }
786
-
787
518
  // 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);
519
+ setSelectedImage('');
520
+ setImages([]);
793
521
  }
794
522
 
523
+ setLoading(false);
524
+ setUploadingMessageId(null);
795
525
  return { message: response.data?.sendMessage };
796
526
  } catch (error) {
797
- console.error('Error in sendMessageWithFileImpl:', error);
527
+ setLoading(false);
528
+ setUploadingMessageId(null);
529
+ setError(String(error));
798
530
  return { error: String(error) };
799
531
  }
800
- }, [state.context.channelId, state.context.messageText, state.context.images, startUpload, sendMsg, safeSend]);
532
+ }, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth]);
801
533
 
534
+ // Create direct channel implementation
802
535
  const createDirectChannelImpl = useCallback(async () => {
803
536
  try {
537
+ setLoading(true);
804
538
  if (
805
539
  !rest?.isCreateNewChannel ||
806
540
  rest?.newChannelData?.type !== RoomType?.Direct ||
807
541
  !rest?.newChannelData?.userIds?.length
808
542
  ) {
543
+ setLoading(false);
809
544
  return { error: 'Invalid channel data' };
810
545
  }
811
546
 
547
+ // Store current message text
548
+ const currentMessageText = messageText;
549
+ // Clear message text immediately for better UX
550
+ setMessageText('');
551
+
812
552
  const response = await addDirectChannel({
813
553
  variables: {
814
554
  receiver: [...(rest?.newChannelData?.userIds ?? [])],
@@ -817,10 +557,12 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
817
557
  });
818
558
 
819
559
  if (!response?.data?.createDirectChannel?.id) {
560
+ setLoading(false);
820
561
  return { error: 'Failed to create channel' };
821
562
  }
822
563
 
823
564
  const newChannelId = response.data.createDirectChannel.id;
565
+ setChannelId(newChannelId);
824
566
 
825
567
  const notificationData: IExpoNotificationData = {
826
568
  url: config.INBOX_MESSEGE_PATH,
@@ -829,113 +571,74 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
829
571
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
830
572
  };
831
573
 
574
+ // Create unique message ID for optimistic response
575
+ const messageId = objectId();
576
+
577
+ // Fix the createDirectChannelImpl optimisticMessage with all required fields
578
+ const optimisticMessage = {
579
+ __typename: 'Post' as const,
580
+ id: messageId,
581
+ message: currentMessageText,
582
+ createdAt: new Date().toISOString(),
583
+ updatedAt: new Date().toISOString(),
584
+ author: {
585
+ __typename: 'UserAccount' as const,
586
+ id: auth?.id,
587
+ givenName: auth?.givenName || '',
588
+ familyName: auth?.familyName || '',
589
+ picture: auth?.picture || '',
590
+ username: auth?.username || '',
591
+ email: auth?.email || '',
592
+ alias: [] as string[],
593
+ tokens: [],
594
+ },
595
+ isDelivered: true,
596
+ isRead: false,
597
+ type: 'TEXT' as any,
598
+ parentId: null,
599
+ fromServer: false,
600
+ channel: {
601
+ __typename: 'Channel' as const,
602
+ id: newChannelId,
603
+ },
604
+ propsConfiguration: {
605
+ __typename: 'MachineConfiguration' as const,
606
+ resource: '' as any, // Cast to any to bypass the URI type check
607
+ },
608
+ props: {},
609
+ files: {
610
+ __typename: 'FilesInfo' as const,
611
+ data: [],
612
+ totalCount: 0,
613
+ },
614
+ replies: {
615
+ __typename: 'Messages' as const,
616
+ data: [],
617
+ totalCount: 0,
618
+ },
619
+ };
620
+
621
+ // Send message in the new channel
832
622
  await sendMsg({
833
623
  variables: {
834
624
  channelId: newChannelId,
835
- content: state.context.messageText,
625
+ content: currentMessageText,
836
626
  notificationParams: notificationData,
837
627
  },
628
+ optimisticResponse: {
629
+ __typename: 'Mutation',
630
+ sendMessage: optimisticMessage,
631
+ },
838
632
  });
839
633
 
634
+ setLoading(false);
840
635
  return { channelId: newChannelId };
841
636
  } catch (error) {
637
+ setLoading(false);
638
+ setError(String(error));
842
639
  return { error: String(error) };
843
640
  }
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]);
641
+ }, [rest, messageText, addDirectChannel, sendMsg, auth]);
939
642
 
940
643
  // Optimize onFetchOld by adding debounce logic
941
644
  const onFetchOld = useCallback(() => {
@@ -943,49 +646,80 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
943
646
  if (fetchOldDebounceRef.current) return;
944
647
 
945
648
  // Check if we need to fetch more messages
946
- if (
947
- state?.context?.totalCount > state?.context?.channelMessages?.length &&
948
- !state?.context?.loadingOldMessages
949
- ) {
649
+ if (totalCount > channelMessages.length && !loadingOldMessages) {
950
650
  // Set debounce
951
651
  fetchOldDebounceRef.current = true;
952
652
 
953
- // Send fetch event
954
- send({ type: ConversationActions.FETCH_MORE_MESSAGES });
653
+ // Fetch more messages
654
+ fetchMoreMessagesImpl();
955
655
 
956
656
  // Clear debounce after a timeout
957
657
  setTimeout(() => {
958
658
  fetchOldDebounceRef.current = false;
959
659
  }, 1000);
960
660
  }
961
- }, [state?.context?.totalCount, state?.context?.channelMessages, state?.context?.loadingOldMessages]);
962
-
963
- // Add debounce ref
964
- const fetchOldDebounceRef = useRef(false);
661
+ }, [totalCount, channelMessages.length, loadingOldMessages, fetchMoreMessagesImpl]);
965
662
 
966
663
  const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
967
664
  const paddingToTop = 60;
968
665
  return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
969
666
  };
970
667
 
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);
668
+ // Transform the message data for GiftedChat
669
+ const messageList = useMemo(() => {
670
+ // Short-circuit if no messages to process
671
+ if (!channelMessages || channelMessages.length === 0) {
672
+ return [];
979
673
  }
980
- return new File([u8arr], filename, { type: mime });
981
- };
982
674
 
983
- // Fix the render send function to ensure it works for image-only messages
675
+ // Use a more efficient approach - pre-filter messages once
676
+ const filteredMessages = uniqBy(channelMessages, ({ id }) => id);
677
+
678
+ // Skip processing if no filtered messages
679
+ if (filteredMessages.length === 0) {
680
+ return [];
681
+ }
682
+
683
+ // Transform messages only once and return
684
+ return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
685
+ const date = new Date(msg.createdAt);
686
+
687
+ // Extract image URL from files data
688
+ let imageUrl = null;
689
+ if (msg.files?.data && msg.files.data.length > 0) {
690
+ const fileData = msg.files.data[0];
691
+ if (fileData && fileData.url) {
692
+ imageUrl = fileData.url;
693
+ }
694
+ }
695
+
696
+ // Create message in a more direct way
697
+ return {
698
+ _id: msg.id,
699
+ text: msg.message,
700
+ createdAt: date,
701
+ user: {
702
+ _id: msg.author?.id || '',
703
+ name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
704
+ avatar: msg.author?.picture || '',
705
+ },
706
+ image: imageUrl,
707
+ sent: msg?.isDelivered,
708
+ received: msg?.isRead,
709
+ type: msg?.type,
710
+ propsConfiguration: msg?.propsConfiguration,
711
+ replies: msg?.replies ?? [],
712
+ isShowThreadMessage,
713
+ };
714
+ });
715
+ }, [channelMessages, isShowThreadMessage]);
716
+
717
+ // Render the send button
984
718
  const renderSend = useCallback(
985
719
  (props) => {
986
720
  // 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;
721
+ const hasContent = !!props.text || images?.length > 0;
722
+ const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
989
723
 
990
724
  return (
991
725
  <Send
@@ -1011,157 +745,58 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1011
745
  </Send>
1012
746
  );
1013
747
  },
1014
- [state?.context?.channelId, state?.context?.images, rest?.isCreateNewChannel],
748
+ [channelId, images, rest?.isCreateNewChannel],
1015
749
  );
1016
750
 
1017
- // Fix the handleSend function to properly handle image-only messages
751
+ // Handle send for messages
1018
752
  const handleSend = useCallback(
1019
753
  async (messages) => {
1020
754
  // 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);
755
+ const newMessageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
1024
756
 
1025
757
  // 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');
758
+ if (!channelId && !rest?.isCreateNewChannel) {
1028
759
  return;
1029
760
  }
1030
761
 
1031
762
  // 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;
763
+ const hasText = !!newMessageText && newMessageText !== ' ';
764
+ const hasImages = images && images.length > 0;
1034
765
 
1035
766
  if (!hasText && !hasImages) {
1036
- console.log('Nothing to send - no text or images');
1037
767
  return;
1038
768
  }
1039
769
 
1040
- // Set the message text in the state (even if empty for image-only messages)
1041
- safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText } });
770
+ // Update the message text state - now handled in send functions for better UX
771
+ setMessageText(newMessageText);
1042
772
 
1043
773
  // Handle direct channel creation if needed
1044
- if (rest?.isCreateNewChannel && !state.context.channelId) {
774
+ if (rest?.isCreateNewChannel && !channelId) {
1045
775
  if (rest?.newChannelData?.type === RoomType?.Direct) {
1046
- safeSend({ type: ConversationActions.CREATE_DIRECT_CHANNEL });
776
+ createDirectChannelImpl();
1047
777
  }
1048
778
  return;
1049
779
  }
1050
780
 
1051
781
  // Send message with or without image based on state
1052
782
  if (hasImages) {
1053
- console.log('Sending message with file');
1054
- safeSend({ type: ConversationActions.SEND_MESSAGE_WITH_FILE });
783
+ sendMessageWithFileImpl();
1055
784
  } else {
1056
- console.log('Sending text-only message');
1057
- safeSend({ type: ConversationActions.SEND_MESSAGE });
785
+ sendMessageImpl();
1058
786
  }
1059
787
  },
1060
- [state.context.channelId, state.context.images, rest?.isCreateNewChannel, rest?.newChannelData?.type, safeSend],
788
+ [
789
+ channelId,
790
+ images,
791
+ rest?.isCreateNewChannel,
792
+ rest?.newChannelData?.type,
793
+ createDirectChannelImpl,
794
+ sendMessageWithFileImpl,
795
+ sendMessageImpl,
796
+ ],
1061
797
  );
1062
798
 
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
799
+ // Render message text with customizations for alerts and replies
1165
800
  const renderMessageText = useCallback(
1166
801
  (props: any) => {
1167
802
  const { currentMessage } = props;
@@ -1226,7 +861,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1226
861
  onPress={() => {
1227
862
  if (currentMessage?.isShowThreadMessage)
1228
863
  navigation.navigate(config.THREAD_MESSEGE_PATH, {
1229
- channelId: state?.context?.channelId,
864
+ channelId: channelId,
1230
865
  title: 'Message',
1231
866
  postParentId: currentMessage?._id,
1232
867
  isPostParentIdThread: true,
@@ -1291,7 +926,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1291
926
  onPress={() => {
1292
927
  if (currentMessage?.isShowThreadMessage)
1293
928
  navigation.navigate(config.THREAD_MESSEGE_PATH, {
1294
- channelId: state?.context?.channelId,
929
+ channelId: channelId,
1295
930
  title: 'Message',
1296
931
  postParentId: currentMessage?._id,
1297
932
  isPostParentIdThread: true,
@@ -1343,9 +978,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1343
978
  );
1344
979
  }
1345
980
  },
1346
- [navigation, state?.context?.channelId, role],
981
+ [navigation, channelId, role],
1347
982
  );
1348
983
 
984
+ // Render action buttons (including image upload)
1349
985
  const renderActions = (props) => {
1350
986
  return (
1351
987
  <Actions
@@ -1379,79 +1015,115 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1379
1015
  };
1380
1016
 
1381
1017
  // 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
- }
1018
+ const renderAccessory = useCallback(() => {
1019
+ if (!selectedImage) {
1020
+ return null;
1021
+ }
1389
1022
 
1390
- return (
1023
+ return (
1024
+ <View
1025
+ style={{
1026
+ height: 70,
1027
+ backgroundColor: 'white',
1028
+ borderTopWidth: 1,
1029
+ borderTopColor: '#e0e0e0',
1030
+ flexDirection: 'row',
1031
+ alignItems: 'center',
1032
+ margin: 0,
1033
+ padding: 0,
1034
+ paddingVertical: 0,
1035
+ position: 'absolute',
1036
+ bottom: Platform.OS === 'ios' ? 105 : 95, // Position well above the input area
1037
+ left: 0,
1038
+ right: 0,
1039
+ zIndex: 1,
1040
+ elevation: 3,
1041
+ shadowColor: '#000',
1042
+ shadowOffset: { width: 0, height: -1 },
1043
+ shadowOpacity: 0.05,
1044
+ shadowRadius: 2,
1045
+ }}
1046
+ >
1391
1047
  <View
1392
1048
  style={{
1393
- height: 50,
1394
- padding: 3,
1395
- backgroundColor: 'white',
1396
- borderTopWidth: 1,
1397
- borderTopColor: '#e0e0e0',
1049
+ flex: 1,
1398
1050
  flexDirection: 'row',
1399
1051
  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,
1052
+ paddingLeft: 15,
1053
+ paddingRight: 5,
1408
1054
  }}
1409
1055
  >
1410
1056
  <View
1411
1057
  style={{
1412
- flex: 1,
1413
- flexDirection: 'row',
1414
- alignItems: 'center',
1415
- paddingHorizontal: 15,
1058
+ width: 56,
1059
+ height: 56,
1060
+ marginRight: 15,
1061
+ borderRadius: 4,
1062
+ backgroundColor: colors.gray[200],
1063
+ overflow: 'hidden',
1064
+ borderWidth: 1,
1065
+ borderColor: '#e0e0e0',
1416
1066
  }}
1417
1067
  >
1418
1068
  <Image
1419
- key={state?.context?.selectedImage}
1069
+ key={selectedImage}
1420
1070
  alt={'selected image'}
1421
- source={{ uri: state?.context?.selectedImage }}
1071
+ source={{ uri: selectedImage }}
1422
1072
  style={{
1423
- width: 36,
1424
- height: 36,
1425
- borderRadius: 5,
1426
- marginRight: 15,
1073
+ width: '100%',
1074
+ height: '100%',
1427
1075
  }}
1428
- size={'xs'}
1076
+ size={'md'}
1429
1077
  />
1078
+ {loading && (
1079
+ <View
1080
+ style={{
1081
+ position: 'absolute',
1082
+ top: 0,
1083
+ left: 0,
1084
+ right: 0,
1085
+ bottom: 0,
1086
+ backgroundColor: 'rgba(255, 255, 255, 0.7)',
1087
+ justifyContent: 'center',
1088
+ alignItems: 'center',
1089
+ }}
1090
+ >
1091
+ <Spinner size="small" color={colors.blue[500]} />
1092
+ </View>
1093
+ )}
1094
+ </View>
1430
1095
 
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>
1096
+ <View style={{ flex: 1 }}>
1097
+ <Text style={{ fontSize: 14, fontWeight: '400', color: colors.gray[800] }}>
1098
+ {images[0]?.fileName || 'image_' + new Date().getTime() + '.jpg'}
1099
+ </Text>
1100
+ <Text style={{ fontSize: 12, color: colors.gray[500], marginTop: 2 }}>
1101
+ {loading ? 'Preparing...' : 'Ready to send'}
1102
+ </Text>
1449
1103
  </View>
1104
+
1105
+ <TouchableHighlight
1106
+ underlayColor={'rgba(0,0,0,0.1)'}
1107
+ onPress={() => {
1108
+ setSelectedImage('');
1109
+ setImages([]);
1110
+ }}
1111
+ style={{
1112
+ backgroundColor: colors.red[500],
1113
+ borderRadius: 24,
1114
+ width: 36,
1115
+ height: 36,
1116
+ alignItems: 'center',
1117
+ justifyContent: 'center',
1118
+ marginRight: 10,
1119
+ }}
1120
+ >
1121
+ <Ionicons name="close" size={20} color="white" />
1122
+ </TouchableHighlight>
1450
1123
  </View>
1451
- );
1452
- },
1453
- [state?.context?.selectedImage, safeSend],
1454
- );
1124
+ </View>
1125
+ );
1126
+ }, [selectedImage, loading, images]);
1455
1127
 
1456
1128
  const setImageViewerObject = (obj: any, v: boolean) => {
1457
1129
  setImageObject(obj);
@@ -1459,13 +1131,14 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1459
1131
  };
1460
1132
 
1461
1133
  const modalContent = React.useMemo(() => {
1462
- if (!imageObject) return <></>;
1134
+ if (!imageObject || !imageObject.image) return null;
1463
1135
  const { image, _id } = imageObject;
1136
+
1464
1137
  return (
1465
1138
  <CachedImage
1466
1139
  style={{ width: '100%', height: '100%' }}
1467
1140
  resizeMode={'cover'}
1468
- cacheKey={`${_id}-slack-bubble-imageKey`}
1141
+ cacheKey={`${_id}-modal-imageKey`}
1469
1142
  source={{
1470
1143
  uri: image,
1471
1144
  expiresIn: 86400,
@@ -1475,14 +1148,60 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1475
1148
  );
1476
1149
  }, [imageObject]);
1477
1150
 
1151
+ // Create a skeleton component for message bubbles with images
1478
1152
  const renderMessage = useCallback(
1479
1153
  (props: any) => {
1154
+ // Check if this message ID matches the uploading message ID
1155
+ const isUploading = props.currentMessage._id === uploadingMessageId && loading;
1156
+
1157
+ if (isUploading && props.currentMessage.image) {
1158
+ // Return a custom message skeleton during upload
1159
+ return (
1160
+ <View
1161
+ style={{
1162
+ padding: 10,
1163
+ marginBottom: 10,
1164
+ marginRight: 10,
1165
+ alignSelf: 'flex-end',
1166
+ borderRadius: 15,
1167
+ backgroundColor: colors.gray[100],
1168
+ maxWidth: '80%',
1169
+ }}
1170
+ >
1171
+ {props.currentMessage.text && props.currentMessage.text.trim() !== '' && (
1172
+ <Box
1173
+ style={{
1174
+ height: 15,
1175
+ borderRadius: 4,
1176
+ backgroundColor: colors.gray[200],
1177
+ overflow: 'hidden',
1178
+ marginBottom: 8,
1179
+ }}
1180
+ >
1181
+ <Skeleton variant="rounded" style={{ flex: 1 }} />
1182
+ </Box>
1183
+ )}
1184
+ <Box
1185
+ style={{
1186
+ height: 150,
1187
+ width: 150,
1188
+ borderRadius: 10,
1189
+ backgroundColor: colors.gray[200],
1190
+ overflow: 'hidden',
1191
+ }}
1192
+ >
1193
+ <Skeleton variant="rounded" style={{ flex: 1 }} />
1194
+ </Box>
1195
+ </View>
1196
+ );
1197
+ }
1198
+
1480
1199
  // Use memo to prevent unnecessary re-renders of each message
1481
1200
  return (
1482
1201
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1483
1202
  );
1484
1203
  },
1485
- [isShowImageViewer],
1204
+ [isShowImageViewer, uploadingMessageId, loading],
1486
1205
  );
1487
1206
 
1488
1207
  let onScroll = false;
@@ -1499,211 +1218,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1499
1218
  };
1500
1219
 
1501
1220
  const onEndReached = () => {
1502
- console.log('on end reached');
1503
1221
  if (!onScroll) return;
1504
1222
  onScroll = false;
1505
1223
  };
1506
1224
 
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
1225
  // Add a loader for when more messages are being loaded
1705
1226
  const renderLoadEarlier = useCallback(() => {
1706
- return state?.context?.loadingOldMessages ? (
1227
+ return loadingOldMessages ? (
1707
1228
  <View
1708
1229
  style={{
1709
1230
  padding: 10,
@@ -1715,7 +1236,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1715
1236
  <Spinner size="small" color="#3b82f6" />
1716
1237
  </View>
1717
1238
  ) : null;
1718
- }, [state?.context?.loadingOldMessages]);
1239
+ }, [loadingOldMessages]);
1719
1240
 
1720
1241
  // Add renderInputToolbar function
1721
1242
  const renderInputToolbar = useCallback((props) => {
@@ -1739,15 +1260,106 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1739
1260
  );
1740
1261
  }, []);
1741
1262
 
1263
+ // Create a memoized ImageViewerModal component
1264
+ const imageViewerModal = useMemo(
1265
+ () => (
1266
+ <ImageViewerModal isVisible={isShowImageViewer} setVisible={setImageViewer} modalContent={modalContent} />
1267
+ ),
1268
+ [isShowImageViewer, modalContent],
1269
+ );
1270
+
1271
+ // Create a memoized subscription handler component
1272
+ const subscriptionHandler = useMemo(
1273
+ () => (
1274
+ <SubscriptionHandler
1275
+ channelId={channelId?.toString()}
1276
+ subscribeToNewMessages={() =>
1277
+ subscribeToMore({
1278
+ document: CHAT_MESSAGE_ADDED,
1279
+ variables: {
1280
+ channelId: channelId?.toString(),
1281
+ },
1282
+ // updateQuery: (prev, { subscriptionData }: any) => {
1283
+ // try {
1284
+ // // Check if we have valid subscription data
1285
+ // if (!subscriptionData?.data?.chatMessageAdded) {
1286
+ // return prev;
1287
+ // }
1288
+
1289
+ // const newMessage = subscriptionData.data.chatMessageAdded;
1290
+
1291
+ // // Check if message is from current user - skip update as it's handled by optimistic UI
1292
+ // if (newMessage.author?.id === auth?.id) {
1293
+ // return prev;
1294
+ // }
1295
+
1296
+ // // Check if we already have this message to avoid duplicates
1297
+ // const currentMessages = prev?.messages?.data || [];
1298
+ // if (currentMessages.some((msg) => msg.id === newMessage.id)) {
1299
+ // return prev;
1300
+ // }
1301
+
1302
+ // // Update Apollo cache
1303
+ // const updatedData = {
1304
+ // ...prev,
1305
+ // messages: {
1306
+ // ...prev.messages,
1307
+ // data: [newMessage, ...currentMessages],
1308
+ // totalCount: (prev?.messages?.totalCount || 0) + 1,
1309
+ // },
1310
+ // };
1311
+
1312
+ // return updatedData;
1313
+ // } catch (error) {
1314
+ // return prev;
1315
+ // }
1316
+ // },
1317
+ })
1318
+ }
1319
+ />
1320
+ ),
1321
+ [channelId, subscribeToMore, auth?.id],
1322
+ );
1323
+
1324
+ // Create a memoized renderChatFooter function
1325
+ const renderChatFooter = useCallback(() => {
1326
+ return (
1327
+ <>
1328
+ {imageViewerModal}
1329
+ {subscriptionHandler}
1330
+ </>
1331
+ );
1332
+ }, [imageViewerModal, subscriptionHandler]);
1333
+
1334
+ // Add optimized listViewProps to reduce re-renders and improve list performance
1335
+ const listViewProps = useMemo(
1336
+ () => ({
1337
+ onEndReached: onEndReached,
1338
+ onEndReachedThreshold: 0.5,
1339
+ onMomentumScrollBegin: onMomentumScrollBegin,
1340
+ removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1341
+ initialNumToRender: 10, // Reduce initial render amount
1342
+ maxToRenderPerBatch: 7, // Reduce number in each render batch
1343
+ windowSize: 7, // Reduce the window size
1344
+ updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
1345
+ keyExtractor: (item) => item._id, // Add explicit key extractor
1346
+ }),
1347
+ [onEndReached, onMomentumScrollBegin],
1348
+ );
1349
+
1742
1350
  // Return optimized component with performance improvements
1743
1351
  return (
1744
1352
  <View
1745
1353
  style={{
1746
1354
  flex: 1,
1747
1355
  backgroundColor: 'white',
1356
+ position: 'relative',
1748
1357
  }}
1749
1358
  >
1750
- {state?.matches && state.matches(BaseState.FetchMessages) && <Spinner color={'#3b82f6'} />}
1359
+ {messageLoading && <Spinner color={'#3b82f6'} />}
1360
+
1361
+ {/* Render the image preview directly in the container so it's properly positioned */}
1362
+ {selectedImage ? renderAccessory() : null}
1751
1363
 
1752
1364
  <GiftedChat
1753
1365
  ref={messageRootListRef}
@@ -1757,20 +1369,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1757
1369
  listViewProps={{
1758
1370
  ...listViewProps,
1759
1371
  contentContainerStyle: {
1760
- paddingBottom: 10,
1372
+ paddingBottom: selectedImage ? 90 : 0, // Add padding at the bottom when image is selected
1761
1373
  },
1762
- keyboardShouldPersistTaps: 'handled',
1763
1374
  }}
1764
1375
  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
- }
1376
+ text={messageText || ' '}
1377
+ onInputTextChanged={(text) => setMessageText(text)}
1378
+ renderFooter={() => (loading && !images.length ? <Spinner color={'#3b82f6'} /> : null)}
1774
1379
  scrollToBottom
1775
1380
  user={{
1776
1381
  _id: auth?.id || '',
@@ -1781,14 +1386,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1781
1386
  renderMessageText={renderMessageText}
1782
1387
  renderInputToolbar={renderInputToolbar}
1783
1388
  minInputToolbarHeight={50}
1784
- renderActions={safeContextProperty('channelId') && renderActions}
1785
- renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
1389
+ renderActions={channelId && renderActions}
1786
1390
  renderMessage={renderMessage}
1787
1391
  renderChatFooter={renderChatFooter}
1788
1392
  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
1393
+ loadEarlier={totalCount > channelMessages.length}
1394
+ isLoadingEarlier={loadingOldMessages}
1395
+ bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0} // Adjust bottom offset based on image preview
1792
1396
  textInputProps={{
1793
1397
  style: {
1794
1398
  borderWidth: 1,
@@ -1832,12 +1436,9 @@ const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptio
1832
1436
  useEffect(() => {
1833
1437
  // Don't set up subscription if there's no channel ID
1834
1438
  if (!channelId) {
1835
- console.log('⚠️ No channel ID for subscription');
1836
1439
  return;
1837
1440
  }
1838
1441
 
1839
- console.log('🔄 Setting up NEW subscription for channel:', channelId);
1840
-
1841
1442
  // Call the subscribe function and store the unsubscribe function
1842
1443
  const unsubscribe = subscribeToNewMessages();
1843
1444
 
@@ -1847,7 +1448,6 @@ const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptio
1847
1448
  // Return cleanup function
1848
1449
  return () => {
1849
1450
  if (unsubscribe && typeof unsubscribe === 'function') {
1850
- console.log('🔄 Cleaning up subscription for channel:', channelIdRef.current);
1851
1451
  unsubscribe();
1852
1452
  }
1853
1453
  };