@messenger-box/platform-mobile 10.0.3-alpha.34 → 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 +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 +80 -256
  5. package/lib/screens/inbox/components/DialogsListItem.js.map +1 -1
  6. package/lib/screens/inbox/components/ServiceDialogsListItem.js +222 -324
  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 +487 -888
  11. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  12. package/lib/screens/inbox/containers/Dialogs.js +243 -547
  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 +4 -4
  17. package/src/screens/inbox/components/CachedImage/index.tsx +191 -140
  18. package/src/screens/inbox/components/DialogsListItem.tsx +112 -345
  19. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +316 -437
  20. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +2 -4
  21. package/src/screens/inbox/containers/ConversationView.tsx +676 -993
  22. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  23. package/src/screens/inbox/containers/Dialogs.tsx +345 -636
  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,477 +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
- send(event);
364
- } catch (error) {
365
- console.error('Error sending event to state machine:', error, event);
366
- }
367
- },
368
- [send],
369
- );
370
-
371
- // Immediately set initial context if needed
372
- useEffect(() => {
373
- if (ChannelId) {
374
- console.log('Setting initial channel ID on mount:', ChannelId);
375
- try {
376
- send({
377
- type: ConversationActions.INITIAL_CONTEXT,
378
- data: { channelId: ChannelId },
379
- });
380
- } catch (error) {
381
- console.error('Error sending initial context:', error);
382
- }
383
- }
384
- }, []);
385
-
386
- // Use a ref to track the current machine snapshot for safer access
387
- const stateRef = useRef(state);
388
-
389
- // Keep the ref updated with the latest snapshot
390
- useEffect(() => {
391
- stateRef.current = state;
392
- }, [state]);
90
+ // Fix for the optimistic response types
91
+ type OptimisticPropsConfig = {
92
+ __typename: 'MachineConfiguration';
93
+ resource: string;
94
+ };
393
95
 
394
- // Avoid referencing state.context directly in places that might cause undefined errors
395
- const safeGetContext = useCallback(() => {
396
- if (stateRef.current && stateRef.current.context) {
397
- return stateRef.current.context;
398
- }
399
- // Return default values if context is undefined
400
- return {
401
- channelId: null,
402
- channelMessages: [],
403
- totalCount: 0,
404
- skip: 0,
405
- loading: false,
406
- loadingOldMessages: false,
407
- error: null,
408
- selectedImage: '',
409
- files: [],
410
- images: [],
411
- messageText: '',
412
- imageLoading: false,
413
- };
414
- }, []);
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>({});
415
108
 
416
- // Use cleanup function to prevent setting state after unmount
417
- useEffect(() => {
418
- return () => {
419
- isMountedRef.current = false;
420
- };
421
- }, []);
109
+ // Create refs for various operations
110
+ const messageRootListRef = useRef<any>(null);
111
+ const isMounted = useRef(true);
112
+ const fetchOldDebounceRef = useRef(false);
422
113
 
114
+ // Navigation and auth
423
115
  const auth: any = useSelector(userSelector);
424
116
  const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
425
117
  const navigation = useNavigation<any>();
426
- const [selectedImage, setImage] = useState<string>('');
427
- const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
428
- const [imageObject, setImageObject] = useState<any>({});
429
- const messageRootListRef = useRef<any>(null);
430
118
  const isFocused = useIsFocused();
431
119
 
120
+ // Apollo mutations
432
121
  const [addDirectChannel] = useAddDirectChannelMutation();
433
122
  const { startUpload } = useUploadFilesNative();
434
123
  const [sendMsg] = useSendMessagesMutation();
435
124
  const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
436
125
 
126
+ // Apollo query for messages
437
127
  const {
438
128
  data,
439
129
  loading: messageLoading,
440
130
  refetch,
441
131
  fetchMore: fetchMoreMessages,
442
132
  subscribeToMore,
443
- }: any = useMessagesQuery({
133
+ } = useMessagesQuery({
444
134
  variables: {
445
- channelId: state.context.channelId?.toString(),
135
+ channelId: channelId?.toString(),
446
136
  parentId: null,
447
137
  limit: MESSAGES_PER_PAGE,
448
- skip: state.context.skip,
138
+ skip: skip,
449
139
  },
450
- skip: !state.context.channelId,
140
+ skip: !channelId,
451
141
  fetchPolicy: 'cache-and-network',
452
142
  nextFetchPolicy: 'cache-first',
453
143
  refetchWritePolicy: 'merge',
144
+ notifyOnNetworkStatusChange: true,
454
145
  onCompleted: (queryData) => {
455
- console.log('MESSAGE QUERY COMPLETED:', queryData);
456
- if (queryData?.messages?.data) {
457
- console.log(
458
- 'Raw message data from query:',
459
- JSON.stringify(queryData.messages.data).substring(0, 100) + '...',
460
- );
461
- console.log('Message count from query:', queryData.messages.data.length);
462
- console.log('Total count from query:', queryData.messages.totalCount);
463
- }
146
+ // MESSAGE QUERY COMPLETED
464
147
  },
465
148
  onError: (error) => {
466
- console.error('MESSAGE QUERY ERROR:', error);
149
+ setError(String(error));
467
150
  },
468
151
  });
469
152
 
470
- // Modify the fetchMessagesDirectly function to use safe access
471
- const fetchMessagesDirectly = useCallback(async () => {
472
- const channelId = safeGetContext().channelId;
473
- if (!channelId) {
474
- console.warn('Cannot fetch messages: No channel ID');
475
- return;
476
- }
153
+ // Extract messages from the query data
154
+ const channelMessages = useMemo(() => {
155
+ return data?.messages?.data || [];
156
+ }, [data?.messages?.data]);
477
157
 
478
- try {
479
- console.log('💫 FETCHING messages for channel:', channelId);
158
+ // Get total message count
159
+ const totalCount = useMemo(() => {
160
+ return data?.messages?.totalCount || 0;
161
+ }, [data?.messages?.totalCount]);
480
162
 
481
- // Use loading state to prevent duplicate fetches
482
- send({ type: ConversationActions.START_LOADING });
163
+ // Clear messages when component unmounts
164
+ useEffect(() => {
165
+ return () => {
166
+ isMounted.current = false;
167
+ };
168
+ }, []);
483
169
 
484
- const response = await refetch({
485
- channelId: channelId.toString(),
486
- parentId: null,
487
- limit: MESSAGES_PER_PAGE,
488
- skip: 0,
489
- });
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]);
490
177
 
491
- if (response?.data?.messages) {
492
- const { data: messages, totalCount } = response.data.messages;
493
-
494
- if (messages && messages.length > 0) {
495
- // Batch update to reduce renders
496
- safeSend({
497
- type: ConversationActions.SET_CHANNEL_MESSAGES,
498
- data: { messages, totalCount },
499
- });
500
- } else {
501
- console.warn('No messages found for channel', channelId);
502
- // Still clear loading state
503
- send({ type: ConversationActions.STOP_LOADING });
504
- }
505
- } else {
506
- console.warn('Query returned no messages data');
507
- send({ type: ConversationActions.STOP_LOADING });
178
+ // Focus/unfocus behavior
179
+ useFocusEffect(
180
+ React.useCallback(() => {
181
+ if (channelId) {
182
+ // Refresh messages when screen comes into focus
183
+ refetch();
508
184
  }
509
- } catch (error) {
510
- console.error('ERROR fetching messages:', error);
511
- 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);
512
195
  }
513
- }, [safeGetContext, refetch, safeSend]);
196
+ }, [selectedImage]);
514
197
 
198
+ // Fetch more messages function
515
199
  const fetchMoreMessagesImpl = useCallback(async () => {
516
200
  try {
201
+ setLoadingOldMessages(true);
517
202
  const response = await fetchMoreMessages({
518
203
  variables: {
519
- channelId: state.context.channelId?.toString(),
204
+ channelId: channelId?.toString(),
520
205
  parentId: null,
521
- skip: state.context.channelMessages.length,
206
+ skip: channelMessages.length,
522
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
+ // },
523
226
  });
524
227
 
228
+ setLoadingOldMessages(false);
525
229
  if (!response?.data?.messages?.data) {
526
230
  return { error: 'No messages returned' };
527
231
  }
528
232
 
529
233
  return { messages: response.data.messages.data };
530
234
  } catch (error) {
235
+ setLoadingOldMessages(false);
236
+ setError(String(error));
531
237
  return { error: String(error) };
532
238
  }
533
- }, [state.context.channelId, state.context.channelMessages.length, fetchMoreMessages]);
239
+ }, [channelId, channelMessages.length, fetchMoreMessages]);
534
240
 
241
+ // Send message function
535
242
  const sendMessageImpl = useCallback(async () => {
536
243
  try {
244
+ // Store the current message text and clear input immediately for better UX
245
+ const currentMessageText = messageText;
246
+ setMessageText('');
247
+
537
248
  const notificationData: IExpoNotificationData = {
538
249
  url: config.INBOX_MESSEGE_PATH,
539
- params: { channelId: state.context.channelId, hideTabBar: true },
250
+ params: { channelId, hideTabBar: true },
540
251
  screen: 'DialogMessages',
541
252
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
542
253
  };
543
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
+
544
300
  const response = await sendMsg({
545
301
  variables: {
546
- channelId: state.context.channelId,
547
- content: state.context.messageText,
302
+ channelId,
303
+ content: currentMessageText,
548
304
  notificationParams: notificationData,
549
305
  },
306
+ optimisticResponse: {
307
+ __typename: 'Mutation',
308
+ sendMessage: optimisticMessage,
309
+ },
550
310
  });
551
311
 
552
312
  return { message: response.data?.sendMessage };
553
313
  } catch (error) {
314
+ setLoading(false);
315
+ setError(String(error));
554
316
  return { error: String(error) };
555
317
  }
556
- }, [state.context.channelId, state.context.messageText, sendMsg]);
318
+ }, [channelId, messageText, sendMsg, auth]);
557
319
 
558
- // Fix the image selection process to ensure proper format for upload
320
+ // Image selection handler
559
321
  const onSelectImages = async () => {
560
- safeSend({ type: ConversationActions.START_LOADING });
322
+ setLoading(true);
561
323
 
562
324
  try {
563
- console.log('Starting image picker...');
564
325
  let imageSource = await ImagePicker.launchImageLibraryAsync({
565
326
  mediaTypes: ImagePicker.MediaTypeOptions.Images,
566
327
  allowsEditing: true,
@@ -571,22 +332,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
571
332
  });
572
333
 
573
334
  if (!imageSource?.canceled) {
574
- console.log(
575
- 'Image selected. Asset details:',
576
- JSON.stringify({
577
- uri: imageSource?.assets?.[0]?.uri?.substring(0, 30) + '...',
578
- width: imageSource?.assets?.[0]?.width,
579
- height: imageSource?.assets?.[0]?.height,
580
- hasBase64: !!imageSource?.assets?.[0]?.base64,
581
- hasUri: !!imageSource?.assets?.[0]?.uri,
582
- }),
583
- );
584
-
585
335
  // Get the asset
586
336
  const selectedAsset = imageSource?.assets?.[0];
587
337
  if (!selectedAsset) {
588
- console.error('No asset found in selected image');
589
- safeSend({ type: ConversationActions.STOP_LOADING });
338
+ setLoading(false);
590
339
  return;
591
340
  }
592
341
 
@@ -602,58 +351,49 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
602
351
  mimeType: 'image/jpeg',
603
352
  };
604
353
 
605
- console.log('Prepared image asset for upload:', {
606
- hasUrl: !!asset.url,
607
- hasFileName: !!asset.fileName,
608
- hasMimeType: !!asset.mimeType,
609
- previewAvailable: !!previewImage,
610
- });
611
-
612
354
  // Update state with the new image
613
- safeSend({
614
- type: ConversationActions.SET_IMAGE,
615
- data: {
616
- image: previewImage,
617
- images: [asset], // Replace existing images with the new one
618
- },
619
- });
620
-
621
- console.log('Image state updated successfully');
355
+ setSelectedImage(previewImage);
356
+ setImages([asset]);
622
357
  } else {
623
- console.log('Image selection cancelled');
624
- safeSend({ type: ConversationActions.STOP_LOADING });
358
+ setLoading(false);
625
359
  }
626
360
  } catch (error) {
627
- console.error('Error selecting image:', error);
628
- safeSend({ type: ConversationActions.STOP_LOADING });
361
+ setLoading(false);
629
362
  }
630
363
  };
631
364
 
632
- // 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
633
369
  const sendMessageWithFileImpl = useCallback(async () => {
634
370
  try {
635
- 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);
636
373
 
637
374
  // Generate a unique post ID for the message
638
375
  const postId = objectId();
639
- console.log('Generated postId for file upload:', postId);
376
+
377
+ // Set the message ID that should show the skeleton
378
+ setUploadingMessageId(postId);
640
379
 
641
380
  // Prepare notification data
642
381
  const notificationData: IExpoNotificationData = {
643
382
  url: config.INBOX_MESSEGE_PATH,
644
- params: { channelId: state.context.channelId, hideTabBar: true },
383
+ params: { channelId, hideTabBar: true },
645
384
  screen: 'DialogMessages',
646
385
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
647
386
  };
648
387
 
649
388
  // Safety check for images
650
- if (!state.context.images || state.context.images.length === 0) {
651
- console.error('No images found in state');
389
+ if (!images || images.length === 0) {
390
+ setLoading(false);
391
+ setUploadingMessageId(null);
652
392
  return { error: 'No images available to upload' };
653
393
  }
654
394
 
655
395
  // Format the images for upload if needed
656
- const imagesToUpload = state.context.images.map((img) => {
396
+ const imagesToUpload = images.map((img) => {
657
397
  // Ensure the image has all required properties
658
398
  return {
659
399
  ...img,
@@ -663,20 +403,82 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
663
403
  };
664
404
  });
665
405
 
666
- console.log(
667
- 'Formatted images for upload:',
668
- imagesToUpload.map((img) => ({
669
- hasUri: !!img.uri,
670
- hasUrl: !!img.url,
671
- hasName: !!img.name,
672
- hasType: !!img.type,
673
- hasFileName: !!img.fileName,
674
- uri: img.uri?.substring(0, 30) + '...',
675
- })),
676
- );
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
+ };
677
480
 
678
481
  // Upload the files
679
- console.log('Starting file upload...');
680
482
  const uploadResponse = await startUpload({
681
483
  file: imagesToUpload,
682
484
  saveUploadedFile: {
@@ -687,89 +489,66 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
687
489
  },
688
490
  });
689
491
 
690
- console.log(
691
- 'Upload response received:',
692
- uploadResponse?.data ? 'Has data' : 'No data',
693
- 'Error:',
694
- uploadResponse?.error ? uploadResponse.error : 'None',
695
- );
696
-
697
492
  if (uploadResponse?.error) {
698
- console.error('Upload error:', uploadResponse.error);
493
+ setLoading(false);
494
+ setUploadingMessageId(null);
699
495
  return { error: String(uploadResponse.error) };
700
496
  }
701
497
 
702
498
  // Get uploaded file IDs
703
499
  const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
704
- console.log(
705
- 'Uploaded files:',
706
- uploadedFiles
707
- ? JSON.stringify(uploadedFiles.map((f) => ({ id: f.id, url: f.url?.substring(0, 30) + '...' })))
708
- : 'null',
709
- );
710
-
711
500
  const files = uploadedFiles?.map((f: any) => f.id) ?? null;
712
501
 
713
- console.log('Files uploaded successfully. File IDs:', files);
714
-
715
502
  // Send the message with the uploaded files
716
- console.log('Sending message with files:', {
717
- postId,
718
- channelId: state.context.channelId,
719
- content: state.context.messageText || ' ',
720
- hasFiles: !!files,
721
- fileCount: files?.length || 0,
722
- });
723
-
724
503
  const response = await sendMsg({
725
504
  variables: {
726
505
  postId,
727
- channelId: state.context.channelId,
728
- content: state.context.messageText || ' ', // Use a space if no text
506
+ channelId,
507
+ content: currentMessageText || ' ', // Use a space if no text
729
508
  files,
730
509
  notificationParams: notificationData,
731
510
  },
511
+ optimisticResponse: {
512
+ __typename: 'Mutation',
513
+ sendMessage: optimisticMessage,
514
+ },
732
515
  });
733
516
 
734
517
  if (response?.data?.sendMessage) {
735
- console.log('Message with file sent successfully:', response.data.sendMessage.id);
736
-
737
- // Log the file data from the response to verify it's being returned correctly
738
- if (response.data.sendMessage.files?.data) {
739
- console.log(
740
- '📷 Message response file data:',
741
- JSON.stringify({
742
- fileCount: response.data.sendMessage.files.data.length,
743
- fileUrl: response.data.sendMessage.files.data[0]?.url?.substring(0, 30) + '...',
744
- }),
745
- );
746
- }
747
-
748
518
  // Clear the images after successful send
749
- setTimeout(() => {
750
- safeSend({ type: ConversationActions.CLEAR_IMAGE });
751
- }, 100);
752
- } else {
753
- console.error('Failed to send message with file:', response?.errors);
519
+ setSelectedImage('');
520
+ setImages([]);
754
521
  }
755
522
 
523
+ setLoading(false);
524
+ setUploadingMessageId(null);
756
525
  return { message: response.data?.sendMessage };
757
526
  } catch (error) {
758
- console.error('Error in sendMessageWithFileImpl:', error);
527
+ setLoading(false);
528
+ setUploadingMessageId(null);
529
+ setError(String(error));
759
530
  return { error: String(error) };
760
531
  }
761
- }, [state.context.channelId, state.context.messageText, state.context.images, startUpload, sendMsg, safeSend]);
532
+ }, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth]);
762
533
 
534
+ // Create direct channel implementation
763
535
  const createDirectChannelImpl = useCallback(async () => {
764
536
  try {
537
+ setLoading(true);
765
538
  if (
766
539
  !rest?.isCreateNewChannel ||
767
540
  rest?.newChannelData?.type !== RoomType?.Direct ||
768
541
  !rest?.newChannelData?.userIds?.length
769
542
  ) {
543
+ setLoading(false);
770
544
  return { error: 'Invalid channel data' };
771
545
  }
772
546
 
547
+ // Store current message text
548
+ const currentMessageText = messageText;
549
+ // Clear message text immediately for better UX
550
+ setMessageText('');
551
+
773
552
  const response = await addDirectChannel({
774
553
  variables: {
775
554
  receiver: [...(rest?.newChannelData?.userIds ?? [])],
@@ -778,10 +557,12 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
778
557
  });
779
558
 
780
559
  if (!response?.data?.createDirectChannel?.id) {
560
+ setLoading(false);
781
561
  return { error: 'Failed to create channel' };
782
562
  }
783
563
 
784
564
  const newChannelId = response.data.createDirectChannel.id;
565
+ setChannelId(newChannelId);
785
566
 
786
567
  const notificationData: IExpoNotificationData = {
787
568
  url: config.INBOX_MESSEGE_PATH,
@@ -790,99 +571,74 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
790
571
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
791
572
  };
792
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
793
622
  await sendMsg({
794
623
  variables: {
795
624
  channelId: newChannelId,
796
- content: state.context.messageText,
625
+ content: currentMessageText,
797
626
  notificationParams: notificationData,
798
627
  },
628
+ optimisticResponse: {
629
+ __typename: 'Mutation',
630
+ sendMessage: optimisticMessage,
631
+ },
799
632
  });
800
633
 
634
+ setLoading(false);
801
635
  return { channelId: newChannelId };
802
636
  } catch (error) {
637
+ setLoading(false);
638
+ setError(String(error));
803
639
  return { error: String(error) };
804
640
  }
805
- }, [rest, state.context.messageText, addDirectChannel, sendMsg]);
806
-
807
- // Remove the implementation inside this effect
808
- useEffect(() => {
809
- // We've moved these implementations to useCallback hooks above
810
- }, [state.value, sendMsg, refetch, fetchMoreMessages, addDirectChannel, startUpload, rest, state.context]);
811
-
812
- React.useEffect(() => {
813
- return () => {
814
- send({ type: ConversationActions.CLEAR_MESSAGES });
815
- };
816
- }, []);
817
-
818
- useFocusEffect(
819
- React.useCallback(() => {
820
- if (state.context.channelId) {
821
- send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: state.context.channelId } });
822
- }
823
- return () => {
824
- send({ type: ConversationActions.CLEAR_MESSAGES });
825
- };
826
- }, [state.context.channelId, isFocused]),
827
- );
828
-
829
- React.useEffect(() => {
830
- const currentChannelId = ChannelId || currentRoute?.params?.channelId;
831
- if (currentChannelId) {
832
- console.log('Setting initial channel ID:', currentChannelId);
833
- send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: currentChannelId } });
834
- }
835
- }, [ChannelId, currentRoute]);
836
-
837
- React.useEffect(() => {
838
- if (state.context.selectedImage) {
839
- send({ type: ConversationActions.STOP_LOADING });
840
- }
841
- }, [state.context.selectedImage]);
842
-
843
- useEffect(() => {
844
- if (data?.messages?.data) {
845
- console.log('📩 QUERY DATA CHANGED - Messages received:', data.messages.data.length);
846
- const { data: messages, totalCount: messageTotalCount } = data.messages;
847
-
848
- if (messages && messages.length > 0) {
849
- console.log('📩 QUERY DATA - Setting channel messages, count:', messages.length);
850
-
851
- // First try dispatching the update through XState
852
- send({
853
- type: ConversationActions.SET_CHANNEL_MESSAGES,
854
- data: {
855
- messages: uniqBy([...messages, ...state.context.channelMessages], ({ id }) => id),
856
- totalCount: messageTotalCount,
857
- },
858
- });
859
-
860
- // Debug: Log the first message to verify data format
861
- if (messages[0]) {
862
- const sample = messages[0];
863
- console.log(
864
- '📩 SAMPLE MESSAGE:',
865
- JSON.stringify({
866
- id: sample.id,
867
- message: sample.message,
868
- author: {
869
- id: sample.author?.id,
870
- name: `${sample.author?.givenName} ${sample.author?.familyName}`,
871
- },
872
- createdAt: sample.createdAt,
873
- }),
874
- );
875
- }
876
-
877
- // Check if the state machine actually updated (debug only)
878
- setTimeout(() => {
879
- if (state.context.channelMessages?.length === 0) {
880
- console.warn('⚠️ STATE NOT UPDATED after message data received - may need fallback');
881
- }
882
- }, 500);
883
- }
884
- }
885
- }, [data]);
641
+ }, [rest, messageText, addDirectChannel, sendMsg, auth]);
886
642
 
887
643
  // Optimize onFetchOld by adding debounce logic
888
644
  const onFetchOld = useCallback(() => {
@@ -890,49 +646,80 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
890
646
  if (fetchOldDebounceRef.current) return;
891
647
 
892
648
  // Check if we need to fetch more messages
893
- if (
894
- state?.context?.totalCount > state?.context?.channelMessages?.length &&
895
- !state?.context?.loadingOldMessages
896
- ) {
649
+ if (totalCount > channelMessages.length && !loadingOldMessages) {
897
650
  // Set debounce
898
651
  fetchOldDebounceRef.current = true;
899
652
 
900
- // Send fetch event
901
- send({ type: ConversationActions.FETCH_MORE_MESSAGES });
653
+ // Fetch more messages
654
+ fetchMoreMessagesImpl();
902
655
 
903
656
  // Clear debounce after a timeout
904
657
  setTimeout(() => {
905
658
  fetchOldDebounceRef.current = false;
906
659
  }, 1000);
907
660
  }
908
- }, [state?.context?.totalCount, state?.context?.channelMessages, state?.context?.loadingOldMessages]);
909
-
910
- // Add debounce ref
911
- const fetchOldDebounceRef = useRef(false);
661
+ }, [totalCount, channelMessages.length, loadingOldMessages, fetchMoreMessagesImpl]);
912
662
 
913
663
  const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
914
664
  const paddingToTop = 60;
915
665
  return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
916
666
  };
917
667
 
918
- const dataURLtoFile = (dataurl: any, filename: any) => {
919
- var arr = dataurl.split(','),
920
- mime = arr[0].match(/:(.*?);/)[1],
921
- bstr = atob(arr[1]),
922
- n = bstr.length,
923
- u8arr = new Uint8Array(n);
924
- while (n--) {
925
- 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 [];
926
673
  }
927
- return new File([u8arr], filename, { type: mime });
928
- };
929
674
 
930
- // 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
931
718
  const renderSend = useCallback(
932
719
  (props) => {
933
720
  // Enable the send button if there's text OR we have images
934
- const hasContent = !!props.text || state?.context?.images?.length > 0;
935
- const canSend = (state?.context?.channelId || rest?.isCreateNewChannel) && hasContent;
721
+ const hasContent = !!props.text || images?.length > 0;
722
+ const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
936
723
 
937
724
  return (
938
725
  <Send
@@ -958,157 +745,58 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
958
745
  </Send>
959
746
  );
960
747
  },
961
- [state?.context?.channelId, state?.context?.images, rest?.isCreateNewChannel],
748
+ [channelId, images, rest?.isCreateNewChannel],
962
749
  );
963
750
 
964
- // Fix the handleSend function to properly handle image-only messages
751
+ // Handle send for messages
965
752
  const handleSend = useCallback(
966
753
  async (messages) => {
967
754
  // Extract message text from GiftedChat messages array
968
- const messageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
969
- console.log('Sending message:', messageText);
970
- console.log('Images:', state.context.images?.length);
755
+ const newMessageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
971
756
 
972
757
  // Check if we can send a message (channel exists or we're creating one)
973
- if (!state.context.channelId && !rest?.isCreateNewChannel) {
974
- console.log('Cannot send - no channel');
758
+ if (!channelId && !rest?.isCreateNewChannel) {
975
759
  return;
976
760
  }
977
761
 
978
762
  // Allow sending if we have text OR images (image-only messages are valid)
979
- const hasText = !!messageText && messageText !== ' ';
980
- const hasImages = state.context.images && state.context.images.length > 0;
763
+ const hasText = !!newMessageText && newMessageText !== ' ';
764
+ const hasImages = images && images.length > 0;
981
765
 
982
766
  if (!hasText && !hasImages) {
983
- console.log('Nothing to send - no text or images');
984
767
  return;
985
768
  }
986
769
 
987
- // Set the message text in the state (even if empty for image-only messages)
988
- 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);
989
772
 
990
773
  // Handle direct channel creation if needed
991
- if (rest?.isCreateNewChannel && !state.context.channelId) {
774
+ if (rest?.isCreateNewChannel && !channelId) {
992
775
  if (rest?.newChannelData?.type === RoomType?.Direct) {
993
- safeSend({ type: ConversationActions.CREATE_DIRECT_CHANNEL });
776
+ createDirectChannelImpl();
994
777
  }
995
778
  return;
996
779
  }
997
780
 
998
781
  // Send message with or without image based on state
999
782
  if (hasImages) {
1000
- console.log('Sending message with file');
1001
- safeSend({ type: ConversationActions.SEND_MESSAGE_WITH_FILE });
783
+ sendMessageWithFileImpl();
1002
784
  } else {
1003
- console.log('Sending text-only message');
1004
- safeSend({ type: ConversationActions.SEND_MESSAGE });
785
+ sendMessageImpl();
1005
786
  }
1006
787
  },
1007
- [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
+ ],
1008
797
  );
1009
798
 
1010
- // Update fetchMessagesWithFallback to not use fallback state
1011
- const fetchMessagesWithFallback = useCallback(async () => {
1012
- if (!state.context.channelId) return;
1013
-
1014
- try {
1015
- console.log('🔄 DIRECT FETCH: Using direct approach for channel:', state.context.channelId);
1016
-
1017
- const response = await refetch({
1018
- channelId: state.context.channelId?.toString(),
1019
- parentId: null,
1020
- limit: MESSAGES_PER_PAGE,
1021
- skip: 0,
1022
- });
1023
-
1024
- if (response?.data?.messages?.data) {
1025
- const messages = response.data.messages.data;
1026
- console.log('✅ DIRECT FETCH: Got messages:', messages.length);
1027
-
1028
- // Skip fallback and send directly to state machine
1029
- send({
1030
- type: ConversationActions.SET_CHANNEL_MESSAGES,
1031
- data: {
1032
- messages,
1033
- totalCount: response.data.messages.totalCount,
1034
- },
1035
- });
1036
- }
1037
- } catch (error) {
1038
- console.error('❌ DIRECT FETCH ERROR:', error);
1039
- }
1040
- }, [state.context.channelId, refetch]);
1041
-
1042
- // Auto-trigger fallback if needed
1043
- useEffect(() => {
1044
- let timeoutId: NodeJS.Timeout;
1045
-
1046
- if (state.context.channelId && state.context.channelMessages.length === 0) {
1047
- timeoutId = setTimeout(() => {
1048
- console.log('⚠️ ACTIVATING FALLBACK - XState not updating after timeout');
1049
- fetchMessagesWithFallback();
1050
- }, 3000); // Wait 3 seconds for normal flow to work
1051
- }
1052
-
1053
- return () => {
1054
- if (timeoutId) clearTimeout(timeoutId);
1055
- };
1056
- }, [state.context.channelId, state.context.channelMessages, fetchMessagesWithFallback]);
1057
-
1058
- // Optimize the messageList calculation for better performance
1059
- const messageList = useMemo(() => {
1060
- // Only recalculate when these dependencies change
1061
- console.log('🔄 CALCULATING MESSAGE LIST - Optimized version');
1062
-
1063
- // Short-circuit if no messages to process
1064
- if (!state?.context?.channelMessages || state.context.channelMessages.length === 0) {
1065
- console.log('No messages to process');
1066
- return [];
1067
- }
1068
-
1069
- // Use a more efficient approach - pre-filter messages once
1070
- const filteredMessages = uniqBy(state.context.channelMessages, ({ id }) => id);
1071
-
1072
- // Skip processing if no filtered messages
1073
- if (filteredMessages.length === 0) {
1074
- return [];
1075
- }
1076
-
1077
- // Transform messages only once and return
1078
- return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
1079
- const date = new Date(msg.createdAt);
1080
-
1081
- // Extract image URL from files data
1082
- let imageUrl = null;
1083
- if (msg.files?.data && msg.files.data.length > 0) {
1084
- const fileData = msg.files.data[0];
1085
- if (fileData && fileData.url) {
1086
- imageUrl = fileData.url;
1087
- }
1088
- }
1089
-
1090
- // Create message in a more direct way
1091
- return {
1092
- _id: msg.id,
1093
- text: msg.message,
1094
- createdAt: date,
1095
- user: {
1096
- _id: msg.author?.id || '',
1097
- name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
1098
- avatar: msg.author?.picture || '',
1099
- },
1100
- image: imageUrl,
1101
- sent: msg?.isDelivered,
1102
- received: msg?.isRead,
1103
- type: msg?.type,
1104
- propsConfiguration: msg?.propsConfiguration,
1105
- replies: msg?.replies ?? [],
1106
- isShowThreadMessage,
1107
- };
1108
- });
1109
- }, [state?.context?.channelMessages, isShowThreadMessage]);
1110
-
1111
- // Memoize the renderMessageText function
799
+ // Render message text with customizations for alerts and replies
1112
800
  const renderMessageText = useCallback(
1113
801
  (props: any) => {
1114
802
  const { currentMessage } = props;
@@ -1173,7 +861,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1173
861
  onPress={() => {
1174
862
  if (currentMessage?.isShowThreadMessage)
1175
863
  navigation.navigate(config.THREAD_MESSEGE_PATH, {
1176
- channelId: state?.context?.channelId,
864
+ channelId: channelId,
1177
865
  title: 'Message',
1178
866
  postParentId: currentMessage?._id,
1179
867
  isPostParentIdThread: true,
@@ -1238,7 +926,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1238
926
  onPress={() => {
1239
927
  if (currentMessage?.isShowThreadMessage)
1240
928
  navigation.navigate(config.THREAD_MESSEGE_PATH, {
1241
- channelId: state?.context?.channelId,
929
+ channelId: channelId,
1242
930
  title: 'Message',
1243
931
  postParentId: currentMessage?._id,
1244
932
  isPostParentIdThread: true,
@@ -1290,9 +978,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1290
978
  );
1291
979
  }
1292
980
  },
1293
- [navigation, state?.context?.channelId, role],
981
+ [navigation, channelId, role],
1294
982
  );
1295
983
 
984
+ // Render action buttons (including image upload)
1296
985
  const renderActions = (props) => {
1297
986
  return (
1298
987
  <Actions
@@ -1326,79 +1015,115 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1326
1015
  };
1327
1016
 
1328
1017
  // Create a more visible and reliable image preview with cancel button
1329
- const renderAccessory = useCallback(
1330
- (props) => {
1331
- const selectedImage = safeContextProperty('selectedImage', '');
1332
-
1333
- if (!selectedImage) {
1334
- return null;
1335
- }
1018
+ const renderAccessory = useCallback(() => {
1019
+ if (!selectedImage) {
1020
+ return null;
1021
+ }
1336
1022
 
1337
- 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
+ >
1338
1047
  <View
1339
1048
  style={{
1340
- height: 50,
1341
- padding: 3,
1342
- backgroundColor: 'white',
1343
- borderTopWidth: 1,
1344
- borderTopColor: '#e0e0e0',
1049
+ flex: 1,
1345
1050
  flexDirection: 'row',
1346
1051
  alignItems: 'center',
1347
- margin: 0,
1348
- paddingBottom: 0,
1349
- paddingTop: 5,
1350
- position: 'absolute',
1351
- bottom: 0,
1352
- left: 0,
1353
- right: 0,
1354
- zIndex: 999,
1052
+ paddingLeft: 15,
1053
+ paddingRight: 5,
1355
1054
  }}
1356
1055
  >
1357
1056
  <View
1358
1057
  style={{
1359
- flex: 1,
1360
- flexDirection: 'row',
1361
- alignItems: 'center',
1362
- 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',
1363
1066
  }}
1364
1067
  >
1365
1068
  <Image
1366
- key={state?.context?.selectedImage}
1069
+ key={selectedImage}
1367
1070
  alt={'selected image'}
1368
- source={{ uri: state?.context?.selectedImage }}
1071
+ source={{ uri: selectedImage }}
1369
1072
  style={{
1370
- width: 36,
1371
- height: 36,
1372
- borderRadius: 5,
1373
- marginRight: 15,
1073
+ width: '100%',
1074
+ height: '100%',
1374
1075
  }}
1375
- size={'xs'}
1076
+ size={'md'}
1376
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>
1377
1095
 
1378
- <TouchableHighlight
1379
- underlayColor="#dddddd"
1380
- onPress={() => safeSend({ type: ConversationActions.CLEAR_IMAGE })}
1381
- style={{
1382
- backgroundColor: '#f44336',
1383
- paddingVertical: 2,
1384
- paddingHorizontal: 5,
1385
- borderRadius: 5,
1386
- marginLeft: 10,
1387
- elevation: 3,
1388
- shadowColor: '#000',
1389
- shadowOffset: { width: 0, height: 1 },
1390
- shadowOpacity: 0.3,
1391
- shadowRadius: 2,
1392
- }}
1393
- >
1394
- <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1395
- </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>
1396
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>
1397
1123
  </View>
1398
- );
1399
- },
1400
- [state?.context?.selectedImage, safeSend],
1401
- );
1124
+ </View>
1125
+ );
1126
+ }, [selectedImage, loading, images]);
1402
1127
 
1403
1128
  const setImageViewerObject = (obj: any, v: boolean) => {
1404
1129
  setImageObject(obj);
@@ -1406,13 +1131,14 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1406
1131
  };
1407
1132
 
1408
1133
  const modalContent = React.useMemo(() => {
1409
- if (!imageObject) return <></>;
1134
+ if (!imageObject || !imageObject.image) return null;
1410
1135
  const { image, _id } = imageObject;
1136
+
1411
1137
  return (
1412
1138
  <CachedImage
1413
1139
  style={{ width: '100%', height: '100%' }}
1414
1140
  resizeMode={'cover'}
1415
- cacheKey={`${_id}-slack-bubble-imageKey`}
1141
+ cacheKey={`${_id}-modal-imageKey`}
1416
1142
  source={{
1417
1143
  uri: image,
1418
1144
  expiresIn: 86400,
@@ -1422,14 +1148,60 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1422
1148
  );
1423
1149
  }, [imageObject]);
1424
1150
 
1151
+ // Create a skeleton component for message bubbles with images
1425
1152
  const renderMessage = useCallback(
1426
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
+
1427
1199
  // Use memo to prevent unnecessary re-renders of each message
1428
1200
  return (
1429
1201
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1430
1202
  );
1431
1203
  },
1432
- [isShowImageViewer],
1204
+ [isShowImageViewer, uploadingMessageId, loading],
1433
1205
  );
1434
1206
 
1435
1207
  let onScroll = false;
@@ -1446,197 +1218,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1446
1218
  };
1447
1219
 
1448
1220
  const onEndReached = () => {
1449
- console.log('on end reached');
1450
1221
  if (!onScroll) return;
1451
1222
  onScroll = false;
1452
1223
  };
1453
1224
 
1454
- // Add debug logging to help diagnose the issue
1455
- useEffect(() => {
1456
- console.log('Current channel ID:', state.context.channelId);
1457
- console.log('Current state:', state.value);
1458
- console.log('Channel messages count:', state.context.channelMessages.length);
1459
- }, [state.context.channelId, state.value, state.context.channelMessages]);
1460
-
1461
- // Fix the infinite update loop in useEffect monitoring state changes
1462
- useEffect(() => {
1463
- // Only trigger effect if we have a specific state to handle
1464
- // Check if function exists and if we're in a valid state before calling implementation functions
1465
- if (state && typeof state.matches === 'function') {
1466
- if (state.matches(BaseState.FetchMessages)) {
1467
- console.log('In FetchMessages state, attempting to fetch messages');
1468
- // Use a ref to track if we've already fetched for this state update
1469
- if (!fetchInProgressRef.current) {
1470
- fetchInProgressRef.current = true;
1471
- fetchMessagesDirectly().finally(() => {
1472
- fetchInProgressRef.current = false;
1473
- });
1474
- }
1475
- } else if (state.matches(MainState.FetchMoreMessages)) {
1476
- if (!fetchMoreInProgressRef.current) {
1477
- fetchMoreInProgressRef.current = true;
1478
- fetchMoreMessagesImpl().then((result) => {
1479
- if (result.error) {
1480
- console.error('Error fetching more messages:', result.error);
1481
- safeSend({ type: 'ERROR', data: { message: result.error } });
1482
- } else {
1483
- safeSend({ type: 'FETCH_MORE_MESSAGES_SUCCESS', data: result });
1484
- }
1485
- fetchMoreInProgressRef.current = false;
1486
- });
1487
- }
1488
- } else if (state.matches(MainState.SendMessage)) {
1489
- if (!sendInProgressRef.current) {
1490
- sendInProgressRef.current = true;
1491
- sendMessageImpl().then((result) => {
1492
- if (result.error) {
1493
- console.error('Error sending message:', result.error);
1494
- safeSend({ type: 'ERROR', data: { message: result.error } });
1495
- } else {
1496
- safeSend({ type: 'SEND_MESSAGE_SUCCESS', data: result });
1497
- }
1498
- sendInProgressRef.current = false;
1499
- });
1500
- }
1501
- } else if (state.matches(MainState.SendMessageWithFile)) {
1502
- if (!sendFileInProgressRef.current) {
1503
- sendFileInProgressRef.current = true;
1504
- sendMessageWithFileImpl().then((result) => {
1505
- if (result.error) {
1506
- console.error('Error sending message with file:', result.error);
1507
- safeSend({ type: 'ERROR', data: { message: result.error } });
1508
- } else {
1509
- safeSend({ type: 'SEND_MESSAGE_WITH_FILE_SUCCESS', data: result });
1510
- }
1511
- sendFileInProgressRef.current = false;
1512
- });
1513
- }
1514
- } else if (state.matches(MainState.CreateDirectChannel)) {
1515
- if (!createChannelInProgressRef.current) {
1516
- createChannelInProgressRef.current = true;
1517
- createDirectChannelImpl().then((result) => {
1518
- if (result.error) {
1519
- console.error('Error creating direct channel:', result.error);
1520
- safeSend({ type: 'ERROR', data: { message: result.error } });
1521
- } else {
1522
- safeSend({ type: 'CREATE_DIRECT_CHANNEL_SUCCESS', data: result });
1523
- }
1524
- createChannelInProgressRef.current = false;
1525
- });
1526
- }
1527
- }
1528
- }
1529
- }, [
1530
- state?.value,
1531
- fetchMessagesDirectly,
1532
- fetchMoreMessagesImpl,
1533
- sendMessageImpl,
1534
- sendMessageWithFileImpl,
1535
- createDirectChannelImpl,
1536
- safeSend,
1537
- ]);
1538
-
1539
- // Add refs to prevent duplicate operations
1540
- const fetchInProgressRef = useRef(false);
1541
- const fetchMoreInProgressRef = useRef(false);
1542
- const sendInProgressRef = useRef(false);
1543
- const sendFileInProgressRef = useRef(false);
1544
- const createChannelInProgressRef = useRef(false);
1545
-
1546
- // Fix subscription handler to prevent infinite updates
1547
- const renderChatFooter = useCallback(() => {
1548
- return (
1549
- <>
1550
- <ImageViewerModal
1551
- isVisible={isShowImageViewer}
1552
- setVisible={setImageViewer}
1553
- modalContent={modalContent}
1554
- />
1555
- <SubscriptionHandler
1556
- channelId={state?.context?.channelId?.toString()}
1557
- subscribeToNewMessages={() =>
1558
- subscribeToMore({
1559
- document: CHAT_MESSAGE_ADDED,
1560
- variables: {
1561
- channelId: state?.context?.channelId?.toString(),
1562
- },
1563
- updateQuery: (prev, { subscriptionData }: any) => {
1564
- if (!subscriptionData?.data?.chatMessageAdded) return prev;
1565
-
1566
- const newMessage = subscriptionData.data.chatMessageAdded;
1567
- const currentMessages = prev?.messages?.data || [];
1568
-
1569
- // Check if message already exists to prevent duplicates
1570
- if (currentMessages.some((msg) => msg.id === newMessage.id)) {
1571
- return prev; // Skip update if message already exists
1572
- }
1573
-
1574
- // Use a ref to track the last processed message ID to prevent duplicate processing
1575
- if (lastProcessedMessageRef.current === newMessage.id) {
1576
- return prev;
1577
- }
1578
-
1579
- lastProcessedMessageRef.current = newMessage.id;
1580
-
1581
- // Use a batch update strategy to avoid frequent re-renders
1582
- queueMicrotask(() => {
1583
- safeSend({
1584
- type: ConversationActions.SET_CHANNEL_MESSAGES,
1585
- data: {
1586
- messages: uniqBy(
1587
- [...state.context.channelMessages, newMessage],
1588
- ({ id }) => id,
1589
- ),
1590
- totalCount: (prev?.messages?.totalCount || 0) + 1,
1591
- },
1592
- });
1593
- });
1594
-
1595
- return {
1596
- ...prev,
1597
- messages: {
1598
- ...prev?.messages,
1599
- data: [...currentMessages, newMessage],
1600
- totalCount: (prev?.messages?.totalCount || 0) + 1,
1601
- },
1602
- };
1603
- },
1604
- })
1605
- }
1606
- />
1607
- </>
1608
- );
1609
- }, [
1610
- isShowImageViewer,
1611
- modalContent,
1612
- state?.context?.channelId,
1613
- state?.context?.channelMessages,
1614
- subscribeToMore,
1615
- safeSend,
1616
- ]);
1617
-
1618
- // Add ref to track last processed message
1619
- const lastProcessedMessageRef = useRef(null);
1620
-
1621
- // Add optimized listViewProps to reduce re-renders and improve list performance
1622
- const listViewProps = useMemo(
1623
- () => ({
1624
- onEndReached: onEndReached,
1625
- onEndReachedThreshold: 0.5,
1626
- onMomentumScrollBegin: onMomentumScrollBegin,
1627
- removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1628
- initialNumToRender: 10, // Reduce initial render amount
1629
- maxToRenderPerBatch: 7, // Reduce number in each render batch
1630
- windowSize: 7, // Reduce the window size
1631
- updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
1632
- keyExtractor: (item) => item._id, // Add explicit key extractor
1633
- }),
1634
- [onEndReached, onMomentumScrollBegin],
1635
- );
1636
-
1637
1225
  // Add a loader for when more messages are being loaded
1638
1226
  const renderLoadEarlier = useCallback(() => {
1639
- return state?.context?.loadingOldMessages ? (
1227
+ return loadingOldMessages ? (
1640
1228
  <View
1641
1229
  style={{
1642
1230
  padding: 10,
@@ -1648,7 +1236,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1648
1236
  <Spinner size="small" color="#3b82f6" />
1649
1237
  </View>
1650
1238
  ) : null;
1651
- }, [state?.context?.loadingOldMessages]);
1239
+ }, [loadingOldMessages]);
1652
1240
 
1653
1241
  // Add renderInputToolbar function
1654
1242
  const renderInputToolbar = useCallback((props) => {
@@ -1672,15 +1260,106 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1672
1260
  );
1673
1261
  }, []);
1674
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
+
1675
1350
  // Return optimized component with performance improvements
1676
1351
  return (
1677
1352
  <View
1678
1353
  style={{
1679
1354
  flex: 1,
1680
1355
  backgroundColor: 'white',
1356
+ position: 'relative',
1681
1357
  }}
1682
1358
  >
1683
- {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}
1684
1363
 
1685
1364
  <GiftedChat
1686
1365
  ref={messageRootListRef}
@@ -1690,20 +1369,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1690
1369
  listViewProps={{
1691
1370
  ...listViewProps,
1692
1371
  contentContainerStyle: {
1693
- paddingBottom: 10,
1372
+ paddingBottom: selectedImage ? 90 : 0, // Add padding at the bottom when image is selected
1694
1373
  },
1695
- keyboardShouldPersistTaps: 'handled',
1696
1374
  }}
1697
1375
  onSend={handleSend}
1698
- text={safeContextProperty('messageText', ' ') || ' '}
1699
- onInputTextChanged={(text) =>
1700
- safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText: text } })
1701
- }
1702
- renderFooter={() =>
1703
- safeContextProperty('loading') || safeContextProperty('imageLoading') ? (
1704
- <Spinner color={'#3b82f6'} />
1705
- ) : null
1706
- }
1376
+ text={messageText || ' '}
1377
+ onInputTextChanged={(text) => setMessageText(text)}
1378
+ renderFooter={() => (loading && !images.length ? <Spinner color={'#3b82f6'} /> : null)}
1707
1379
  scrollToBottom
1708
1380
  user={{
1709
1381
  _id: auth?.id || '',
@@ -1714,14 +1386,13 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1714
1386
  renderMessageText={renderMessageText}
1715
1387
  renderInputToolbar={renderInputToolbar}
1716
1388
  minInputToolbarHeight={50}
1717
- renderActions={safeContextProperty('channelId') && renderActions}
1718
- renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
1389
+ renderActions={channelId && renderActions}
1719
1390
  renderMessage={renderMessage}
1720
1391
  renderChatFooter={renderChatFooter}
1721
1392
  renderLoadEarlier={renderLoadEarlier}
1722
- loadEarlier={state?.context?.totalCount > state?.context?.channelMessages?.length}
1723
- isLoadingEarlier={state?.context?.loadingOldMessages}
1724
- 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
1725
1396
  textInputProps={{
1726
1397
  style: {
1727
1398
  borderWidth: 1,
@@ -1759,15 +1430,27 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
1759
1430
  };
1760
1431
 
1761
1432
  const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptionHandlerProps) => {
1762
- // Use memo for the channelId dependency to prevent unnecessary subscriptions
1433
+ // Store the channelId in a ref to track changes
1763
1434
  const channelIdRef = useRef(channelId);
1764
1435
 
1765
1436
  useEffect(() => {
1766
- if (channelId && channelId !== channelIdRef.current) {
1767
- channelIdRef.current = channelId;
1768
- console.log('Setting up subscription for channel:', channelId);
1769
- return subscribeToNewMessages();
1437
+ // Don't set up subscription if there's no channel ID
1438
+ if (!channelId) {
1439
+ return;
1770
1440
  }
1441
+
1442
+ // Call the subscribe function and store the unsubscribe function
1443
+ const unsubscribe = subscribeToNewMessages();
1444
+
1445
+ // Update the ref with the current channelId
1446
+ channelIdRef.current = channelId;
1447
+
1448
+ // Return cleanup function
1449
+ return () => {
1450
+ if (unsubscribe && typeof unsubscribe === 'function') {
1451
+ unsubscribe();
1452
+ }
1453
+ };
1771
1454
  }, [channelId, subscribeToNewMessages]);
1772
1455
 
1773
1456
  return null;