@messenger-box/platform-mobile 10.0.3-alpha.16 → 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 (31) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/routes.json +14 -1
  3. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  4. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  5. package/lib/screens/inbox/components/CachedImage/index.js +125 -16
  6. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  7. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +32 -21
  8. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  9. package/lib/screens/inbox/containers/ConversationView.js +1175 -400
  10. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  11. package/lib/screens/inbox/containers/Dialogs.js +290 -21
  12. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  13. package/lib/screens/inbox/containers/ThreadConversationView.js +858 -351
  14. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  15. package/lib/screens/inbox/containers/workflow/conversation-xstate.js +380 -0
  16. package/lib/screens/inbox/containers/workflow/conversation-xstate.js.map +1 -0
  17. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +235 -0
  18. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +1 -0
  19. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js +438 -0
  20. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js.map +1 -0
  21. package/package.json +4 -4
  22. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  23. package/src/screens/inbox/components/CachedImage/index.tsx +137 -17
  24. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +35 -9
  25. package/src/screens/inbox/containers/ConversationView.tsx +1510 -641
  26. package/src/screens/inbox/containers/Dialogs.tsx +415 -123
  27. package/src/screens/inbox/containers/ThreadConversationView.tsx +1053 -288
  28. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  29. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  30. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  31. 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 } 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);
845
+ console.log('📩 QUERY DATA CHANGED - Messages received:', data.messages.data.length);
846
+ const { data: messages, totalCount: messageTotalCount } = data.messages;
189
847
 
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
- }
848
+ if (messages && messages.length > 0) {
849
+ console.log('📩 QUERY DATA - Setting channel messages, count:', messages.length);
197
850
 
198
- if (loadingOldMessages && channelMessages) setLoadingOldMessages(false);
199
- }
200
- }, [data, loadingOldMessages, channelMessages, totalCount]);
201
-
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,454 +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: any = await ImagePicker.launchImageLibraryAsync({
252
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
253
- allowsEditing: true,
254
- aspect: [4, 3],
255
- quality: 1,
256
- base64: true,
257
- });
258
- // if (!imageSource.cancelled) {
259
- // const image = 'data:image/jpeg;base64,' + imageSource?.base64;
260
- // setImage(image);
261
- // const file = dataURLtoFile(image, 'inputImage.jpg');
262
- // setFiles((files) => files.concat(file));
263
- // setImages((images) => images.concat(imageSource as ImagePicker.ImageInfo));
264
- // }
265
- // if (imageSource.cancelled) setLoading(false);
266
-
267
- if (!imageSource.canceled) {
268
- const image = 'data:image/jpeg;base64,' + imageSource?.assets[0]?.base64;
269
- setImage(image);
270
- const file = dataURLtoFile(image, 'inputImage.jpg');
271
- setFiles((files) => files.concat(file));
272
- setImages((images) => images.concat(imageSource?.assets[0] as ImagePicker.ImagePickerAsset));
273
- }
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;
274
936
 
275
- if (imageSource.canceled) setLoading(false);
276
- };
277
-
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
- if (!channelId) return;
328
- 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);
329
971
 
330
- const notificationData: IExpoNotificationData = {
331
- url: config.INBOX_MESSEGE_PATH,
332
- params: { channelId, hideTabBar: true },
333
- screen: 'DialogMessages',
334
- other: { sound: Platform.OS === 'android' ? undefined : 'default' },
335
- };
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
+ }
336
977
 
337
- if (images && images.length > 0) {
338
- 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;
339
981
 
340
- setLoading(true);
341
- const uploadResponse = await startUpload({
342
- file: images,
343
- saveUploadedFile: {
344
- variables: {
345
- postId,
346
- },
347
- },
348
- createUploadLink: {
349
- variables: {
350
- postId,
351
- },
352
- },
353
- });
354
- if (uploadResponse?.error) setLoading(false);
355
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
356
-
357
- if (uploadResponse.data) {
358
- setImage('');
359
- setFiles([]);
360
- setImages([]);
361
- //setLoading(false);
362
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
363
- await sendMsg({
364
- variables: {
365
- postId,
366
- channelId,
367
- content: message,
368
- files,
369
- notificationParams: notificationData,
370
- },
371
- update: (cache, { data, errors }: any) => {
372
- if (!data || errors) {
373
- setLoading(false);
374
- return;
375
- }
376
- //Temporary fix.....//
377
- const newMessage: any = data?.sendMessage;
378
- setChannelMessages((oldMessages: any) =>
379
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
380
- );
381
- setTotalCount((t) => t + 1);
382
- //Temporary fix.....//
383
-
384
- setChannelToTop(channelToTop + 1);
385
- setLoading(false);
386
- setMsg('');
387
- },
388
- });
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 });
389
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 });
390
1002
  } else {
391
- setLoading(true);
392
- await sendMsg({
393
- variables: {
394
- channelId,
395
- content: message,
396
- notificationParams: notificationData,
397
- },
398
- update: (cache, { data, errors }: any) => {
399
- if (!data || errors) {
400
- setLoading(false);
401
- return;
402
- }
403
- //Temporary fix.....//
404
- const newMessage: any = data?.sendMessage;
405
- setChannelMessages((oldMessages: any) => uniqBy([...oldMessages, newMessage], ({ id }) => id));
406
- setTotalCount((t) => t + 1);
407
- //Temporary fix.....//
408
-
409
- setChannelToTop(channelToTop + 1);
410
- setLoading(false);
411
- setMsg('');
412
- },
413
- });
1003
+ console.log('Sending text-only message');
1004
+ safeSend({ type: ConversationActions.SEND_MESSAGE });
414
1005
  }
415
1006
  },
416
- [setChannelMessages, channelId, images, channelToTop, expoTokens],
1007
+ [state.context.channelId, state.context.images, rest?.isCreateNewChannel, rest?.newChannelData?.type, safeSend],
417
1008
  );
418
1009
 
419
- const messageList = useMemo(() => {
420
- let currentDate = '';
421
- let res: any = [];
422
- const filteredMessages =
423
- channelMessages && channelMessages?.length > 0 ? uniqBy([...channelMessages], ({ id }: any) => id) : [];
424
- if (channelId && filteredMessages?.length > 0) {
425
- orderBy(channelMessages, ['createdAt'], ['desc']).map((msg) => {
426
- let message: IMessageProps = {
427
- _id: '',
428
- text: '',
429
- createdAt: 0,
430
- user: {
431
- _id: '',
432
- name: '',
433
- 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,
434
1034
  },
435
- type: '',
436
- };
437
- const date = new Date(msg.createdAt);
438
- message._id = msg.id;
439
- message.text = msg.message;
440
- message.createdAt = date;
441
- (message.user = {
442
- _id: msg.author.id,
443
- name: msg.author.givenName + ' ' + msg.author.familyName,
444
- 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',
445
1078
  }),
446
- (message.image = msg.files?.data[0]?.url),
447
- (message.sent = msg?.isDelivered),
448
- (message.received = msg?.isRead);
449
- message.type = msg?.type;
450
- message.propsConfiguration = msg?.propsConfiguration;
451
- message.replies = msg?.replies ?? [];
452
- message.isShowThreadMessage = isShowThreadMessage;
453
- res.push(message);
454
- });
1079
+ );
455
1080
  }
456
- return res?.length > 0 ? uniqBy([...res], ({ _id }: any) => _id) : [];
457
- //return res;
458
- }, [channelMessages, channelId]);
459
1081
 
460
- const renderSend = (props) => {
461
- return (
462
- <Send {...props} disabled={channelId || rest?.isCreateNewChannel ? false : true}>
463
- <Box>
464
- <MaterialCommunityIcons
465
- name="send-circle"
466
- style={{ marginBottom: 5, marginRight: 5 }}
467
- size={32}
468
- color={channelId || rest?.isCreateNewChannel ? '#2e64e5' : '#b8b2b2'}
469
- />
470
- </Box>
471
- </Send>
472
- );
473
- };
1082
+ // Use a more efficient approach - pre-filter messages once
1083
+ const filteredMessages = uniqBy(state.context.channelMessages, ({ id }) => id);
474
1084
 
475
- const renderMessageText = (props: any) => {
476
- const { currentMessage } = props;
477
- const lastReply: any = currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
478
-
479
- if (currentMessage.type === 'ALERT') {
480
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
481
- let action: string = '';
482
- let actionId: any = '';
483
- let params: any = {};
484
-
485
- if (attachment?.callToAction?.extraParams) {
486
- const extraParams: any = attachment?.callToAction?.extraParams;
487
- const route: any = extraParams?.route ?? null;
488
- let path: any = null;
489
- let param: any = null;
490
- if (role && role == PreDefinedRole.Guest) {
491
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
492
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
493
- } else if (role && role == PreDefinedRole.Owner) {
494
- path = route?.host?.name ? route?.host?.name ?? null : null;
495
- param = route?.host?.params ? route?.host?.params ?? null : null;
496
- } else {
497
- path = route?.host?.name ? route?.host?.name ?? null : null;
498
- param = route?.host?.params ? route?.host?.params ?? null : null;
499
- }
1085
+ // Skip processing if no filtered messages
1086
+ if (filteredMessages.length === 0) {
1087
+ return [];
1088
+ }
1089
+
1090
+ // Transform messages only once and return
1091
+ return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
1092
+ const date = new Date(msg.createdAt);
500
1093
 
501
- action = path;
502
- params = { ...param };
503
- } else if (attachment?.callToAction?.link) {
504
- action = CALL_TO_ACTION_PATH;
505
- actionId = attachment?.callToAction?.link.split('/').pop();
506
- params = { reservationId: actionId };
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
+ }
507
1102
  }
508
1103
 
509
- return (
510
- <>
511
- {attachment?.callToAction && action ? (
512
- <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
513
- <Button
514
- variant={'outline'}
515
- size={'sm'}
516
- className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
517
- onPress={() => action && params && navigation.navigate(action, params)}
518
- // onPress={() => navigation.navigate(action, { reservationId: actionId })}
519
- >
520
- <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
521
- {attachment.callToAction.title}
522
- </ButtonText>
523
- </Button>
524
- <MessageText
525
- {...props}
526
- textStyle={{
527
- 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
+ });
528
1195
  }}
529
- />
530
- </Box>
531
- ) : (
532
- <TouchableHighlight
533
- underlayColor={'#c0c0c0'}
534
- style={{ width: '100%' }}
535
- onPress={() => {
536
- if (currentMessage?.isShowThreadMessage)
537
- navigation.navigate(config.THREAD_MESSEGE_PATH, {
538
- channelId: channelId,
539
- title: 'Message',
540
- postParentId: currentMessage?._id,
541
- isPostParentIdThread: true,
542
- });
543
- }}
544
- >
545
- <>
546
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
547
- {currentMessage?.replies?.data?.length > 0 && (
548
- <HStack space={'sm'} className="px-1 items-center">
549
- <HStack>
550
- {currentMessage?.replies?.data
551
- ?.filter(
552
- (v: any, i: any, a: any) =>
553
- a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
554
- )
555
- ?.slice(0, 2)
556
- ?.reverse()
557
- ?.map((p: any, i: Number) => (
558
- <Avatar
559
- key={'conversations-view-key-' + i}
560
- size={'sm'}
561
- className="bg-transparent"
562
- >
563
- <AvatarFallbackText>
564
- {startCase(p?.author?.username?.charAt(0))}
565
- </AvatarFallbackText>
566
- <AvatarImage
567
- alt="user image"
568
- style={{
569
- borderRadius: 6,
570
- borderWidth: 2,
571
- borderColor: '#fff',
572
- }}
573
- source={{
574
- uri: p?.author?.picture,
575
- }}
576
- />
577
- </Avatar>
578
- ))}
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>
579
1240
  </HStack>
580
- <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
581
- {currentMessage?.replies?.totalCount}{' '}
582
- {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
583
- </Text>
584
- <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
585
- {lastReply ? createdAtText(lastReply?.createdAt) : ''}
586
- </Text>
587
- </HStack>
588
- )}
589
- </>
590
- </TouchableHighlight>
591
- )}
592
- {/* <MessageText
593
- {...props}
594
- textStyle={{ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 } }}
595
- /> */}
596
- </>
597
- );
598
- } else {
599
- return (
600
- <TouchableHighlight
601
- underlayColor={'#c0c0c0'}
602
- style={{ width: '100%' }}
603
- onPress={() => {
604
- if (currentMessage?.isShowThreadMessage)
605
- navigation.navigate(config.THREAD_MESSEGE_PATH, {
606
- channelId: channelId,
607
- title: 'Message',
608
- postParentId: currentMessage?._id,
609
- isPostParentIdThread: true,
610
- });
611
- }}
612
- >
613
- <>
614
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
615
- {currentMessage?.replies?.data?.length > 0 && (
616
- <HStack space={'sm'} className="px-1 items-center">
617
- <HStack>
618
- {currentMessage?.replies?.data
619
- ?.filter(
620
- (v: any, i: any, a: any) =>
621
- a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
622
- )
623
- ?.slice(0, 2)
624
- ?.reverse()
625
- ?.map((p: any, i: Number) => (
626
- <Avatar
627
- key={'conversation-replies-key-' + i}
628
- className="bg-transparent"
629
- size={'sm'}
630
- >
631
- <AvatarFallbackText>
632
- {startCase(p?.author?.username?.charAt(0))}
633
- </AvatarFallbackText>
634
- <AvatarImage
635
- alt="user image"
636
- style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
637
- source={{
638
- uri: p?.author?.picture,
639
- }}
640
- />
641
- </Avatar>
642
- ))}
643
- </HStack>
644
- <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
645
- {currentMessage?.replies?.totalCount}{' '}
646
- {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
647
- </Text>
648
- <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
649
- {lastReply ? createdAtText(lastReply?.createdAt) : ''}
650
- </Text>
651
- </HStack>
1241
+ )}
1242
+ </>
1243
+ </TouchableHighlight>
652
1244
  )}
653
1245
  </>
654
- </TouchableHighlight>
655
- );
656
- }
657
- };
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
+ );
658
1309
 
659
1310
  const renderActions = (props) => {
660
1311
  return (
661
1312
  <Actions
662
1313
  {...props}
663
- icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
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
+ }}
664
1338
  />
665
1339
  );
666
1340
  };
667
1341
 
668
- const renderAccessory = (props) => {
669
- return (
670
- <Box>
671
- {selectedImage !== '' ? (
672
- <HStack className="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
+ >
673
1379
  <Image
674
- key={selectedImage}
675
- alt={'image'}
676
- 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
+ }}
677
1389
  size={'xs'}
678
- className="ml-3"
679
1390
  />
680
- <Button
681
- variant={'solid'}
682
- className="bg-transparent"
683
- //colorScheme={'secondary'}
684
- onPress={() => {
685
- setFiles([]);
686
- setImage('');
687
- 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,
688
1406
  }}
689
1407
  >
690
- <ButtonText className="color-black">Cancel</ButtonText>
691
- </Button>
692
- </HStack>
693
- ) : null}
694
- </Box>
695
- );
696
- };
1408
+ <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1409
+ </TouchableHighlight>
1410
+ </View>
1411
+ </View>
1412
+ );
1413
+ },
1414
+ [state?.context?.selectedImage, safeSend],
1415
+ );
697
1416
 
698
1417
  const setImageViewerObject = (obj: any, v: boolean) => {
699
1418
  setImageObject(obj);
@@ -707,11 +1426,9 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
707
1426
  <CachedImage
708
1427
  style={{ width: '100%', height: '100%' }}
709
1428
  resizeMode={'cover'}
710
- // cacheKey={`${_id}-conversation-modal-image-key`}
711
1429
  cacheKey={`${_id}-slack-bubble-imageKey`}
712
1430
  source={{
713
1431
  uri: image,
714
- //headers: `Authorization: Bearer ${token}`,
715
1432
  expiresIn: 86400,
716
1433
  }}
717
1434
  alt={'image'}
@@ -721,22 +1438,7 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
721
1438
 
722
1439
  const renderMessage = useCallback(
723
1440
  (props: any) => {
724
- // const {
725
- // currentMessage: { text: currText },
726
- // } = props;
727
-
728
- //let messageTextStyle: any;
729
-
730
- // Make "pure emoji" messages much bigger than plain text.
731
- // if (currText && emojiUtils.isPureEmojiString(currText)) {
732
- // messageTextStyle = {
733
- // fontSize: 28,
734
- // // Emoji get clipped if lineHeight isn't increased; make it consistent across platforms.
735
- // lineHeight: Platform.OS === 'android' ? 34 : 30,
736
- // }
737
- // }
738
-
739
- // return <SlackMessage {...props} messageTextStyle={messageTextStyle} />;
1441
+ // Use memo to prevent unnecessary re-renders of each message
740
1442
  return (
741
1443
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
742
1444
  );
@@ -744,164 +1446,331 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
744
1446
  [isShowImageViewer],
745
1447
  );
746
1448
 
747
- // const renderMessage = (props: any) => {
748
- // // const {
749
- // // currentMessage: { text: currText },
750
- // // } = props;
751
-
752
- // //let messageTextStyle: any;
753
-
754
- // // Make "pure emoji" messages much bigger than plain text.
755
- // // if (currText && emojiUtils.isPureEmojiString(currText)) {
756
- // // messageTextStyle = {
757
- // // fontSize: 28,
758
- // // // Emoji get clipped if lineHeight isn't increased; make it consistent across platforms.
759
- // // lineHeight: Platform.OS === 'android' ? 34 : 30,
760
- // // }
761
- // // }
762
-
763
- // // return <SlackMessage {...props} messageTextStyle={messageTextStyle} />;
764
- // return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
765
- // };
766
-
767
1449
  let onScroll = false;
768
1450
 
1451
+ // Optimize onMomentumScrollBegin for better scroll performance
769
1452
  const onMomentumScrollBegin = async ({ nativeEvent }: any) => {
1453
+ // Set scroll state
770
1454
  onScroll = true;
771
- console.log('scroll top');
772
- if (!loadingOldMessages && isCloseToTop(nativeEvent) && totalCount > channelMessages?.length) {
773
- await onFetchOld();
1455
+
1456
+ // Use the debounced fetch function to prevent excessive calls
1457
+ if (isCloseToTop(nativeEvent)) {
1458
+ onFetchOld();
774
1459
  }
775
1460
  };
776
1461
 
777
1462
  const onEndReached = () => {
778
1463
  console.log('on end reached');
779
1464
  if (!onScroll) return;
780
- // load messages, show ActivityIndicator
781
1465
  onScroll = false;
782
- // setLoadingOldMessages(true);
783
1466
  };
784
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
785
1688
  return (
786
- <>
787
- {loadEarlierMsg && <Spinner color={'$blue500'} />}
1689
+ <View
1690
+ style={{
1691
+ flex: 1,
1692
+ backgroundColor: 'white',
1693
+ }}
1694
+ >
1695
+ {state?.matches && state.matches(BaseState.FetchMessages) && <Spinner color={'#3b82f6'} />}
788
1696
 
789
1697
  <GiftedChat
790
1698
  ref={messageRootListRef}
791
- wrapInSafeArea={false}
792
- renderLoading={() => <Spinner color={'$blue500'} />}
1699
+ wrapInSafeArea={true}
1700
+ renderLoading={() => <Spinner color={'#3b82f6'} />}
793
1701
  messages={messageList}
794
1702
  listViewProps={{
795
- onEndReached: onEndReached,
796
- onEndReachedThreshold: 0.5,
797
- onMomentumScrollBegin: onMomentumScrollBegin,
1703
+ ...listViewProps,
1704
+ contentContainerStyle: {
1705
+ paddingBottom: 10,
1706
+ },
1707
+ keyboardShouldPersistTaps: 'handled',
798
1708
  }}
799
- // listViewProps={{
800
- // scrollEventThrottle: 400,
801
- // onScroll: ({ nativeEvent }) => { console.log('scroll')
802
- // if (!loadingOldMessages && isCloseToTop(nativeEvent)) {
803
- // onFetchOld();
804
- // }
805
- // }
806
- // }}
807
- onSend={(messages) =>
808
- rest?.isCreateNewChannel && !channelId
809
- ? rest?.newChannelData?.type === RoomType?.Direct
810
- ? createDirectChannel(messages[0]?.text ?? ' ')
811
- : null
812
- : 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 } })
813
1713
  }
814
- text={msg ? msg : ' '}
815
- onInputTextChanged={(text) => setMsg(text)}
816
1714
  renderFooter={() =>
817
- loading ? <Spinner color={'$blue500'} /> : imageLoading ? <Spinner color={'$blue500'} /> : ''
1715
+ safeContextProperty('loading') ? (
1716
+ <Spinner color={'#3b82f6'} />
1717
+ ) : safeContextProperty('imageLoading') ? (
1718
+ <Spinner color={'#3b82f6'} />
1719
+ ) : (
1720
+ ''
1721
+ )
818
1722
  }
819
1723
  scrollToBottom
820
1724
  user={{
821
- // _id: currentUser?.id || '',
822
1725
  _id: auth?.id || '',
823
1726
  }}
824
- isTyping={true}
825
- alwaysShowSend={loading ? false : true}
826
- //onLoadEarlier={onFetchOld}
827
- //infiniteScroll={true}
1727
+ isTyping={false} // Setting to false to reduce animations
1728
+ alwaysShowSend={true} // Always show send button regardless of text content
828
1729
  renderSend={renderSend}
829
- // loadEarlier={data?.messages?.totalCount > channelMessages.length}
830
- //isLoadingEarlier={loadEarlierMsg}
831
- //extraData={{ isLoadingEarlier: loadingOldMessages }}
832
- // renderLoadEarlier={() =>
833
- // !loadEarlierMsg && (
834
- // <Center py={2}>
835
- // <Button
836
- // onPress={() => onFetchOld()}
837
- // variant={'outline'}
838
- // _text={{ color: 'black', fontSize: 15, fontWeight: 'bold' }}
839
- // >
840
- // Load earlier messages
841
- // </Button>
842
- // </Center>
843
- // )
844
- // }
845
1730
  renderMessageText={renderMessageText}
1731
+ renderInputToolbar={renderInputToolbar}
846
1732
  minInputToolbarHeight={50}
847
- renderActions={channelId && renderActions}
848
- renderAccessory={renderAccessory}
1733
+ renderActions={safeContextProperty('channelId') && renderActions}
1734
+ renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
849
1735
  renderMessage={renderMessage}
850
- renderChatFooter={() => (
851
- <>
852
- <ImageViewerModal
853
- isVisible={isShowImageViewer}
854
- setVisible={setImageViewer}
855
- modalContent={modalContent}
856
- />
857
- <SubscriptionHandler
858
- channelId={channelId?.toString()}
859
- subscribeToNewMessages={() =>
860
- subscribeToMore({
861
- document: CHAT_MESSAGE_ADDED,
862
- variables: {
863
- channelId: channelId?.toString(),
864
- },
865
- updateQuery: (prev, { subscriptionData }: any) => {
866
- if (!subscriptionData.data) return prev;
867
- setSkip(0);
868
- const newMessage: any = subscriptionData?.data?.chatMessageAdded;
869
- const previousData = prev?.messages?.data
870
- ? [...prev.messages.data, newMessage]
871
- : [];
872
- const totalMsgCount = prev?.messages?.totalCount + 1;
873
- setChannelMessages((oldMessages: any) =>
874
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
875
- );
876
- setTotalCount(totalMsgCount);
877
- const merged = {
878
- ...prev,
879
- messages: {
880
- ...prev?.messages,
881
- data: [...(prev?.messages?.data ?? []), newMessage],
882
- totalCount: totalMsgCount,
883
- },
884
- };
885
- return merged;
886
- // return Object.assign({}, prev, {
887
- // messages: {
888
- // data: [...prev.messages.data, newMessage],
889
- // totalCount: prev.messages.totalCount + 1,
890
- // },
891
- // });
892
- },
893
- })
894
- }
895
- />
896
- </>
897
- )}
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..."
898
1766
  lightboxProps={{
899
1767
  underlayColor: 'transparent',
900
1768
  springConfig: { tension: 90000, friction: 90000 },
901
1769
  disabled: true,
902
1770
  }}
1771
+ infiniteScroll={false} // Disable automatic loading
903
1772
  />
904
- </>
1773
+ </View>
905
1774
  );
906
1775
  };
907
1776