@messenger-box/platform-mobile 10.0.3-alpha.17 → 10.0.3-alpha.18

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 (30) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  3. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  4. package/lib/screens/inbox/components/CachedImage/index.js +125 -16
  5. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  6. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +32 -21
  7. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  8. package/lib/screens/inbox/containers/ConversationView.js +1174 -403
  9. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  10. package/lib/screens/inbox/containers/Dialogs.js +290 -21
  11. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  12. package/lib/screens/inbox/containers/ThreadConversationView.js +858 -351
  13. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  14. package/lib/screens/inbox/containers/workflow/conversation-xstate.js +380 -0
  15. package/lib/screens/inbox/containers/workflow/conversation-xstate.js.map +1 -0
  16. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +235 -0
  17. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +1 -0
  18. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js +438 -0
  19. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js.map +1 -0
  20. package/package.json +2 -2
  21. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  22. package/src/screens/inbox/components/CachedImage/index.tsx +137 -17
  23. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +35 -9
  24. package/src/screens/inbox/containers/ConversationView.tsx +1512 -647
  25. package/src/screens/inbox/containers/Dialogs.tsx +415 -123
  26. package/src/screens/inbox/containers/ThreadConversationView.tsx +1053 -288
  27. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  28. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  29. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  30. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
@@ -12,7 +12,7 @@ import {
12
12
  Spinner,
13
13
  Text,
14
14
  } from '@admin-layout/gluestack-ui-mobile';
15
- import { Platform, TouchableHighlight, SafeAreaView } from 'react-native';
15
+ import { Platform, TouchableHighlight, SafeAreaView, View } from 'react-native';
16
16
  import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native';
17
17
  import { navigationRef } from '@common-stack/client-react';
18
18
  import { useSelector } from 'react-redux';
@@ -20,7 +20,7 @@ import { orderBy, startCase, uniqBy } from 'lodash-es';
20
20
  import * as ImagePicker from 'expo-image-picker';
21
21
  import { encode as atob } from 'base-64';
22
22
  import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
23
- import { Actions, GiftedChat, IMessage, MessageText, Send } from 'react-native-gifted-chat';
23
+ import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
24
24
  import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo } from 'common';
25
25
  import {
26
26
  OnChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
@@ -37,6 +37,20 @@ import { format, isToday, isYesterday } from 'date-fns';
37
37
  import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
38
38
  import CachedImage from '../components/CachedImage';
39
39
  import { config } from '../config';
40
+ import {
41
+ conversationXstate,
42
+ Actions as ConversationActions,
43
+ BaseState,
44
+ MainState,
45
+ } from './workflow/conversation-xstate';
46
+ import colors from 'tailwindcss/colors';
47
+
48
+ // Define an extended interface for ImagePickerAsset with url property
49
+ interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
50
+ url?: string;
51
+ fileName?: string;
52
+ mimeType?: string;
53
+ }
40
54
 
41
55
  const {
42
56
  MESSAGES_PER_PAGE,
@@ -76,35 +90,348 @@ export interface AlertMessageAttachmentsInterface {
76
90
  };
77
91
  }
78
92
 
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
+
79
317
  const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMessage, ...rest }: any) => {
80
318
  const [channelToTop, setChannelToTop] = useState(0);
81
- const [channelMessages, setChannelMessages] = useState<any>([]);
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]);
393
+
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
+ }, []);
415
+
416
+ // Use cleanup function to prevent setting state after unmount
417
+ useEffect(() => {
418
+ return () => {
419
+ isMountedRef.current = false;
420
+ };
421
+ }, []);
422
+
82
423
  const auth: any = useSelector(userSelector);
83
- const [totalCount, setTotalCount] = useState<any>(0);
84
- const [selectedImage, setImage] = useState<string>('');
85
424
  const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
86
- const [channelId, setChannelId] = useState<any>(rest?.isCreateNewChannel ? null : ChannelId);
87
- const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
88
425
  const navigation = useNavigation<any>();
89
- const [files, setFiles] = useState<File[]>([]);
90
- const [images, setImages] = useState<ImagePicker.ImagePickerAsset[]>([]);
91
- const [msg, setMsg] = useState<string>('');
92
- const [loading, setLoading] = useState(false);
93
- const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
94
- const [imageLoading, setImageLoading] = useState(false);
95
- const [expoTokens, setExpoTokens] = useState<any[]>([]);
426
+ const [selectedImage, setImage] = useState<string>('');
96
427
  const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
97
428
  const [imageObject, setImageObject] = useState<any>({});
98
- const [skip, setSkip] = useState(0);
99
429
  const messageRootListRef = useRef<any>(null);
100
430
  const isFocused = useIsFocused();
101
431
 
102
- const [addDirectChannel, { loading: addDirectChannaleLoading }] = useAddDirectChannelMutation();
103
-
432
+ const [addDirectChannel] = useAddDirectChannelMutation();
104
433
  const { startUpload } = useUploadFilesNative();
105
-
106
434
  const [sendMsg] = useSendMessagesMutation();
107
-
108
435
  const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
109
436
 
110
437
  const {
@@ -115,119 +442,473 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
115
442
  subscribeToMore,
116
443
  }: any = useMessagesQuery({
117
444
  variables: {
118
- channelId: channelId?.toString(),
445
+ channelId: state.context.channelId?.toString(),
119
446
  parentId: null,
120
447
  limit: MESSAGES_PER_PAGE,
121
- skip: skip,
448
+ skip: state.context.skip,
122
449
  },
123
- skip: !channelId,
450
+ skip: !state.context.channelId,
124
451
  fetchPolicy: 'cache-and-network',
125
452
  nextFetchPolicy: 'cache-first',
126
453
  refetchWritePolicy: 'merge',
454
+ 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
+ }
464
+ },
465
+ onError: (error) => {
466
+ console.error('MESSAGE QUERY ERROR:', error);
467
+ },
127
468
  });
128
469
 
129
- // const {
130
- // data: channelsDetail,
131
- // loading: channelLoading,
132
- // refetch: refetchChannelDetail,
133
- // } = useViewChannelDetailQuery({
134
- // variables: {
135
- // id: channelId?.toString(),
136
- // },
137
- // });
138
- // const { data: users } = useGetAllUsersQuery();
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
+ }
477
+
478
+ try {
479
+ console.log('💫 FETCHING messages for channel:', channelId);
480
+
481
+ // Use loading state to prevent duplicate fetches
482
+ send({ type: ConversationActions.START_LOADING });
483
+
484
+ const response = await refetch({
485
+ channelId: channelId.toString(),
486
+ parentId: null,
487
+ limit: MESSAGES_PER_PAGE,
488
+ skip: 0,
489
+ });
490
+
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 });
508
+ }
509
+ } catch (error) {
510
+ console.error('ERROR fetching messages:', error);
511
+ send({ type: ConversationActions.STOP_LOADING });
512
+ }
513
+ }, [safeGetContext, refetch, safeSend]);
514
+
515
+ const fetchMoreMessagesImpl = useCallback(async () => {
516
+ try {
517
+ const response = await fetchMoreMessages({
518
+ variables: {
519
+ channelId: state.context.channelId?.toString(),
520
+ parentId: null,
521
+ skip: state.context.channelMessages.length,
522
+ },
523
+ });
524
+
525
+ if (!response?.data?.messages?.data) {
526
+ return { error: 'No messages returned' };
527
+ }
528
+
529
+ return { messages: response.data.messages.data };
530
+ } catch (error) {
531
+ return { error: String(error) };
532
+ }
533
+ }, [state.context.channelId, state.context.channelMessages.length, fetchMoreMessages]);
534
+
535
+ const sendMessageImpl = useCallback(async () => {
536
+ try {
537
+ const notificationData: IExpoNotificationData = {
538
+ url: config.INBOX_MESSEGE_PATH,
539
+ params: { channelId: state.context.channelId, hideTabBar: true },
540
+ screen: 'DialogMessages',
541
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
542
+ };
543
+
544
+ const response = await sendMsg({
545
+ variables: {
546
+ channelId: state.context.channelId,
547
+ content: state.context.messageText,
548
+ notificationParams: notificationData,
549
+ },
550
+ });
551
+
552
+ return { message: response.data?.sendMessage };
553
+ } catch (error) {
554
+ return { error: String(error) };
555
+ }
556
+ }, [state.context.channelId, state.context.messageText, sendMsg]);
557
+
558
+ // Fix the image selection process to ensure proper format for upload
559
+ const onSelectImages = async () => {
560
+ safeSend({ type: ConversationActions.START_LOADING });
561
+
562
+ try {
563
+ console.log('Starting image picker...');
564
+ let imageSource = await ImagePicker.launchImageLibraryAsync({
565
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
566
+ allowsEditing: true,
567
+ aspect: [4, 3],
568
+ quality: 0.8,
569
+ base64: true,
570
+ exif: false,
571
+ });
572
+
573
+ 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
+ // Get the asset
586
+ const selectedAsset = imageSource?.assets?.[0];
587
+ if (!selectedAsset) {
588
+ console.error('No asset found in selected image');
589
+ safeSend({ type: ConversationActions.STOP_LOADING });
590
+ return;
591
+ }
592
+
593
+ // Create a base64 image string for preview
594
+ const base64Data = selectedAsset.base64;
595
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
596
+
597
+ // Format the asset for upload service requirements
598
+ const asset: ExtendedImagePickerAsset = {
599
+ ...selectedAsset,
600
+ url: selectedAsset.uri,
601
+ fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
602
+ mimeType: 'image/jpeg',
603
+ };
604
+
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
+ // 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');
622
+ } else {
623
+ console.log('Image selection cancelled');
624
+ safeSend({ type: ConversationActions.STOP_LOADING });
625
+ }
626
+ } catch (error) {
627
+ console.error('Error selecting image:', error);
628
+ safeSend({ type: ConversationActions.STOP_LOADING });
629
+ }
630
+ };
631
+
632
+ // Update the sendMessageWithFileImpl function to fix image uploads
633
+ const sendMessageWithFileImpl = useCallback(async () => {
634
+ try {
635
+ console.log('Executing sendMessageWithFileImpl');
636
+
637
+ // Generate a unique post ID for the message
638
+ const postId = objectId();
639
+ console.log('Generated postId for file upload:', postId);
640
+
641
+ // Prepare notification data
642
+ const notificationData: IExpoNotificationData = {
643
+ url: config.INBOX_MESSEGE_PATH,
644
+ params: { channelId: state.context.channelId, hideTabBar: true },
645
+ screen: 'DialogMessages',
646
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
647
+ };
648
+
649
+ // Safety check for images
650
+ if (!state.context.images || state.context.images.length === 0) {
651
+ console.error('No images found in state');
652
+ return { error: 'No images available to upload' };
653
+ }
654
+
655
+ // Format the images for upload if needed
656
+ const imagesToUpload = state.context.images.map((img) => {
657
+ // Ensure the image has all required properties
658
+ return {
659
+ ...img,
660
+ uri: img.uri || img.url, // Use either uri or url
661
+ type: 'image/jpeg',
662
+ name: img.fileName || `image_${Date.now()}.jpg`,
663
+ };
664
+ });
665
+
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
+ );
677
+
678
+ // Upload the files
679
+ console.log('Starting file upload...');
680
+ const uploadResponse = await startUpload({
681
+ file: imagesToUpload,
682
+ saveUploadedFile: {
683
+ variables: { postId },
684
+ },
685
+ createUploadLink: {
686
+ variables: { postId },
687
+ },
688
+ });
689
+
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
+ if (uploadResponse?.error) {
698
+ console.error('Upload error:', uploadResponse.error);
699
+ return { error: String(uploadResponse.error) };
700
+ }
701
+
702
+ // Get uploaded file IDs
703
+ 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
+ const files = uploadedFiles?.map((f: any) => f.id) ?? null;
712
+
713
+ console.log('Files uploaded successfully. File IDs:', files);
714
+
715
+ // 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
+ const response = await sendMsg({
725
+ variables: {
726
+ postId,
727
+ channelId: state.context.channelId,
728
+ content: state.context.messageText || ' ', // Use a space if no text
729
+ files,
730
+ notificationParams: notificationData,
731
+ },
732
+ });
733
+
734
+ 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
+ // 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);
754
+ }
755
+
756
+ return { message: response.data?.sendMessage };
757
+ } catch (error) {
758
+ console.error('Error in sendMessageWithFileImpl:', error);
759
+ return { error: String(error) };
760
+ }
761
+ }, [state.context.channelId, state.context.messageText, state.context.images, startUpload, sendMsg, safeSend]);
762
+
763
+ const createDirectChannelImpl = useCallback(async () => {
764
+ try {
765
+ if (
766
+ !rest?.isCreateNewChannel ||
767
+ rest?.newChannelData?.type !== RoomType?.Direct ||
768
+ !rest?.newChannelData?.userIds?.length
769
+ ) {
770
+ return { error: 'Invalid channel data' };
771
+ }
772
+
773
+ const response = await addDirectChannel({
774
+ variables: {
775
+ receiver: [...(rest?.newChannelData?.userIds ?? [])],
776
+ displayName: 'DIRECT CHANNEL',
777
+ },
778
+ });
779
+
780
+ if (!response?.data?.createDirectChannel?.id) {
781
+ return { error: 'Failed to create channel' };
782
+ }
783
+
784
+ const newChannelId = response.data.createDirectChannel.id;
785
+
786
+ const notificationData: IExpoNotificationData = {
787
+ url: config.INBOX_MESSEGE_PATH,
788
+ params: { channelId: newChannelId, hideTabBar: true },
789
+ screen: 'DialogMessages',
790
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
791
+ };
792
+
793
+ await sendMsg({
794
+ variables: {
795
+ channelId: newChannelId,
796
+ content: state.context.messageText,
797
+ notificationParams: notificationData,
798
+ },
799
+ });
800
+
801
+ return { channelId: newChannelId };
802
+ } catch (error) {
803
+ return { error: String(error) };
804
+ }
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]);
139
811
 
140
812
  React.useEffect(() => {
141
813
  return () => {
142
- setChannelMessages([]);
814
+ send({ type: ConversationActions.CLEAR_MESSAGES });
143
815
  };
144
816
  }, []);
145
817
 
146
818
  useFocusEffect(
147
819
  React.useCallback(() => {
148
- // Do something when the screen is focused
149
- setSkip(0);
150
- // refetchChannelDetail({ id: channelId?.toString() });
151
- if (channelId) {
152
- refetch({
153
- channelId: channelId?.toString(),
154
- parentId: null,
155
- limit: MESSAGES_PER_PAGE,
156
- skip: 0,
157
- }).then(({ data }) => {
158
- if (!data?.messages) {
159
- return;
160
- }
161
- const { data: messages, totalCount }: any = data.messages;
162
- setTotalCount(totalCount);
163
- setChannelMessages(messages);
164
- });
820
+ if (state.context.channelId) {
821
+ send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: state.context.channelId } });
165
822
  }
166
823
  return () => {
167
- // Do something when the screen is unfocused
168
- // Useful for cleanup functions
169
- setChannelId(null);
170
- setTotalCount(0);
171
- setSkip(0);
824
+ send({ type: ConversationActions.CLEAR_MESSAGES });
172
825
  };
173
- }, [channelId, isFocused]),
826
+ }, [state.context.channelId, isFocused]),
174
827
  );
175
828
 
176
829
  React.useEffect(() => {
177
830
  const currentChannelId = ChannelId || currentRoute?.params?.channelId;
178
- setChannelId(currentChannelId);
831
+ if (currentChannelId) {
832
+ console.log('Setting initial channel ID:', currentChannelId);
833
+ send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: currentChannelId } });
834
+ }
179
835
  }, [ChannelId, currentRoute]);
180
836
 
181
837
  React.useEffect(() => {
182
- if (selectedImage) setImageLoading(false);
183
- }, [selectedImage]);
838
+ if (state.context.selectedImage) {
839
+ send({ type: ConversationActions.STOP_LOADING });
840
+ }
841
+ }, [state.context.selectedImage]);
184
842
 
185
843
  useEffect(() => {
186
844
  if (data?.messages?.data) {
187
- const { data: messages, totalCount: messeageTotalCount } = data.messages;
188
- console.log('messeageTotalCount', messeageTotalCount, ' totalCount=', totalCount);
189
-
190
- if (
191
- (messages && messages.length > 0 && messeageTotalCount > totalCount) ||
192
- (messages && messages.length > 0 && (loadingOldMessages || channelMessages.length === 0))
193
- ) {
194
- setChannelMessages((oldMessages: any) => uniqBy([...messages, ...oldMessages], ({ id }) => id));
195
- setTotalCount(messeageTotalCount);
196
- }
845
+ console.log('📩 QUERY DATA CHANGED - Messages received:', data.messages.data.length);
846
+ const { data: messages, totalCount: messageTotalCount } = data.messages;
197
847
 
198
- if (loadingOldMessages && channelMessages) setLoadingOldMessages(false);
199
- }
200
- }, [data, loadingOldMessages, channelMessages, totalCount]);
848
+ if (messages && messages.length > 0) {
849
+ console.log('📩 QUERY DATA - Setting channel messages, count:', messages.length);
201
850
 
202
- const onFetchOld = useCallback(async () => {
203
- if (totalCount > channelMessages.length && !loadingOldMessages) {
204
- setLoadEarlierMsg(true);
205
- try {
206
- const response = await fetchMoreMessages({
207
- variables: {
208
- channelId: channelId?.toString(),
209
- parentId: null,
210
- skip: channelMessages.length,
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,
211
857
  },
212
858
  });
213
- if (response?.data) {
214
- setSkip(channelMessages.length);
215
- setLoadEarlierMsg(false);
216
- setLoadingOldMessages(true);
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
+ );
217
875
  }
218
- } catch (error: any) {
219
- setLoadEarlierMsg(false);
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);
220
883
  }
884
+ }
885
+ }, [data]);
886
+
887
+ // Optimize onFetchOld by adding debounce logic
888
+ const onFetchOld = useCallback(() => {
889
+ // Prevent multiple rapid calls
890
+ if (fetchOldDebounceRef.current) return;
221
891
 
222
- // ?.then((res: any) => {
223
- // setLoadingOldMessages(true);
224
- // });
892
+ // Check if we need to fetch more messages
893
+ if (
894
+ state?.context?.totalCount > state?.context?.channelMessages?.length &&
895
+ !state?.context?.loadingOldMessages
896
+ ) {
897
+ // Set debounce
898
+ fetchOldDebounceRef.current = true;
899
+
900
+ // Send fetch event
901
+ send({ type: ConversationActions.FETCH_MORE_MESSAGES });
902
+
903
+ // Clear debounce after a timeout
904
+ setTimeout(() => {
905
+ fetchOldDebounceRef.current = false;
906
+ }, 1000);
225
907
  }
226
- }, [totalCount, channelMessages]);
908
+ }, [state?.context?.totalCount, state?.context?.channelMessages, state?.context?.loadingOldMessages]);
227
909
 
228
- // const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
229
- // return contentOffset.y <= 100; // 100px from top
230
- // };
910
+ // Add debounce ref
911
+ const fetchOldDebounceRef = useRef(false);
231
912
 
232
913
  const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
233
914
  const paddingToTop = 60;
@@ -246,458 +927,492 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
246
927
  return new File([u8arr], filename, { type: mime });
247
928
  };
248
929
 
249
- const onSelectImages = async () => {
250
- setImageLoading(true);
251
- let imageSource = await ImagePicker.launchImageLibraryAsync({
252
- // mediaTypes: ImagePicker.MediaTypeOptions.Images,
253
- mediaTypes: ['images'],
254
- allowsEditing: true,
255
- aspect: [4, 3],
256
- quality: 1,
257
- base64: true,
258
- });
259
- // if (!imageSource.cancelled) {
260
- // const image = 'data:image/jpeg;base64,' + imageSource?.base64;
261
- // setImage(image);
262
- // const file = dataURLtoFile(image, 'inputImage.jpg');
263
- // setFiles((files) => files.concat(file));
264
- // setImages((images) => images.concat(imageSource as ImagePicker.ImageInfo));
265
- // }
266
- // if (imageSource.cancelled) setLoading(false);
267
- if (!imageSource.canceled) {
268
- setImages((images) => images.concat(imageSource?.assets[0] as ImagePicker.ImagePickerAsset));
269
- const image = 'data:image/jpeg;base64,' + imageSource?.assets[0]?.base64;
270
- setImage(image);
271
- const file = dataURLtoFile(image, 'inputImage.jpg');
272
- setFiles((files) => files.concat(file));
273
- }
274
-
275
- if (imageSource.canceled) setLoading(false);
276
- };
930
+ // Fix the render send function to ensure it works for image-only messages
931
+ const renderSend = useCallback(
932
+ (props) => {
933
+ // 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;
277
936
 
278
- const createDirectChannel = useCallback(
279
- (msg: string) => {
280
- if (
281
- rest?.isCreateNewChannel &&
282
- rest?.newChannelData?.type === RoomType?.Direct &&
283
- rest?.newChannelData?.userIds?.length > 0
284
- ) {
285
- addDirectChannel({
286
- variables: {
287
- receiver: [...(rest?.newChannelData?.userIds ?? [])],
288
- displayName: 'DIRECT CHANNEL',
289
- },
290
- })
291
- ?.then(async (res) => {
292
- if (res?.data?.createDirectChannel?.id) {
293
- setChannelId(res?.data?.createDirectChannel?.id);
294
- const notificationData: IExpoNotificationData = {
295
- url: config.INBOX_MESSEGE_PATH,
296
- params: { channelId: res?.data?.createDirectChannel?.id, hideTabBar: true },
297
- screen: 'DialogMessages',
298
- other: { sound: Platform.OS === 'android' ? undefined : 'default' },
299
- };
300
- setLoading(true);
301
- await sendMsg({
302
- variables: {
303
- channelId: res?.data?.createDirectChannel?.id,
304
- content: msg,
305
- notificationParams: notificationData,
306
- },
307
- update: (cache, { data, errors }: any) => {
308
- if (!data || errors) {
309
- setLoading(false);
310
- return;
311
- }
312
- setChannelToTop(channelToTop + 1);
313
- setLoading(false);
314
- setMsg('');
315
- },
316
- });
317
- }
318
- })
319
- ?.catch((e: any) => console.log('error', JSON.stringify(e)));
320
- }
937
+ return (
938
+ <Send
939
+ {...props}
940
+ disabled={!canSend}
941
+ containerStyle={{
942
+ justifyContent: 'center',
943
+ alignItems: 'center',
944
+ height: 40,
945
+ width: 44,
946
+ marginRight: 4,
947
+ marginBottom: 0,
948
+ marginLeft: 4,
949
+ }}
950
+ >
951
+ <View style={{ padding: 4 }}>
952
+ <MaterialCommunityIcons
953
+ name="send-circle"
954
+ size={32}
955
+ color={canSend ? colors.blue[500] : colors.gray[400]}
956
+ />
957
+ </View>
958
+ </Send>
959
+ );
321
960
  },
322
- [rest],
961
+ [state?.context?.channelId, state?.context?.images, rest?.isCreateNewChannel],
323
962
  );
324
963
 
964
+ // Fix the handleSend function to properly handle image-only messages
325
965
  const handleSend = useCallback(
326
- async (message: string) => {
327
- console.log('images', images);
328
- if (!channelId) return;
329
- if (!message && message != ' ' && images.length == 0) return;
966
+ async (messages) => {
967
+ // 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);
330
971
 
331
- const notificationData: IExpoNotificationData = {
332
- url: config.INBOX_MESSEGE_PATH,
333
- params: { channelId, hideTabBar: true },
334
- screen: 'DialogMessages',
335
- other: { sound: Platform.OS === 'android' ? undefined : 'default' },
336
- };
972
+ // 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');
975
+ return;
976
+ }
337
977
 
338
- if (images && images.length > 0) {
339
- const postId = objectId();
978
+ // 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;
340
981
 
341
- setLoading(true);
342
- const uploadResponse = await startUpload({
343
- file: images,
344
- saveUploadedFile: {
345
- variables: {
346
- postId,
347
- },
348
- },
349
- createUploadLink: {
350
- variables: {
351
- postId,
352
- },
353
- },
354
- });
355
- if (uploadResponse?.error) setLoading(false);
356
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
357
-
358
- if (uploadResponse.data) {
359
- setImage('');
360
- setFiles([]);
361
- setImages([]);
362
- //setLoading(false);
363
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
364
- await sendMsg({
365
- variables: {
366
- postId,
367
- channelId,
368
- content: message,
369
- files,
370
- notificationParams: notificationData,
371
- },
372
- update: (cache, { data, errors }: any) => {
373
- if (!data || errors) {
374
- setLoading(false);
375
- return;
376
- }
377
- //Temporary fix.....//
378
- const newMessage: any = data?.sendMessage;
379
- setChannelMessages((oldMessages: any) =>
380
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
381
- );
382
- setTotalCount((t) => t + 1);
383
- //Temporary fix.....//
384
-
385
- setChannelToTop(channelToTop + 1);
386
- setLoading(false);
387
- setMsg('');
388
- },
389
- });
982
+ if (!hasText && !hasImages) {
983
+ console.log('Nothing to send - no text or images');
984
+ return;
985
+ }
986
+
987
+ // Set the message text in the state (even if empty for image-only messages)
988
+ safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText } });
989
+
990
+ // Handle direct channel creation if needed
991
+ if (rest?.isCreateNewChannel && !state.context.channelId) {
992
+ if (rest?.newChannelData?.type === RoomType?.Direct) {
993
+ safeSend({ type: ConversationActions.CREATE_DIRECT_CHANNEL });
390
994
  }
995
+ return;
996
+ }
997
+
998
+ // Send message with or without image based on state
999
+ if (hasImages) {
1000
+ console.log('Sending message with file');
1001
+ safeSend({ type: ConversationActions.SEND_MESSAGE_WITH_FILE });
391
1002
  } else {
392
- setLoading(true);
393
- await sendMsg({
394
- variables: {
395
- channelId,
396
- content: message,
397
- notificationParams: notificationData,
398
- },
399
- update: (cache, { data, errors }: any) => {
400
- if (!data || errors) {
401
- setLoading(false);
402
- return;
403
- }
404
- //Temporary fix.....//
405
- const newMessage: any = data?.sendMessage;
406
- setChannelMessages((oldMessages: any) => uniqBy([...oldMessages, newMessage], ({ id }) => id));
407
- setTotalCount((t) => t + 1);
408
- //Temporary fix.....//
409
-
410
- setChannelToTop(channelToTop + 1);
411
- setLoading(false);
412
- setMsg('');
413
- },
414
- });
1003
+ console.log('Sending text-only message');
1004
+ safeSend({ type: ConversationActions.SEND_MESSAGE });
415
1005
  }
416
1006
  },
417
- [setChannelMessages, channelId, images, channelToTop, expoTokens],
1007
+ [state.context.channelId, state.context.images, rest?.isCreateNewChannel, rest?.newChannelData?.type, safeSend],
418
1008
  );
419
1009
 
420
- const messageList = useMemo(() => {
421
- let currentDate = '';
422
- let res: any = [];
423
- const filteredMessages =
424
- channelMessages && channelMessages?.length > 0 ? uniqBy([...channelMessages], ({ id }: any) => id) : [];
425
- if (channelId && filteredMessages?.length > 0) {
426
- orderBy(channelMessages, ['createdAt'], ['desc']).map((msg) => {
427
- let message: IMessageProps = {
428
- _id: '',
429
- text: '',
430
- createdAt: 0,
431
- user: {
432
- _id: '',
433
- name: '',
434
- avatar: '',
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,
435
1034
  },
436
- type: '',
437
- };
438
- const date = new Date(msg.createdAt);
439
- message._id = msg.id;
440
- message.text = msg.message;
441
- message.createdAt = date;
442
- (message.user = {
443
- _id: msg.author.id,
444
- name: msg.author.givenName + ' ' + msg.author.familyName,
445
- avatar: msg.author?.picture,
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
+ // Log the first message for debugging
1070
+ if (state.context.channelMessages[0]) {
1071
+ const sampleMsg = state.context.channelMessages[0];
1072
+ console.log(
1073
+ '📷 Sample message files:',
1074
+ JSON.stringify({
1075
+ hasFiles: !!sampleMsg.files,
1076
+ fileCount: sampleMsg.files?.data?.length || 0,
1077
+ fileUrl: sampleMsg.files?.data?.[0]?.url || 'none',
446
1078
  }),
447
- (message.image = msg.files?.data[0]?.url),
448
- (message.sent = msg?.isDelivered),
449
- (message.received = msg?.isRead);
450
- message.type = msg?.type;
451
- message.propsConfiguration = msg?.propsConfiguration;
452
- message.replies = msg?.replies ?? [];
453
- message.isShowThreadMessage = isShowThreadMessage;
454
- res.push(message);
455
- });
1079
+ );
456
1080
  }
457
- return res?.length > 0 ? uniqBy([...res], ({ _id }: any) => _id) : [];
458
- //return res;
459
- }, [channelMessages, channelId]);
460
1081
 
461
- const renderSend = (props) => {
462
- return (
463
- <Send {...props} disabled={channelId || rest?.isCreateNewChannel ? false : true}>
464
- <Box>
465
- <MaterialCommunityIcons
466
- name="send-circle"
467
- style={{ marginBottom: 5, marginRight: 5 }}
468
- size={32}
469
- color={channelId || rest?.isCreateNewChannel ? '#2e64e5' : '#b8b2b2'}
470
- />
471
- </Box>
472
- </Send>
473
- );
474
- };
1082
+ // Use a more efficient approach - pre-filter messages once
1083
+ const filteredMessages = uniqBy(state.context.channelMessages, ({ id }) => id);
475
1084
 
476
- const renderMessageText = (props: any) => {
477
- const { currentMessage } = props;
478
- const lastReply: any = currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
479
-
480
- if (currentMessage.type === 'ALERT') {
481
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
482
- let action: string = '';
483
- let actionId: any = '';
484
- let params: any = {};
485
-
486
- if (attachment?.callToAction?.extraParams) {
487
- const extraParams: any = attachment?.callToAction?.extraParams;
488
- const route: any = extraParams?.route ?? null;
489
- let path: any = null;
490
- let param: any = null;
491
- if (role && role == PreDefinedRole.Guest) {
492
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
493
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
494
- } else if (role && role == PreDefinedRole.Owner) {
495
- path = route?.host?.name ? route?.host?.name ?? null : null;
496
- param = route?.host?.params ? route?.host?.params ?? null : null;
497
- } else {
498
- path = route?.host?.name ? route?.host?.name ?? null : null;
499
- param = route?.host?.params ? route?.host?.params ?? null : null;
500
- }
1085
+ // Skip processing if no filtered messages
1086
+ if (filteredMessages.length === 0) {
1087
+ return [];
1088
+ }
501
1089
 
502
- action = path;
503
- params = { ...param };
504
- } else if (attachment?.callToAction?.link) {
505
- action = CALL_TO_ACTION_PATH;
506
- actionId = attachment?.callToAction?.link.split('/').pop();
507
- params = { reservationId: actionId };
1090
+ // Transform messages only once and return
1091
+ return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
1092
+ const date = new Date(msg.createdAt);
1093
+
1094
+ // Extract image URL from files data
1095
+ let imageUrl = null;
1096
+ if (msg.files?.data && msg.files.data.length > 0) {
1097
+ const fileData = msg.files.data[0];
1098
+ if (fileData && fileData.url) {
1099
+ imageUrl = fileData.url;
1100
+ console.log('📷 Found image URL for message', msg.id, ':', imageUrl);
1101
+ }
508
1102
  }
509
1103
 
510
- return (
511
- <>
512
- {attachment?.callToAction && action ? (
513
- <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
514
- <Button
515
- variant={'outline'}
516
- size={'sm'}
517
- className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
518
- onPress={() => action && params && navigation.navigate(action, params)}
519
- // onPress={() => navigation.navigate(action, { reservationId: actionId })}
520
- >
521
- <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
522
- {attachment.callToAction.title}
523
- </ButtonText>
524
- </Button>
525
- <MessageText
526
- {...props}
527
- textStyle={{
528
- left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1104
+ // Create message in a more direct way
1105
+ return {
1106
+ _id: msg.id,
1107
+ text: msg.message,
1108
+ createdAt: date,
1109
+ user: {
1110
+ _id: msg.author?.id || '',
1111
+ name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
1112
+ avatar: msg.author?.picture || '',
1113
+ },
1114
+ image: imageUrl,
1115
+ sent: msg?.isDelivered,
1116
+ received: msg?.isRead,
1117
+ type: msg?.type,
1118
+ propsConfiguration: msg?.propsConfiguration,
1119
+ replies: msg?.replies ?? [],
1120
+ isShowThreadMessage,
1121
+ };
1122
+ });
1123
+ }, [state?.context?.channelMessages, state?.context?.channelId, isShowThreadMessage]);
1124
+
1125
+ // Memoize the renderMessageText function
1126
+ const renderMessageText = useCallback(
1127
+ (props: any) => {
1128
+ const { currentMessage } = props;
1129
+ const lastReply: any =
1130
+ currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
1131
+
1132
+ if (currentMessage.type === 'ALERT') {
1133
+ const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1134
+ let action: string = '';
1135
+ let actionId: any = '';
1136
+ let params: any = {};
1137
+
1138
+ if (attachment?.callToAction?.extraParams) {
1139
+ const extraParams: any = attachment?.callToAction?.extraParams;
1140
+ const route: any = extraParams?.route ?? null;
1141
+ let path: any = null;
1142
+ let param: any = null;
1143
+ if (role && role == PreDefinedRole.Guest) {
1144
+ path = route?.guest?.name ? route?.guest?.name ?? null : null;
1145
+ param = route?.guest?.params ? route?.guest?.params ?? null : null;
1146
+ } else if (role && role == PreDefinedRole.Owner) {
1147
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1148
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1149
+ } else {
1150
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1151
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1152
+ }
1153
+
1154
+ action = path;
1155
+ params = { ...param };
1156
+ } else if (attachment?.callToAction?.link) {
1157
+ action = CALL_TO_ACTION_PATH;
1158
+ actionId = attachment?.callToAction?.link.split('/').pop();
1159
+ params = { reservationId: actionId };
1160
+ }
1161
+
1162
+ return (
1163
+ <>
1164
+ {attachment?.callToAction && action ? (
1165
+ <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
1166
+ <Button
1167
+ variant={'outline'}
1168
+ size={'sm'}
1169
+ className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
1170
+ onPress={() => action && params && navigation.navigate(action, params)}
1171
+ >
1172
+ <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
1173
+ {attachment.callToAction.title}
1174
+ </ButtonText>
1175
+ </Button>
1176
+ <MessageText
1177
+ {...props}
1178
+ textStyle={{
1179
+ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1180
+ }}
1181
+ />
1182
+ </Box>
1183
+ ) : (
1184
+ <TouchableHighlight
1185
+ underlayColor={'#c0c0c0'}
1186
+ style={{ width: '100%' }}
1187
+ onPress={() => {
1188
+ if (currentMessage?.isShowThreadMessage)
1189
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
1190
+ channelId: state?.context?.channelId,
1191
+ title: 'Message',
1192
+ postParentId: currentMessage?._id,
1193
+ isPostParentIdThread: true,
1194
+ });
529
1195
  }}
530
- />
531
- </Box>
532
- ) : (
533
- <TouchableHighlight
534
- underlayColor={'#c0c0c0'}
535
- style={{ width: '100%' }}
536
- onPress={() => {
537
- if (currentMessage?.isShowThreadMessage)
538
- navigation.navigate(config.THREAD_MESSEGE_PATH, {
539
- channelId: channelId,
540
- title: 'Message',
541
- postParentId: currentMessage?._id,
542
- isPostParentIdThread: true,
543
- });
544
- }}
545
- >
546
- <>
547
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
548
- {currentMessage?.replies?.data?.length > 0 && (
549
- <HStack space={'sm'} className="px-1 items-center">
550
- <HStack>
551
- {currentMessage?.replies?.data
552
- ?.filter(
553
- (v: any, i: any, a: any) =>
554
- a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
555
- )
556
- ?.slice(0, 2)
557
- ?.reverse()
558
- ?.map((p: any, i: Number) => (
559
- <Avatar
560
- key={'conversations-view-key-' + i}
561
- size={'sm'}
562
- className="bg-transparent"
563
- >
564
- <AvatarFallbackText>
565
- {startCase(p?.author?.username?.charAt(0))}
566
- </AvatarFallbackText>
567
- <AvatarImage
568
- alt="user image"
569
- style={{
570
- borderRadius: 6,
571
- borderWidth: 2,
572
- borderColor: '#fff',
573
- }}
574
- source={{
575
- uri: p?.author?.picture,
576
- }}
577
- />
578
- </Avatar>
579
- ))}
1196
+ >
1197
+ <>
1198
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1199
+ {currentMessage?.replies?.data?.length > 0 && (
1200
+ <HStack space={'sm'} className="px-1 items-center">
1201
+ <HStack>
1202
+ {currentMessage?.replies?.data
1203
+ ?.filter(
1204
+ (v: any, i: any, a: any) =>
1205
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) ===
1206
+ i,
1207
+ )
1208
+ ?.slice(0, 2)
1209
+ ?.reverse()
1210
+ ?.map((p: any, i: Number) => (
1211
+ <Avatar
1212
+ key={'conversations-view-key-' + i}
1213
+ size={'sm'}
1214
+ className="bg-transparent"
1215
+ >
1216
+ <AvatarFallbackText>
1217
+ {startCase(p?.author?.username?.charAt(0))}
1218
+ </AvatarFallbackText>
1219
+ <AvatarImage
1220
+ alt="user image"
1221
+ style={{
1222
+ borderRadius: 6,
1223
+ borderWidth: 2,
1224
+ borderColor: '#fff',
1225
+ }}
1226
+ source={{
1227
+ uri: p?.author?.picture,
1228
+ }}
1229
+ />
1230
+ </Avatar>
1231
+ ))}
1232
+ </HStack>
1233
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
1234
+ {currentMessage?.replies?.totalCount}{' '}
1235
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
1236
+ </Text>
1237
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
1238
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
1239
+ </Text>
580
1240
  </HStack>
581
- <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
582
- {currentMessage?.replies?.totalCount}{' '}
583
- {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
584
- </Text>
585
- <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
586
- {lastReply ? createdAtText(lastReply?.createdAt) : ''}
587
- </Text>
588
- </HStack>
589
- )}
590
- </>
591
- </TouchableHighlight>
592
- )}
593
- {/* <MessageText
594
- {...props}
595
- textStyle={{ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 } }}
596
- /> */}
597
- </>
598
- );
599
- } else {
600
- return (
601
- <TouchableHighlight
602
- underlayColor={'#c0c0c0'}
603
- style={{ width: '100%' }}
604
- onPress={() => {
605
- if (currentMessage?.isShowThreadMessage)
606
- navigation.navigate(config.THREAD_MESSEGE_PATH, {
607
- channelId: channelId,
608
- title: 'Message',
609
- postParentId: currentMessage?._id,
610
- isPostParentIdThread: true,
611
- });
612
- }}
613
- >
614
- <>
615
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
616
- {currentMessage?.replies?.data?.length > 0 && (
617
- <HStack space={'sm'} className="px-1 items-center">
618
- <HStack>
619
- {currentMessage?.replies?.data
620
- ?.filter(
621
- (v: any, i: any, a: any) =>
622
- a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
623
- )
624
- ?.slice(0, 2)
625
- ?.reverse()
626
- ?.map((p: any, i: Number) => (
627
- <Avatar
628
- key={'conversation-replies-key-' + i}
629
- className="bg-transparent"
630
- size={'sm'}
631
- >
632
- <AvatarFallbackText>
633
- {startCase(p?.author?.username?.charAt(0))}
634
- </AvatarFallbackText>
635
- <AvatarImage
636
- alt="user image"
637
- style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
638
- source={{
639
- uri: p?.author?.picture,
640
- }}
641
- />
642
- </Avatar>
643
- ))}
644
- </HStack>
645
- <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
646
- {currentMessage?.replies?.totalCount}{' '}
647
- {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
648
- </Text>
649
- <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
650
- {lastReply ? createdAtText(lastReply?.createdAt) : ''}
651
- </Text>
652
- </HStack>
1241
+ )}
1242
+ </>
1243
+ </TouchableHighlight>
653
1244
  )}
654
1245
  </>
655
- </TouchableHighlight>
656
- );
657
- }
658
- };
1246
+ );
1247
+ } else {
1248
+ return (
1249
+ <TouchableHighlight
1250
+ underlayColor={'#c0c0c0'}
1251
+ style={{ width: '100%' }}
1252
+ onPress={() => {
1253
+ if (currentMessage?.isShowThreadMessage)
1254
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
1255
+ channelId: state?.context?.channelId,
1256
+ title: 'Message',
1257
+ postParentId: currentMessage?._id,
1258
+ isPostParentIdThread: true,
1259
+ });
1260
+ }}
1261
+ >
1262
+ <>
1263
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1264
+ {currentMessage?.replies?.data?.length > 0 && (
1265
+ <HStack space={'sm'} className="px-1 items-center">
1266
+ <HStack>
1267
+ {currentMessage?.replies?.data
1268
+ ?.filter(
1269
+ (v: any, i: any, a: any) =>
1270
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
1271
+ )
1272
+ ?.slice(0, 2)
1273
+ ?.reverse()
1274
+ ?.map((p: any, i: Number) => (
1275
+ <Avatar
1276
+ key={'conversation-replies-key-' + i}
1277
+ className="bg-transparent"
1278
+ size={'sm'}
1279
+ >
1280
+ <AvatarFallbackText>
1281
+ {startCase(p?.author?.username?.charAt(0))}
1282
+ </AvatarFallbackText>
1283
+ <AvatarImage
1284
+ alt="user image"
1285
+ style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
1286
+ source={{
1287
+ uri: p?.author?.picture,
1288
+ }}
1289
+ />
1290
+ </Avatar>
1291
+ ))}
1292
+ </HStack>
1293
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
1294
+ {currentMessage?.replies?.totalCount}{' '}
1295
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
1296
+ </Text>
1297
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
1298
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
1299
+ </Text>
1300
+ </HStack>
1301
+ )}
1302
+ </>
1303
+ </TouchableHighlight>
1304
+ );
1305
+ }
1306
+ },
1307
+ [navigation, state?.context?.channelId, role],
1308
+ );
659
1309
 
660
1310
  const renderActions = (props) => {
661
1311
  return (
662
- <Box>
663
- <Actions
664
- {...props}
665
- icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
666
- />
667
- </Box>
1312
+ <Actions
1313
+ {...props}
1314
+ options={{
1315
+ ['Choose from Library']: onSelectImages,
1316
+ ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1317
+ }}
1318
+ optionTintColor="#000000"
1319
+ cancelButtonIndex={1} // Set the Cancel option as the cancel button
1320
+ icon={() => (
1321
+ <Box
1322
+ style={{
1323
+ width: 32,
1324
+ height: 32,
1325
+ alignItems: 'center',
1326
+ justifyContent: 'center',
1327
+ }}
1328
+ >
1329
+ <Ionicons name="image" size={24} color={colors.blue[500]} />
1330
+ </Box>
1331
+ )}
1332
+ containerStyle={{
1333
+ alignItems: 'center',
1334
+ justifyContent: 'center',
1335
+ marginLeft: 8,
1336
+ marginBottom: 0,
1337
+ }}
1338
+ />
668
1339
  );
669
1340
  };
670
1341
 
671
- const renderAccessory = (props) => {
672
- return (
673
- <Box className="flex-1">
674
- {selectedImage !== '' ? (
675
- <HStack space={'md'} className="px-6 pl-14 items-center">
1342
+ // Create a more visible and reliable image preview with cancel button
1343
+ const renderAccessory = useCallback(
1344
+ (props) => {
1345
+ const selectedImage = safeContextProperty('selectedImage', '');
1346
+
1347
+ if (!selectedImage) {
1348
+ return null;
1349
+ }
1350
+
1351
+ return (
1352
+ <View
1353
+ style={{
1354
+ height: 50,
1355
+ padding: 3,
1356
+ backgroundColor: 'white',
1357
+ borderTopWidth: 1,
1358
+ borderTopColor: '#e0e0e0',
1359
+ flexDirection: 'row',
1360
+ alignItems: 'center',
1361
+ margin: 0,
1362
+ paddingBottom: 0,
1363
+ paddingTop: 5,
1364
+ position: 'absolute',
1365
+ bottom: 0,
1366
+ left: 0,
1367
+ right: 0,
1368
+ zIndex: 999,
1369
+ }}
1370
+ >
1371
+ <View
1372
+ style={{
1373
+ flex: 1,
1374
+ flexDirection: 'row',
1375
+ alignItems: 'center',
1376
+ paddingHorizontal: 15,
1377
+ }}
1378
+ >
676
1379
  <Image
677
- key={selectedImage}
678
- alt={'image'}
679
- source={{ uri: selectedImage }}
1380
+ key={state?.context?.selectedImage}
1381
+ alt={'selected image'}
1382
+ source={{ uri: state?.context?.selectedImage }}
1383
+ style={{
1384
+ width: 36,
1385
+ height: 36,
1386
+ borderRadius: 5,
1387
+ marginRight: 15,
1388
+ }}
680
1389
  size={'xs'}
681
- className="ml-3"
682
1390
  />
683
- <Button
684
- variant={'solid'}
685
- className="bg-gray-500"
686
- size={'xs'}
687
- //colorScheme={'secondary'}
688
- onPress={() => {
689
- setFiles([]);
690
- setImage('');
691
- setImages([]);
1391
+
1392
+ <TouchableHighlight
1393
+ underlayColor="#dddddd"
1394
+ onPress={() => safeSend({ type: ConversationActions.CLEAR_IMAGE })}
1395
+ style={{
1396
+ backgroundColor: '#f44336',
1397
+ paddingVertical: 2,
1398
+ paddingHorizontal: 5,
1399
+ borderRadius: 5,
1400
+ marginLeft: 10,
1401
+ elevation: 3,
1402
+ shadowColor: '#000',
1403
+ shadowOffset: { width: 0, height: 1 },
1404
+ shadowOpacity: 0.3,
1405
+ shadowRadius: 2,
692
1406
  }}
693
1407
  >
694
- <ButtonText className="color-black">Cancel</ButtonText>
695
- </Button>
696
- </HStack>
697
- ) : null}
698
- </Box>
699
- );
700
- };
1408
+ <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1409
+ </TouchableHighlight>
1410
+ </View>
1411
+ </View>
1412
+ );
1413
+ },
1414
+ [state?.context?.selectedImage, safeSend],
1415
+ );
701
1416
 
702
1417
  const setImageViewerObject = (obj: any, v: boolean) => {
703
1418
  setImageObject(obj);
@@ -711,11 +1426,9 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
711
1426
  <CachedImage
712
1427
  style={{ width: '100%', height: '100%' }}
713
1428
  resizeMode={'cover'}
714
- // cacheKey={`${_id}-conversation-modal-image-key`}
715
1429
  cacheKey={`${_id}-slack-bubble-imageKey`}
716
1430
  source={{
717
1431
  uri: image,
718
- //headers: `Authorization: Bearer ${token}`,
719
1432
  expiresIn: 86400,
720
1433
  }}
721
1434
  alt={'image'}
@@ -725,22 +1438,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
725
1438
 
726
1439
  const renderMessage = useCallback(
727
1440
  (props: any) => {
728
- // const {
729
- // currentMessage: { text: currText },
730
- // } = props;
731
-
732
- //let messageTextStyle: any;
733
-
734
- // Make "pure emoji" messages much bigger than plain text.
735
- // if (currText && emojiUtils.isPureEmojiString(currText)) {
736
- // messageTextStyle = {
737
- // fontSize: 28,
738
- // // Emoji get clipped if lineHeight isn't increased; make it consistent across platforms.
739
- // lineHeight: Platform.OS === 'android' ? 34 : 30,
740
- // }
741
- // }
742
-
743
- // return <SlackMessage {...props} messageTextStyle={messageTextStyle} />;
1441
+ // Use memo to prevent unnecessary re-renders of each message
744
1442
  return (
745
1443
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
746
1444
  );
@@ -748,164 +1446,331 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
748
1446
  [isShowImageViewer],
749
1447
  );
750
1448
 
751
- // const renderMessage = (props: any) => {
752
- // // const {
753
- // // currentMessage: { text: currText },
754
- // // } = props;
755
-
756
- // //let messageTextStyle: any;
757
-
758
- // // Make "pure emoji" messages much bigger than plain text.
759
- // // if (currText && emojiUtils.isPureEmojiString(currText)) {
760
- // // messageTextStyle = {
761
- // // fontSize: 28,
762
- // // // Emoji get clipped if lineHeight isn't increased; make it consistent across platforms.
763
- // // lineHeight: Platform.OS === 'android' ? 34 : 30,
764
- // // }
765
- // // }
766
-
767
- // // return <SlackMessage {...props} messageTextStyle={messageTextStyle} />;
768
- // return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
769
- // };
770
-
771
1449
  let onScroll = false;
772
1450
 
1451
+ // Optimize onMomentumScrollBegin for better scroll performance
773
1452
  const onMomentumScrollBegin = async ({ nativeEvent }: any) => {
1453
+ // Set scroll state
774
1454
  onScroll = true;
775
- console.log('scroll top');
776
- if (!loadingOldMessages && isCloseToTop(nativeEvent) && totalCount > channelMessages?.length) {
777
- await onFetchOld();
1455
+
1456
+ // Use the debounced fetch function to prevent excessive calls
1457
+ if (isCloseToTop(nativeEvent)) {
1458
+ onFetchOld();
778
1459
  }
779
1460
  };
780
1461
 
781
1462
  const onEndReached = () => {
782
1463
  console.log('on end reached');
783
1464
  if (!onScroll) return;
784
- // load messages, show ActivityIndicator
785
1465
  onScroll = false;
786
- // setLoadingOldMessages(true);
787
1466
  };
788
1467
 
1468
+ // Add debug logging to help diagnose the issue
1469
+ useEffect(() => {
1470
+ console.log('Current channel ID:', state.context.channelId);
1471
+ console.log('Current state:', state.value);
1472
+ console.log('Channel messages count:', state.context.channelMessages.length);
1473
+ }, [state.context.channelId, state.value, state.context.channelMessages]);
1474
+
1475
+ // Fix the infinite update loop in useEffect monitoring state changes
1476
+ useEffect(() => {
1477
+ // Only trigger effect if we have a specific state to handle
1478
+ // Check if function exists and if we're in a valid state before calling implementation functions
1479
+ if (state && typeof state.matches === 'function') {
1480
+ if (state.matches(BaseState.FetchMessages)) {
1481
+ console.log('In FetchMessages state, attempting to fetch messages');
1482
+ // Use a ref to track if we've already fetched for this state update
1483
+ if (!fetchInProgressRef.current) {
1484
+ fetchInProgressRef.current = true;
1485
+ fetchMessagesDirectly().finally(() => {
1486
+ fetchInProgressRef.current = false;
1487
+ });
1488
+ }
1489
+ } else if (state.matches(MainState.FetchMoreMessages)) {
1490
+ if (!fetchMoreInProgressRef.current) {
1491
+ fetchMoreInProgressRef.current = true;
1492
+ fetchMoreMessagesImpl().then((result) => {
1493
+ if (result.error) {
1494
+ console.error('Error fetching more messages:', result.error);
1495
+ safeSend({ type: 'ERROR', data: { message: result.error } });
1496
+ } else {
1497
+ safeSend({ type: 'FETCH_MORE_MESSAGES_SUCCESS', data: result });
1498
+ }
1499
+ fetchMoreInProgressRef.current = false;
1500
+ });
1501
+ }
1502
+ } else if (state.matches(MainState.SendMessage)) {
1503
+ if (!sendInProgressRef.current) {
1504
+ sendInProgressRef.current = true;
1505
+ sendMessageImpl().then((result) => {
1506
+ if (result.error) {
1507
+ console.error('Error sending message:', result.error);
1508
+ safeSend({ type: 'ERROR', data: { message: result.error } });
1509
+ } else {
1510
+ safeSend({ type: 'SEND_MESSAGE_SUCCESS', data: result });
1511
+ }
1512
+ sendInProgressRef.current = false;
1513
+ });
1514
+ }
1515
+ } else if (state.matches(MainState.SendMessageWithFile)) {
1516
+ if (!sendFileInProgressRef.current) {
1517
+ sendFileInProgressRef.current = true;
1518
+ sendMessageWithFileImpl().then((result) => {
1519
+ if (result.error) {
1520
+ console.error('Error sending message with file:', result.error);
1521
+ safeSend({ type: 'ERROR', data: { message: result.error } });
1522
+ } else {
1523
+ safeSend({ type: 'SEND_MESSAGE_WITH_FILE_SUCCESS', data: result });
1524
+ }
1525
+ sendFileInProgressRef.current = false;
1526
+ });
1527
+ }
1528
+ } else if (state.matches(MainState.CreateDirectChannel)) {
1529
+ if (!createChannelInProgressRef.current) {
1530
+ createChannelInProgressRef.current = true;
1531
+ createDirectChannelImpl().then((result) => {
1532
+ if (result.error) {
1533
+ console.error('Error creating direct channel:', result.error);
1534
+ safeSend({ type: 'ERROR', data: { message: result.error } });
1535
+ } else {
1536
+ safeSend({ type: 'CREATE_DIRECT_CHANNEL_SUCCESS', data: result });
1537
+ }
1538
+ createChannelInProgressRef.current = false;
1539
+ });
1540
+ }
1541
+ }
1542
+ }
1543
+ }, [
1544
+ state?.value,
1545
+ fetchMessagesDirectly,
1546
+ fetchMoreMessagesImpl,
1547
+ sendMessageImpl,
1548
+ sendMessageWithFileImpl,
1549
+ createDirectChannelImpl,
1550
+ safeSend,
1551
+ ]);
1552
+
1553
+ // Add refs to prevent duplicate operations
1554
+ const fetchInProgressRef = useRef(false);
1555
+ const fetchMoreInProgressRef = useRef(false);
1556
+ const sendInProgressRef = useRef(false);
1557
+ const sendFileInProgressRef = useRef(false);
1558
+ const createChannelInProgressRef = useRef(false);
1559
+
1560
+ // Fix subscription handler to prevent infinite updates
1561
+ const renderChatFooter = useCallback(() => {
1562
+ return (
1563
+ <>
1564
+ <ImageViewerModal
1565
+ isVisible={isShowImageViewer}
1566
+ setVisible={setImageViewer}
1567
+ modalContent={modalContent}
1568
+ />
1569
+ <SubscriptionHandler
1570
+ channelId={state?.context?.channelId?.toString()}
1571
+ subscribeToNewMessages={() =>
1572
+ subscribeToMore({
1573
+ document: CHAT_MESSAGE_ADDED,
1574
+ variables: {
1575
+ channelId: state?.context?.channelId?.toString(),
1576
+ },
1577
+ updateQuery: (prev, { subscriptionData }: any) => {
1578
+ if (!subscriptionData?.data?.chatMessageAdded) return prev;
1579
+
1580
+ const newMessage = subscriptionData.data.chatMessageAdded;
1581
+ const currentMessages = prev?.messages?.data || [];
1582
+
1583
+ // Check if message already exists to prevent duplicates
1584
+ if (currentMessages.some((msg) => msg.id === newMessage.id)) {
1585
+ return prev; // Skip update if message already exists
1586
+ }
1587
+
1588
+ // Use a ref to track the last processed message ID to prevent duplicate processing
1589
+ if (lastProcessedMessageRef.current === newMessage.id) {
1590
+ return prev;
1591
+ }
1592
+
1593
+ lastProcessedMessageRef.current = newMessage.id;
1594
+
1595
+ // Send update to state machine using a timeout to break the render cycle
1596
+ setTimeout(() => {
1597
+ safeSend({
1598
+ type: ConversationActions.SET_CHANNEL_MESSAGES,
1599
+ data: {
1600
+ messages: uniqBy(
1601
+ [...state.context.channelMessages, newMessage],
1602
+ ({ id }) => id,
1603
+ ),
1604
+ totalCount: (prev?.messages?.totalCount || 0) + 1,
1605
+ },
1606
+ });
1607
+ }, 0);
1608
+
1609
+ return {
1610
+ ...prev,
1611
+ messages: {
1612
+ ...prev?.messages,
1613
+ data: [...currentMessages, newMessage],
1614
+ totalCount: (prev?.messages?.totalCount || 0) + 1,
1615
+ },
1616
+ };
1617
+ },
1618
+ })
1619
+ }
1620
+ />
1621
+ </>
1622
+ );
1623
+ }, [
1624
+ isShowImageViewer,
1625
+ modalContent,
1626
+ state?.context?.channelId,
1627
+ state?.context?.channelMessages,
1628
+ subscribeToMore,
1629
+ safeSend,
1630
+ ]);
1631
+
1632
+ // Add ref to track last processed message
1633
+ const lastProcessedMessageRef = useRef(null);
1634
+
1635
+ // Add optimized listViewProps to reduce re-renders
1636
+ const listViewProps = useMemo(
1637
+ () => ({
1638
+ onEndReached: onEndReached,
1639
+ onEndReachedThreshold: 0.5,
1640
+ onMomentumScrollBegin: onMomentumScrollBegin,
1641
+ removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1642
+ initialNumToRender: 10, // Reduce initial render amount
1643
+ maxToRenderPerBatch: 10, // Reduce number in each render batch
1644
+ windowSize: 10, // Reduce the window size
1645
+ }),
1646
+ [onEndReached, onMomentumScrollBegin],
1647
+ );
1648
+
1649
+ // Add a loader for when more messages are being loaded
1650
+ const renderLoadEarlier = useCallback(() => {
1651
+ return state?.context?.loadingOldMessages ? (
1652
+ <View
1653
+ style={{
1654
+ padding: 10,
1655
+ backgroundColor: 'rgba(255,255,255,0.8)',
1656
+ borderRadius: 10,
1657
+ marginTop: 10,
1658
+ }}
1659
+ >
1660
+ <Spinner size="small" color="#3b82f6" />
1661
+ </View>
1662
+ ) : null;
1663
+ }, [state?.context?.loadingOldMessages]);
1664
+
1665
+ // Add renderInputToolbar function
1666
+ const renderInputToolbar = useCallback((props) => {
1667
+ return (
1668
+ <InputToolbar
1669
+ {...props}
1670
+ containerStyle={{
1671
+ backgroundColor: 'white',
1672
+ borderTopWidth: 1,
1673
+ borderTopColor: colors.gray[200],
1674
+ paddingHorizontal: 4,
1675
+ paddingVertical: 0,
1676
+ paddingTop: 2,
1677
+ marginBottom: 0,
1678
+ marginTop: 0,
1679
+ }}
1680
+ primaryStyle={{
1681
+ alignItems: 'center',
1682
+ }}
1683
+ />
1684
+ );
1685
+ }, []);
1686
+
1687
+ // Return optimized component with performance improvements
789
1688
  return (
790
- <SafeAreaView style={{ flex: 1 }}>
791
- {loadEarlierMsg && <Spinner color={'#3b82f6'} />}
1689
+ <View
1690
+ style={{
1691
+ flex: 1,
1692
+ backgroundColor: 'white',
1693
+ }}
1694
+ >
1695
+ {state?.matches && state.matches(BaseState.FetchMessages) && <Spinner color={'#3b82f6'} />}
792
1696
 
793
1697
  <GiftedChat
794
1698
  ref={messageRootListRef}
795
- wrapInSafeArea={false}
1699
+ wrapInSafeArea={true}
796
1700
  renderLoading={() => <Spinner color={'#3b82f6'} />}
797
1701
  messages={messageList}
798
1702
  listViewProps={{
799
- onEndReached: onEndReached,
800
- onEndReachedThreshold: 0.5,
801
- onMomentumScrollBegin: onMomentumScrollBegin,
1703
+ ...listViewProps,
1704
+ contentContainerStyle: {
1705
+ paddingBottom: 10,
1706
+ },
1707
+ keyboardShouldPersistTaps: 'handled',
802
1708
  }}
803
- // listViewProps={{
804
- // scrollEventThrottle: 400,
805
- // onScroll: ({ nativeEvent }) => { console.log('scroll')
806
- // if (!loadingOldMessages && isCloseToTop(nativeEvent)) {
807
- // onFetchOld();
808
- // }
809
- // }
810
- // }}
811
- onSend={(messages) =>
812
- rest?.isCreateNewChannel && !channelId
813
- ? rest?.newChannelData?.type === RoomType?.Direct
814
- ? createDirectChannel(messages[0]?.text ?? ' ')
815
- : null
816
- : channelId && handleSend(messages[0]?.text ?? ' ')
1709
+ onSend={handleSend}
1710
+ text={safeContextProperty('messageText', ' ') || ' '}
1711
+ onInputTextChanged={(text) =>
1712
+ safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText: text } })
817
1713
  }
818
- text={msg ? msg : ' '}
819
- onInputTextChanged={(text) => setMsg(text)}
820
1714
  renderFooter={() =>
821
- loading ? <Spinner color={'#3b82f6'} /> : imageLoading ? <Spinner color={'#3b82f6'} /> : ''
1715
+ safeContextProperty('loading') ? (
1716
+ <Spinner color={'#3b82f6'} />
1717
+ ) : safeContextProperty('imageLoading') ? (
1718
+ <Spinner color={'#3b82f6'} />
1719
+ ) : (
1720
+ ''
1721
+ )
822
1722
  }
823
1723
  scrollToBottom
824
1724
  user={{
825
- // _id: currentUser?.id || '',
826
1725
  _id: auth?.id || '',
827
1726
  }}
828
- isTyping={true}
829
- alwaysShowSend={loading ? false : true}
830
- //onLoadEarlier={onFetchOld}
831
- //infiniteScroll={true}
1727
+ isTyping={false} // Setting to false to reduce animations
1728
+ alwaysShowSend={true} // Always show send button regardless of text content
832
1729
  renderSend={renderSend}
833
- // loadEarlier={data?.messages?.totalCount > channelMessages.length}
834
- //isLoadingEarlier={loadEarlierMsg}
835
- //extraData={{ isLoadingEarlier: loadingOldMessages }}
836
- // renderLoadEarlier={() =>
837
- // !loadEarlierMsg && (
838
- // <Center py={2}>
839
- // <Button
840
- // onPress={() => onFetchOld()}
841
- // variant={'outline'}
842
- // _text={{ color: 'black', fontSize: 15, fontWeight: 'bold' }}
843
- // >
844
- // Load earlier messages
845
- // </Button>
846
- // </Center>
847
- // )
848
- // }
849
1730
  renderMessageText={renderMessageText}
1731
+ renderInputToolbar={renderInputToolbar}
850
1732
  minInputToolbarHeight={50}
851
- renderActions={channelId && renderActions}
852
- renderAccessory={renderAccessory}
1733
+ renderActions={safeContextProperty('channelId') && renderActions}
1734
+ renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
853
1735
  renderMessage={renderMessage}
854
- renderChatFooter={() => (
855
- <>
856
- <ImageViewerModal
857
- isVisible={isShowImageViewer}
858
- setVisible={setImageViewer}
859
- modalContent={modalContent}
860
- />
861
- <SubscriptionHandler
862
- channelId={channelId?.toString()}
863
- subscribeToNewMessages={() =>
864
- subscribeToMore({
865
- document: CHAT_MESSAGE_ADDED,
866
- variables: {
867
- channelId: channelId?.toString(),
868
- },
869
- updateQuery: (prev, { subscriptionData }: any) => {
870
- if (!subscriptionData.data) return prev;
871
- setSkip(0);
872
- const newMessage: any = subscriptionData?.data?.chatMessageAdded;
873
- const previousData = prev?.messages?.data
874
- ? [...prev.messages.data, newMessage]
875
- : [];
876
- const totalMsgCount = prev?.messages?.totalCount + 1;
877
- setChannelMessages((oldMessages: any) =>
878
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
879
- );
880
- setTotalCount(totalMsgCount);
881
- const merged = {
882
- ...prev,
883
- messages: {
884
- ...prev?.messages,
885
- data: [...(prev?.messages?.data ?? []), newMessage],
886
- totalCount: totalMsgCount,
887
- },
888
- };
889
- return merged;
890
- // return Object.assign({}, prev, {
891
- // messages: {
892
- // data: [...prev.messages.data, newMessage],
893
- // totalCount: prev.messages.totalCount + 1,
894
- // },
895
- // });
896
- },
897
- })
898
- }
899
- />
900
- </>
901
- )}
1736
+ renderChatFooter={renderChatFooter}
1737
+ renderLoadEarlier={renderLoadEarlier}
1738
+ loadEarlier={state?.context?.totalCount > state?.context?.channelMessages?.length}
1739
+ isLoadingEarlier={state?.context?.loadingOldMessages}
1740
+ bottomOffset={Platform.OS === 'ios' ? 10 : 0} // Reduce bottom offset
1741
+ textInputProps={{
1742
+ style: {
1743
+ borderWidth: 1,
1744
+ borderColor: colors.gray[300],
1745
+ backgroundColor: '#f8f8f8',
1746
+ borderRadius: 20,
1747
+ minHeight: 36,
1748
+ maxHeight: 80,
1749
+ color: '#000',
1750
+ padding: 8,
1751
+ paddingHorizontal: 15,
1752
+ fontSize: 16,
1753
+ flex: 1,
1754
+ marginVertical: 2,
1755
+ marginBottom: 0,
1756
+ },
1757
+ multiline: true,
1758
+ returnKeyType: 'default',
1759
+ enablesReturnKeyAutomatically: true,
1760
+ placeholderTextColor: colors.gray[400],
1761
+ }}
1762
+ minComposerHeight={36}
1763
+ maxComposerHeight={100}
1764
+ isKeyboardInternallyHandled={true}
1765
+ placeholder="Type a message..."
902
1766
  lightboxProps={{
903
1767
  underlayColor: 'transparent',
904
1768
  springConfig: { tension: 90000, friction: 90000 },
905
1769
  disabled: true,
906
1770
  }}
1771
+ infiniteScroll={false} // Disable automatic loading
907
1772
  />
908
- </SafeAreaView>
1773
+ </View>
909
1774
  );
910
1775
  };
911
1776