@messenger-box/platform-mobile 10.0.3-alpha.5 → 10.0.3-alpha.50

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 (98) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/lib/compute.js +2 -3
  3. package/lib/compute.js.map +1 -1
  4. package/lib/index.js.map +1 -1
  5. package/lib/queries/inboxQueries.js +65 -0
  6. package/lib/queries/inboxQueries.js.map +1 -0
  7. package/lib/routes.json +2 -3
  8. package/lib/screens/inbox/DialogMessages.js +1 -1
  9. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  10. package/lib/screens/inbox/DialogThreadMessages.js +4 -8
  11. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  12. package/lib/screens/inbox/DialogThreads.js +57 -12
  13. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  14. package/lib/screens/inbox/Inbox.js +1 -1
  15. package/lib/screens/inbox/Inbox.js.map +1 -1
  16. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  17. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  18. package/lib/screens/inbox/components/CachedImage/index.js +168 -46
  19. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  20. package/lib/screens/inbox/components/DialogItem.js +169 -0
  21. package/lib/screens/inbox/components/DialogItem.js.map +1 -0
  22. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
  23. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  24. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +147 -31
  25. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  26. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +6 -1
  27. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  28. package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
  29. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  30. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -55
  31. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  32. package/lib/screens/inbox/config/config.js +2 -2
  33. package/lib/screens/inbox/config/config.js.map +1 -1
  34. package/lib/screens/inbox/containers/ConversationView.js +1111 -434
  35. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  36. package/lib/screens/inbox/containers/Dialogs.js +193 -80
  37. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  38. package/lib/screens/inbox/containers/ThreadConversationView.js +725 -216
  39. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  40. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  41. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  42. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  43. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  44. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  45. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  46. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  47. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  48. package/package.json +4 -4
  49. package/src/compute.ts +5 -6
  50. package/src/index.ts +2 -0
  51. package/src/navigation/InboxNavigation.tsx +3 -3
  52. package/src/queries/inboxQueries.ts +299 -0
  53. package/src/queries/index.d.ts +2 -0
  54. package/src/queries/index.ts +1 -0
  55. package/src/screens/inbox/DialogMessages.tsx +1 -1
  56. package/src/screens/inbox/DialogThreadMessages.tsx +7 -14
  57. package/src/screens/inbox/DialogThreads.tsx +55 -61
  58. package/src/screens/inbox/Inbox.tsx +1 -1
  59. package/src/screens/inbox/components/Actionsheet.tsx +30 -0
  60. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  61. package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
  62. package/src/screens/inbox/components/DialogItem.tsx +306 -0
  63. package/src/screens/inbox/components/DialogsHeader.tsx +6 -13
  64. package/src/screens/inbox/components/DialogsListItem.tsx +262 -198
  65. package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
  66. package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
  67. package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
  68. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +337 -194
  69. package/src/screens/inbox/components/SlackInput.tsx +23 -0
  70. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
  71. package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +1 -1
  72. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  73. package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
  74. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +53 -55
  75. package/src/screens/inbox/components/ThreadsViewItem.tsx +178 -285
  76. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  77. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  78. package/src/screens/inbox/config/config.ts +2 -2
  79. package/src/screens/inbox/containers/ConversationView.tsx +1843 -702
  80. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  81. package/src/screens/inbox/containers/Dialogs.tsx +402 -204
  82. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +4 -4
  83. package/src/screens/inbox/containers/ThreadConversationView.tsx +1350 -319
  84. package/src/screens/inbox/containers/ThreadsView.tsx +105 -193
  85. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  86. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  87. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  88. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
  89. package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
  90. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  91. package/src/screens/inbox/index.ts +37 -0
  92. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  93. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
  94. package/tsconfig.json +11 -54
  95. package/lib/screens/inbox/components/DialogsListItem.js +0 -171
  96. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  97. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -171
  98. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
@@ -0,0 +1,1467 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Avatar,
4
+ AvatarFallbackText,
5
+ AvatarImage,
6
+ Box,
7
+ Button,
8
+ ButtonText,
9
+ HStack,
10
+ Icon,
11
+ Image,
12
+ Spinner,
13
+ Text,
14
+ Skeleton,
15
+ } from '@admin-layout/gluestack-ui-mobile';
16
+ import { Platform, TouchableHighlight, SafeAreaView, View } from 'react-native';
17
+ import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native';
18
+ import { navigationRef } from '@common-stack/client-react';
19
+ import { useSelector } from 'react-redux';
20
+ import { orderBy, startCase, uniqBy } from 'lodash-es';
21
+ import * as ImagePicker from 'expo-image-picker';
22
+ import { encode as atob } from 'base-64';
23
+ import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
24
+ import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
25
+ import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo, FileRefType, PostTypeEnum } from 'common';
26
+ import {
27
+ OnChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
28
+ useMessagesQuery,
29
+ useSendExpoNotificationOnPostMutation,
30
+ useSendMessagesMutation,
31
+ useViewChannelDetailQuery,
32
+ useAddDirectChannelMutation,
33
+ MessagesDocument,
34
+ } from 'common/graphql';
35
+ import { useUploadFilesNative } from '@messenger-box/platform-client';
36
+ import { objectId } from '@messenger-box/core';
37
+ import { userSelector } from '@adminide-stack/user-auth0-client';
38
+ import { format, isToday, isYesterday } from 'date-fns';
39
+ import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
40
+ import CachedImage from '../components/CachedImage';
41
+ import { config } from '../config';
42
+ import colors from 'tailwindcss/colors';
43
+ import { v4 as uuidv4 } from 'uuid';
44
+
45
+ // Define an extended interface for ImagePickerAsset with url property
46
+ interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
47
+ url?: string;
48
+ fileName?: string;
49
+ mimeType?: string;
50
+ }
51
+
52
+ const {
53
+ MESSAGES_PER_PAGE,
54
+ CALL_TO_ACTION_BOX_BGCOLOR,
55
+ CALL_TO_ACTION_PATH,
56
+ CALL_TO_ACTION_BUTTON_BORDERCOLOR,
57
+ CALL_TO_ACTION_TEXT_COLOR,
58
+ } = config;
59
+
60
+ const createdAtText = (value: string) => {
61
+ if (!value) return '';
62
+ let date = new Date(value);
63
+ if (isToday(date)) return 'Today';
64
+ if (isYesterday(date)) return 'Yesterday';
65
+ return format(new Date(value), 'MMM dd, yyyy');
66
+ };
67
+
68
+ interface ISubscriptionHandlerProps {
69
+ subscribeToNewMessages: () => any;
70
+ channelId: string;
71
+ }
72
+
73
+ interface IMessageProps extends IMessage {
74
+ type: string;
75
+ propsConfiguration?: any;
76
+ replies?: any;
77
+ isShowThreadMessage?: boolean;
78
+ }
79
+
80
+ export interface AlertMessageAttachmentsInterface {
81
+ title: string;
82
+ isTitleHtml: boolean;
83
+ icon: string;
84
+ callToAction: {
85
+ title: string;
86
+ link: string;
87
+ };
88
+ }
89
+
90
+ // Fix for the optimistic response types
91
+ type OptimisticPropsConfig = {
92
+ __typename: 'MachineConfiguration';
93
+ resource: string;
94
+ };
95
+
96
+ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowThreadMessage, ...rest }: any) => {
97
+ // Core state management using React hooks instead of XState
98
+ const [channelId, setChannelId] = useState<string | null>(initialChannelId || null);
99
+ const [messageText, setMessageText] = useState('');
100
+ const [skip, setSkip] = useState(0);
101
+ const [loading, setLoading] = useState(false);
102
+ const [loadingOldMessages, setLoadingOldMessages] = useState(false);
103
+ const [error, setError] = useState<string | null>(null);
104
+ const [selectedImage, setSelectedImage] = useState<string>('');
105
+ const [images, setImages] = useState<any[]>([]);
106
+ const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
107
+ const [imageObject, setImageObject] = useState<any>({});
108
+
109
+ // Create refs for various operations
110
+ const messageRootListRef = useRef<any>(null);
111
+ const isMounted = useRef(true);
112
+ const fetchOldDebounceRef = useRef(false);
113
+
114
+ // Navigation and auth
115
+ const auth: any = useSelector(userSelector);
116
+ const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
117
+ const navigation = useNavigation<any>();
118
+ const isFocused = useIsFocused();
119
+
120
+ // Apollo mutations
121
+ const [addDirectChannel] = useAddDirectChannelMutation();
122
+ const { startUpload } = useUploadFilesNative();
123
+ const [sendMsg] = useSendMessagesMutation();
124
+ const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
125
+
126
+ // Apollo query for messages
127
+ const {
128
+ data,
129
+ loading: messageLoading,
130
+ refetch,
131
+ fetchMore: fetchMoreMessages,
132
+ subscribeToMore,
133
+ } = useMessagesQuery({
134
+ variables: {
135
+ channelId: channelId?.toString(),
136
+ parentId: null,
137
+ limit: MESSAGES_PER_PAGE,
138
+ skip: skip,
139
+ },
140
+ skip: !channelId,
141
+ fetchPolicy: 'cache-and-network',
142
+ nextFetchPolicy: 'cache-first',
143
+ refetchWritePolicy: 'merge',
144
+ notifyOnNetworkStatusChange: true,
145
+ onCompleted: (queryData) => {
146
+ // MESSAGE QUERY COMPLETED
147
+ },
148
+ onError: (error) => {
149
+ setError(String(error));
150
+ },
151
+ });
152
+
153
+ // Extract messages from the query data
154
+ const channelMessages = useMemo(() => {
155
+ return data?.messages?.data || [];
156
+ }, [data?.messages?.data]);
157
+
158
+ // Get total message count
159
+ const totalCount = useMemo(() => {
160
+ return data?.messages?.totalCount || 0;
161
+ }, [data?.messages?.totalCount]);
162
+
163
+ // Clear messages when component unmounts
164
+ useEffect(() => {
165
+ return () => {
166
+ isMounted.current = false;
167
+ };
168
+ }, []);
169
+
170
+ // Update channelId from props or navigation params
171
+ useEffect(() => {
172
+ const currentChannelId = initialChannelId || currentRoute?.params?.channelId;
173
+ if (currentChannelId) {
174
+ setChannelId(currentChannelId);
175
+ }
176
+ }, [initialChannelId, currentRoute]);
177
+
178
+ // Focus/unfocus behavior
179
+ useFocusEffect(
180
+ React.useCallback(() => {
181
+ if (channelId) {
182
+ // Refresh messages when screen comes into focus
183
+ refetch();
184
+ }
185
+ return () => {
186
+ // Nothing needed on unfocus
187
+ };
188
+ }, [channelId, isFocused, refetch]),
189
+ );
190
+
191
+ // Loading state for image selection
192
+ useEffect(() => {
193
+ if (selectedImage) {
194
+ setLoading(false);
195
+ }
196
+ }, [selectedImage]);
197
+
198
+ // Fetch more messages function
199
+ const fetchMoreMessagesImpl = useCallback(async () => {
200
+ try {
201
+ setLoadingOldMessages(true);
202
+ const response = await fetchMoreMessages({
203
+ variables: {
204
+ channelId: channelId?.toString(),
205
+ parentId: null,
206
+ skip: channelMessages.length,
207
+ },
208
+ // updateQuery: (prev, { fetchMoreResult }) => {
209
+ // if (!fetchMoreResult || !fetchMoreResult.messages) return prev;
210
+
211
+ // // Create a new array of all messages deduped by ID
212
+ // const combinedMessages = [...prev.messages.data, ...fetchMoreResult.messages.data].filter(
213
+ // (message, index, self) => index === self.findIndex((m) => m.id === message.id),
214
+ // );
215
+
216
+ // return {
217
+ // ...prev,
218
+ // messages: {
219
+ // ...prev.messages,
220
+ // data: combinedMessages,
221
+ // totalCount: fetchMoreResult.messages.totalCount,
222
+ // __typename: prev.messages.__typename,
223
+ // },
224
+ // };
225
+ // },
226
+ });
227
+
228
+ setLoadingOldMessages(false);
229
+ if (!response?.data?.messages?.data) {
230
+ return { error: 'No messages returned' };
231
+ }
232
+
233
+ return { messages: response.data.messages.data };
234
+ } catch (error) {
235
+ setLoadingOldMessages(false);
236
+ setError(String(error));
237
+ return { error: String(error) };
238
+ }
239
+ }, [channelId, channelMessages.length, fetchMoreMessages]);
240
+
241
+ // Send message function
242
+ const sendMessageImpl = useCallback(async () => {
243
+ try {
244
+ // Store the current message text and clear input immediately for better UX
245
+ const currentMessageText = messageText;
246
+ setMessageText('');
247
+
248
+ const notificationData: IExpoNotificationData = {
249
+ url: config.INBOX_MESSEGE_PATH,
250
+ params: { channelId, hideTabBar: true },
251
+ screen: 'DialogMessages',
252
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
253
+ };
254
+
255
+ // Create optimistic message with consistent structure
256
+ const messageId = objectId();
257
+ const optimisticMessage = {
258
+ __typename: 'Post' as const,
259
+ id: messageId,
260
+ message: currentMessageText,
261
+ createdAt: new Date().toISOString(),
262
+ updatedAt: new Date().toISOString(),
263
+ author: {
264
+ __typename: 'UserAccount' as const,
265
+ id: auth?.id,
266
+ givenName: auth?.givenName || '',
267
+ familyName: auth?.familyName || '',
268
+ picture: auth?.picture || '',
269
+ username: auth?.username || '',
270
+ email: auth?.email || '',
271
+ alias: [] as string[],
272
+ tokens: [],
273
+ },
274
+ isDelivered: true,
275
+ isRead: false,
276
+ type: 'TEXT' as any,
277
+ parentId: null,
278
+ fromServer: false,
279
+ channel: {
280
+ __typename: 'Channel' as const,
281
+ id: channelId,
282
+ },
283
+ propsConfiguration: {
284
+ __typename: 'MachineConfiguration' as const,
285
+ resource: '' as any, // Cast to any to bypass the URI type check
286
+ },
287
+ props: {},
288
+ files: {
289
+ __typename: 'FilesInfo' as const,
290
+ data: [],
291
+ totalCount: 0,
292
+ },
293
+ replies: {
294
+ __typename: 'Messages' as const,
295
+ data: [],
296
+ totalCount: 0,
297
+ },
298
+ };
299
+
300
+ const response = await sendMsg({
301
+ variables: {
302
+ channelId,
303
+ content: currentMessageText,
304
+ notificationParams: notificationData,
305
+ },
306
+ optimisticResponse: {
307
+ __typename: 'Mutation',
308
+ sendMessage: optimisticMessage,
309
+ },
310
+ });
311
+
312
+ return { message: response.data?.sendMessage };
313
+ } catch (error) {
314
+ setLoading(false);
315
+ setError(String(error));
316
+ return { error: String(error) };
317
+ }
318
+ }, [channelId, messageText, sendMsg, auth]);
319
+
320
+ // Image selection handler
321
+ const onSelectImages = async () => {
322
+ setLoading(true);
323
+
324
+ try {
325
+ let imageSource = await ImagePicker.launchImageLibraryAsync({
326
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
327
+ allowsEditing: true,
328
+ aspect: [4, 3],
329
+ quality: 0.8,
330
+ base64: true,
331
+ exif: false,
332
+ });
333
+
334
+ if (!imageSource?.canceled) {
335
+ // Get the asset
336
+ const selectedAsset = imageSource?.assets?.[0];
337
+ if (!selectedAsset) {
338
+ setLoading(false);
339
+ return;
340
+ }
341
+
342
+ // Create a base64 image string for preview
343
+ const base64Data = selectedAsset.base64;
344
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
345
+
346
+ // Format the asset for upload service requirements
347
+ const asset: ExtendedImagePickerAsset = {
348
+ ...selectedAsset,
349
+ url: selectedAsset.uri,
350
+ fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
351
+ mimeType: 'image/jpeg',
352
+ };
353
+
354
+ // Update state with the new image
355
+ setSelectedImage(previewImage);
356
+ setImages([asset]);
357
+ } else {
358
+ setLoading(false);
359
+ }
360
+ } catch (error) {
361
+ setLoading(false);
362
+ }
363
+ };
364
+
365
+ // Add a state variable to track which message should show the skeleton
366
+ const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
367
+
368
+ // Send message with file function - update to set and clear uploadingMessageId
369
+ const sendMessageWithFileImpl = useCallback(async () => {
370
+ try {
371
+ // For file uploads, we still need loading state since we need to wait for the file upload
372
+ setLoading(true);
373
+
374
+ // Generate a unique post ID for the message
375
+ const postId = objectId();
376
+
377
+ // Set the message ID that should show the skeleton
378
+ setUploadingMessageId(postId);
379
+
380
+ // Prepare notification data
381
+ const notificationData: IExpoNotificationData = {
382
+ url: config.INBOX_MESSEGE_PATH,
383
+ params: { channelId, hideTabBar: true },
384
+ screen: 'DialogMessages',
385
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
386
+ };
387
+
388
+ // Safety check for images
389
+ if (!images || images.length === 0) {
390
+ setLoading(false);
391
+ setUploadingMessageId(null);
392
+ return { error: 'No images available to upload' };
393
+ }
394
+
395
+ // Format the images for upload if needed
396
+ const imagesToUpload = images.map((img) => {
397
+ // Ensure the image has all required properties
398
+ return {
399
+ ...img,
400
+ uri: img.uri || img.url, // Use either uri or url
401
+ type: 'image/jpeg',
402
+ name: img.fileName || `image_${Date.now()}.jpg`,
403
+ };
404
+ });
405
+
406
+ // Store current message text and clear inputs immediately for better UX
407
+ const currentMessageText = messageText;
408
+ setMessageText('');
409
+
410
+ // Create file info for optimistic response
411
+ const optimisticFileInfo = {
412
+ __typename: 'FileInfo' as const,
413
+ id: objectId(),
414
+ url: selectedImage,
415
+ name: imagesToUpload[0]?.name || 'image.jpg',
416
+ extension: 'jpg',
417
+ mimeType: 'image/jpeg',
418
+ size: 0,
419
+ refType: FileRefType.Post,
420
+ height: imagesToUpload[0]?.height || 0,
421
+ width: imagesToUpload[0]?.width || 0,
422
+ };
423
+
424
+ // Create optimistic message with file
425
+ const optimisticMessage = {
426
+ __typename: 'Post' as const,
427
+ id: postId,
428
+ message: currentMessageText || ' ',
429
+ createdAt: new Date().toISOString(),
430
+ updatedAt: new Date().toISOString(),
431
+ author: {
432
+ __typename: 'UserAccount' as const,
433
+ id: auth?.id,
434
+ givenName: auth?.givenName || '',
435
+ familyName: auth?.familyName || '',
436
+ picture: auth?.picture || '',
437
+ username: auth?.username || '',
438
+ email: auth?.email || '',
439
+ alias: [] as string[],
440
+ tokens: [],
441
+ },
442
+ isDelivered: true,
443
+ isRead: false,
444
+ type: 'TEXT' as any,
445
+ parentId: null,
446
+ fromServer: false,
447
+ channel: {
448
+ __typename: 'Channel' as const,
449
+ id: channelId,
450
+ },
451
+ propsConfiguration: {
452
+ __typename: 'MachineConfiguration' as const,
453
+ resource: '' as any, // Cast to any to bypass the URI type check
454
+ },
455
+ props: {},
456
+ files: {
457
+ __typename: 'FilesInfo' as const,
458
+ data: [
459
+ {
460
+ __typename: 'FileInfo' as const,
461
+ id: objectId(),
462
+ url: selectedImage,
463
+ name: imagesToUpload[0]?.name || 'image.jpg',
464
+ extension: 'jpg',
465
+ mimeType: 'image/jpeg',
466
+ size: 0,
467
+ refType: FileRefType.Post,
468
+ height: imagesToUpload[0]?.height || 0,
469
+ width: imagesToUpload[0]?.width || 0,
470
+ },
471
+ ] as any,
472
+ totalCount: 1,
473
+ },
474
+ replies: {
475
+ __typename: 'Messages' as const,
476
+ data: [],
477
+ totalCount: 0,
478
+ },
479
+ };
480
+
481
+ // Upload the files
482
+ const uploadResponse = await startUpload({
483
+ file: imagesToUpload,
484
+ saveUploadedFile: {
485
+ variables: { postId },
486
+ },
487
+ createUploadLink: {
488
+ variables: { postId },
489
+ },
490
+ });
491
+
492
+ if (uploadResponse?.error) {
493
+ setLoading(false);
494
+ setUploadingMessageId(null);
495
+ return { error: String(uploadResponse.error) };
496
+ }
497
+
498
+ // Get uploaded file IDs
499
+ const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
500
+ const files = uploadedFiles?.map((f: any) => f.id) ?? null;
501
+
502
+ // Send the message with the uploaded files
503
+ const response = await sendMsg({
504
+ variables: {
505
+ postId,
506
+ channelId,
507
+ content: currentMessageText || ' ', // Use a space if no text
508
+ files,
509
+ notificationParams: notificationData,
510
+ },
511
+ optimisticResponse: {
512
+ __typename: 'Mutation',
513
+ sendMessage: optimisticMessage,
514
+ },
515
+ });
516
+
517
+ if (response?.data?.sendMessage) {
518
+ // Clear the images after successful send
519
+ setSelectedImage('');
520
+ setImages([]);
521
+ }
522
+
523
+ setLoading(false);
524
+ setUploadingMessageId(null);
525
+ return { message: response.data?.sendMessage };
526
+ } catch (error) {
527
+ setLoading(false);
528
+ setUploadingMessageId(null);
529
+ setError(String(error));
530
+ return { error: String(error) };
531
+ }
532
+ }, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth]);
533
+
534
+ // Create direct channel implementation
535
+ const createDirectChannelImpl = useCallback(async () => {
536
+ try {
537
+ setLoading(true);
538
+ if (
539
+ !rest?.isCreateNewChannel ||
540
+ rest?.newChannelData?.type !== RoomType?.Direct ||
541
+ !rest?.newChannelData?.userIds?.length
542
+ ) {
543
+ setLoading(false);
544
+ return { error: 'Invalid channel data' };
545
+ }
546
+
547
+ // Store current message text
548
+ const currentMessageText = messageText;
549
+ // Clear message text immediately for better UX
550
+ setMessageText('');
551
+
552
+ const response = await addDirectChannel({
553
+ variables: {
554
+ receiver: [...(rest?.newChannelData?.userIds ?? [])],
555
+ displayName: 'DIRECT CHANNEL',
556
+ },
557
+ });
558
+
559
+ if (!response?.data?.createDirectChannel?.id) {
560
+ setLoading(false);
561
+ return { error: 'Failed to create channel' };
562
+ }
563
+
564
+ const newChannelId = response.data.createDirectChannel.id;
565
+ setChannelId(newChannelId);
566
+
567
+ const notificationData: IExpoNotificationData = {
568
+ url: config.INBOX_MESSEGE_PATH,
569
+ params: { channelId: newChannelId, hideTabBar: true },
570
+ screen: 'DialogMessages',
571
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
572
+ };
573
+
574
+ // Create unique message ID for optimistic response
575
+ const messageId = objectId();
576
+
577
+ // Fix the createDirectChannelImpl optimisticMessage with all required fields
578
+ const optimisticMessage = {
579
+ __typename: 'Post' as const,
580
+ id: messageId,
581
+ message: currentMessageText,
582
+ createdAt: new Date().toISOString(),
583
+ updatedAt: new Date().toISOString(),
584
+ author: {
585
+ __typename: 'UserAccount' as const,
586
+ id: auth?.id,
587
+ givenName: auth?.givenName || '',
588
+ familyName: auth?.familyName || '',
589
+ picture: auth?.picture || '',
590
+ username: auth?.username || '',
591
+ email: auth?.email || '',
592
+ alias: [] as string[],
593
+ tokens: [],
594
+ },
595
+ isDelivered: true,
596
+ isRead: false,
597
+ type: 'TEXT' as any,
598
+ parentId: null,
599
+ fromServer: false,
600
+ channel: {
601
+ __typename: 'Channel' as const,
602
+ id: newChannelId,
603
+ },
604
+ propsConfiguration: {
605
+ __typename: 'MachineConfiguration' as const,
606
+ resource: '' as any, // Cast to any to bypass the URI type check
607
+ },
608
+ props: {},
609
+ files: {
610
+ __typename: 'FilesInfo' as const,
611
+ data: [],
612
+ totalCount: 0,
613
+ },
614
+ replies: {
615
+ __typename: 'Messages' as const,
616
+ data: [],
617
+ totalCount: 0,
618
+ },
619
+ };
620
+
621
+ // Send message in the new channel
622
+ await sendMsg({
623
+ variables: {
624
+ channelId: newChannelId,
625
+ content: currentMessageText,
626
+ notificationParams: notificationData,
627
+ },
628
+ optimisticResponse: {
629
+ __typename: 'Mutation',
630
+ sendMessage: optimisticMessage,
631
+ },
632
+ });
633
+
634
+ setLoading(false);
635
+ return { channelId: newChannelId };
636
+ } catch (error) {
637
+ setLoading(false);
638
+ setError(String(error));
639
+ return { error: String(error) };
640
+ }
641
+ }, [rest, messageText, addDirectChannel, sendMsg, auth]);
642
+
643
+ // Optimize onFetchOld by adding debounce logic
644
+ const onFetchOld = useCallback(() => {
645
+ // Prevent multiple rapid calls
646
+ if (fetchOldDebounceRef.current) return;
647
+
648
+ // Check if we need to fetch more messages
649
+ if (totalCount > channelMessages.length && !loadingOldMessages) {
650
+ // Set debounce
651
+ fetchOldDebounceRef.current = true;
652
+
653
+ // Fetch more messages
654
+ fetchMoreMessagesImpl();
655
+
656
+ // Clear debounce after a timeout
657
+ setTimeout(() => {
658
+ fetchOldDebounceRef.current = false;
659
+ }, 1000);
660
+ }
661
+ }, [totalCount, channelMessages.length, loadingOldMessages, fetchMoreMessagesImpl]);
662
+
663
+ const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
664
+ const paddingToTop = 60;
665
+ return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
666
+ };
667
+
668
+ // Transform the message data for GiftedChat
669
+ const messageList = useMemo(() => {
670
+ // Short-circuit if no messages to process
671
+ if (!channelMessages || channelMessages.length === 0) {
672
+ return [];
673
+ }
674
+
675
+ // Use a more efficient approach - pre-filter messages once
676
+ const filteredMessages = uniqBy(channelMessages, ({ id }) => id);
677
+
678
+ // Skip processing if no filtered messages
679
+ if (filteredMessages.length === 0) {
680
+ return [];
681
+ }
682
+
683
+ // Transform messages only once and return
684
+ return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
685
+ const date = new Date(msg.createdAt);
686
+
687
+ // Extract image URL from files data
688
+ let imageUrl = null;
689
+ if (msg.files?.data && msg.files.data.length > 0) {
690
+ const fileData = msg.files.data[0];
691
+ if (fileData && fileData.url) {
692
+ imageUrl = fileData.url;
693
+ }
694
+ }
695
+
696
+ // Create message in a more direct way
697
+ return {
698
+ _id: msg.id,
699
+ text: msg.message,
700
+ createdAt: date,
701
+ user: {
702
+ _id: msg.author?.id || '',
703
+ name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
704
+ avatar: msg.author?.picture || '',
705
+ },
706
+ image: imageUrl,
707
+ sent: msg?.isDelivered,
708
+ received: msg?.isRead,
709
+ type: msg?.type,
710
+ propsConfiguration: msg?.propsConfiguration,
711
+ replies: msg?.replies ?? [],
712
+ isShowThreadMessage,
713
+ };
714
+ });
715
+ }, [channelMessages, isShowThreadMessage]);
716
+
717
+ // Render the send button
718
+ const renderSend = useCallback(
719
+ (props) => {
720
+ // Enable the send button if there's text OR we have images
721
+ const hasContent = !!props.text || images?.length > 0;
722
+ const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
723
+
724
+ return (
725
+ <Send
726
+ {...props}
727
+ disabled={!canSend}
728
+ containerStyle={{
729
+ justifyContent: 'center',
730
+ alignItems: 'center',
731
+ height: 40,
732
+ width: 44,
733
+ marginRight: 4,
734
+ marginBottom: 0,
735
+ marginLeft: 4,
736
+ }}
737
+ >
738
+ <View style={{ padding: 4 }}>
739
+ <MaterialCommunityIcons
740
+ name="send-circle"
741
+ size={32}
742
+ color={canSend ? colors.blue[500] : colors.gray[400]}
743
+ />
744
+ </View>
745
+ </Send>
746
+ );
747
+ },
748
+ [channelId, images, rest?.isCreateNewChannel],
749
+ );
750
+
751
+ // Handle send for messages
752
+ const handleSend = useCallback(
753
+ async (messages) => {
754
+ // Extract message text from GiftedChat messages array
755
+ const newMessageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
756
+
757
+ // Check if we can send a message (channel exists or we're creating one)
758
+ if (!channelId && !rest?.isCreateNewChannel) {
759
+ return;
760
+ }
761
+
762
+ // Allow sending if we have text OR images (image-only messages are valid)
763
+ const hasText = !!newMessageText && newMessageText !== ' ';
764
+ const hasImages = images && images.length > 0;
765
+
766
+ if (!hasText && !hasImages) {
767
+ return;
768
+ }
769
+
770
+ // Update the message text state - now handled in send functions for better UX
771
+ setMessageText(newMessageText);
772
+
773
+ // Handle direct channel creation if needed
774
+ if (rest?.isCreateNewChannel && !channelId) {
775
+ if (rest?.newChannelData?.type === RoomType?.Direct) {
776
+ createDirectChannelImpl();
777
+ }
778
+ return;
779
+ }
780
+
781
+ // Send message with or without image based on state
782
+ if (hasImages) {
783
+ sendMessageWithFileImpl();
784
+ } else {
785
+ sendMessageImpl();
786
+ }
787
+ },
788
+ [
789
+ channelId,
790
+ images,
791
+ rest?.isCreateNewChannel,
792
+ rest?.newChannelData?.type,
793
+ createDirectChannelImpl,
794
+ sendMessageWithFileImpl,
795
+ sendMessageImpl,
796
+ ],
797
+ );
798
+
799
+ // Render message text with customizations for alerts and replies
800
+ const renderMessageText = useCallback(
801
+ (props: any) => {
802
+ const { currentMessage } = props;
803
+ const lastReply: any =
804
+ currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
805
+
806
+ if (currentMessage.type === 'ALERT') {
807
+ const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
808
+ let action: string = '';
809
+ let actionId: any = '';
810
+ let params: any = {};
811
+
812
+ if (attachment?.callToAction?.extraParams) {
813
+ const extraParams: any = attachment?.callToAction?.extraParams;
814
+ const route: any = extraParams?.route ?? null;
815
+ let path: any = null;
816
+ let param: any = null;
817
+ if (role && role == PreDefinedRole.Guest) {
818
+ path = route?.guest?.name ? route?.guest?.name ?? null : null;
819
+ param = route?.guest?.params ? route?.guest?.params ?? null : null;
820
+ } else if (role && role == PreDefinedRole.Owner) {
821
+ path = route?.host?.name ? route?.host?.name ?? null : null;
822
+ param = route?.host?.params ? route?.host?.params ?? null : null;
823
+ } else {
824
+ path = route?.host?.name ? route?.host?.name ?? null : null;
825
+ param = route?.host?.params ? route?.host?.params ?? null : null;
826
+ }
827
+
828
+ action = path;
829
+ params = { ...param };
830
+ } else if (attachment?.callToAction?.link) {
831
+ action = CALL_TO_ACTION_PATH;
832
+ actionId = attachment?.callToAction?.link.split('/').pop();
833
+ params = { reservationId: actionId };
834
+ }
835
+
836
+ return (
837
+ <>
838
+ {attachment?.callToAction && action ? (
839
+ <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
840
+ <Button
841
+ variant={'outline'}
842
+ size={'sm'}
843
+ className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
844
+ onPress={() => action && params && navigation.navigate(action, params)}
845
+ >
846
+ <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
847
+ {attachment.callToAction.title}
848
+ </ButtonText>
849
+ </Button>
850
+ <MessageText
851
+ {...props}
852
+ textStyle={{
853
+ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
854
+ }}
855
+ />
856
+ </Box>
857
+ ) : (
858
+ <TouchableHighlight
859
+ underlayColor={'#c0c0c0'}
860
+ style={{ width: '100%' }}
861
+ onPress={() => {
862
+ if (currentMessage?.isShowThreadMessage)
863
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
864
+ channelId: channelId,
865
+ title: 'Message',
866
+ postParentId: currentMessage?._id,
867
+ isPostParentIdThread: true,
868
+ });
869
+ }}
870
+ >
871
+ <>
872
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
873
+ {currentMessage?.replies?.data?.length > 0 && (
874
+ <HStack space={'sm'} className="px-1 items-center">
875
+ <HStack>
876
+ {currentMessage?.replies?.data
877
+ ?.filter(
878
+ (v: any, i: any, a: any) =>
879
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) ===
880
+ i,
881
+ )
882
+ ?.slice(0, 2)
883
+ ?.reverse()
884
+ ?.map((p: any, i: Number) => (
885
+ <Avatar
886
+ key={'conversations-view-key-' + i}
887
+ size={'sm'}
888
+ className="bg-transparent"
889
+ >
890
+ <AvatarFallbackText>
891
+ {startCase(p?.author?.username?.charAt(0))}
892
+ </AvatarFallbackText>
893
+ <AvatarImage
894
+ alt="user image"
895
+ style={{
896
+ borderRadius: 6,
897
+ borderWidth: 2,
898
+ borderColor: '#fff',
899
+ }}
900
+ source={{
901
+ uri: p?.author?.picture,
902
+ }}
903
+ />
904
+ </Avatar>
905
+ ))}
906
+ </HStack>
907
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
908
+ {currentMessage?.replies?.totalCount}{' '}
909
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
910
+ </Text>
911
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
912
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
913
+ </Text>
914
+ </HStack>
915
+ )}
916
+ </>
917
+ </TouchableHighlight>
918
+ )}
919
+ </>
920
+ );
921
+ } else {
922
+ return (
923
+ <TouchableHighlight
924
+ underlayColor={'#c0c0c0'}
925
+ style={{ width: '100%' }}
926
+ onPress={() => {
927
+ if (currentMessage?.isShowThreadMessage)
928
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
929
+ channelId: channelId,
930
+ title: 'Message',
931
+ postParentId: currentMessage?._id,
932
+ isPostParentIdThread: true,
933
+ });
934
+ }}
935
+ >
936
+ <>
937
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
938
+ {currentMessage?.replies?.data?.length > 0 && (
939
+ <HStack space={'sm'} className="px-1 items-center">
940
+ <HStack>
941
+ {currentMessage?.replies?.data
942
+ ?.filter(
943
+ (v: any, i: any, a: any) =>
944
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
945
+ )
946
+ ?.slice(0, 2)
947
+ ?.reverse()
948
+ ?.map((p: any, i: Number) => (
949
+ <Avatar
950
+ key={'conversation-replies-key-' + i}
951
+ className="bg-transparent"
952
+ size={'sm'}
953
+ >
954
+ <AvatarFallbackText>
955
+ {startCase(p?.author?.username?.charAt(0))}
956
+ </AvatarFallbackText>
957
+ <AvatarImage
958
+ alt="user image"
959
+ style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
960
+ source={{
961
+ uri: p?.author?.picture,
962
+ }}
963
+ />
964
+ </Avatar>
965
+ ))}
966
+ </HStack>
967
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
968
+ {currentMessage?.replies?.totalCount}{' '}
969
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
970
+ </Text>
971
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
972
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
973
+ </Text>
974
+ </HStack>
975
+ )}
976
+ </>
977
+ </TouchableHighlight>
978
+ );
979
+ }
980
+ },
981
+ [navigation, channelId, role],
982
+ );
983
+
984
+ // Render action buttons (including image upload)
985
+ const renderActions = (props) => {
986
+ return (
987
+ <Actions
988
+ {...props}
989
+ options={{
990
+ ['Choose from Library']: onSelectImages,
991
+ ['Cancel']: () => {}, // Add this option to make the sheet dismissible
992
+ }}
993
+ optionTintColor="#000000"
994
+ cancelButtonIndex={1} // Set the Cancel option as the cancel button
995
+ icon={() => (
996
+ <Box
997
+ style={{
998
+ width: 32,
999
+ height: 32,
1000
+ alignItems: 'center',
1001
+ justifyContent: 'center',
1002
+ }}
1003
+ >
1004
+ <Ionicons name="image" size={24} color={colors.blue[500]} />
1005
+ </Box>
1006
+ )}
1007
+ containerStyle={{
1008
+ alignItems: 'center',
1009
+ justifyContent: 'center',
1010
+ marginLeft: 8,
1011
+ marginBottom: 0,
1012
+ }}
1013
+ />
1014
+ );
1015
+ };
1016
+
1017
+ // Create a more visible and reliable image preview with cancel button
1018
+ const renderAccessory = useCallback(() => {
1019
+ if (!selectedImage) {
1020
+ return null;
1021
+ }
1022
+
1023
+ return (
1024
+ <View
1025
+ style={{
1026
+ height: 70,
1027
+ backgroundColor: 'white',
1028
+ borderTopWidth: 1,
1029
+ borderTopColor: '#e0e0e0',
1030
+ flexDirection: 'row',
1031
+ alignItems: 'center',
1032
+ margin: 0,
1033
+ padding: 0,
1034
+ paddingVertical: 0,
1035
+ position: 'absolute',
1036
+ bottom: Platform.OS === 'ios' ? 105 : 95, // Position well above the input area
1037
+ left: 0,
1038
+ right: 0,
1039
+ zIndex: 1,
1040
+ elevation: 3,
1041
+ shadowColor: '#000',
1042
+ shadowOffset: { width: 0, height: -1 },
1043
+ shadowOpacity: 0.05,
1044
+ shadowRadius: 2,
1045
+ }}
1046
+ >
1047
+ <View
1048
+ style={{
1049
+ flex: 1,
1050
+ flexDirection: 'row',
1051
+ alignItems: 'center',
1052
+ paddingLeft: 15,
1053
+ paddingRight: 5,
1054
+ }}
1055
+ >
1056
+ <View
1057
+ style={{
1058
+ width: 56,
1059
+ height: 56,
1060
+ marginRight: 15,
1061
+ borderRadius: 4,
1062
+ backgroundColor: colors.gray[200],
1063
+ overflow: 'hidden',
1064
+ borderWidth: 1,
1065
+ borderColor: '#e0e0e0',
1066
+ }}
1067
+ >
1068
+ <Image
1069
+ key={selectedImage}
1070
+ alt={'selected image'}
1071
+ source={{ uri: selectedImage }}
1072
+ style={{
1073
+ width: '100%',
1074
+ height: '100%',
1075
+ }}
1076
+ size={'md'}
1077
+ />
1078
+ {loading && (
1079
+ <View
1080
+ style={{
1081
+ position: 'absolute',
1082
+ top: 0,
1083
+ left: 0,
1084
+ right: 0,
1085
+ bottom: 0,
1086
+ backgroundColor: 'rgba(255, 255, 255, 0.7)',
1087
+ justifyContent: 'center',
1088
+ alignItems: 'center',
1089
+ }}
1090
+ >
1091
+ <Spinner size="small" color={colors.blue[500]} />
1092
+ </View>
1093
+ )}
1094
+ </View>
1095
+
1096
+ <View style={{ flex: 1 }}>
1097
+ <Text style={{ fontSize: 14, fontWeight: '400', color: colors.gray[800] }}>
1098
+ {images[0]?.fileName || 'image_' + new Date().getTime() + '.jpg'}
1099
+ </Text>
1100
+ <Text style={{ fontSize: 12, color: colors.gray[500], marginTop: 2 }}>
1101
+ {loading ? 'Preparing...' : 'Ready to send'}
1102
+ </Text>
1103
+ </View>
1104
+
1105
+ <TouchableHighlight
1106
+ underlayColor={'rgba(0,0,0,0.1)'}
1107
+ onPress={() => {
1108
+ setSelectedImage('');
1109
+ setImages([]);
1110
+ }}
1111
+ style={{
1112
+ backgroundColor: colors.red[500],
1113
+ borderRadius: 24,
1114
+ width: 36,
1115
+ height: 36,
1116
+ alignItems: 'center',
1117
+ justifyContent: 'center',
1118
+ marginRight: 10,
1119
+ }}
1120
+ >
1121
+ <Ionicons name="close" size={20} color="white" />
1122
+ </TouchableHighlight>
1123
+ </View>
1124
+ </View>
1125
+ );
1126
+ }, [selectedImage, loading, images]);
1127
+
1128
+ const setImageViewerObject = (obj: any, v: boolean) => {
1129
+ setImageObject(obj);
1130
+ setImageViewer(v);
1131
+ };
1132
+
1133
+ const modalContent = React.useMemo(() => {
1134
+ if (!imageObject || !imageObject.image) return null;
1135
+ const { image, _id } = imageObject;
1136
+
1137
+ return (
1138
+ <CachedImage
1139
+ style={{ width: '100%', height: '100%' }}
1140
+ resizeMode={'cover'}
1141
+ cacheKey={`${_id}-modal-imageKey`}
1142
+ source={{
1143
+ uri: image,
1144
+ expiresIn: 86400,
1145
+ }}
1146
+ alt={'image'}
1147
+ />
1148
+ );
1149
+ }, [imageObject]);
1150
+
1151
+ // Create a skeleton component for message bubbles with images
1152
+ const renderMessage = useCallback(
1153
+ (props: any) => {
1154
+ // Check if this message ID matches the uploading message ID
1155
+ const isUploading = props.currentMessage._id === uploadingMessageId && loading;
1156
+
1157
+ if (isUploading && props.currentMessage.image) {
1158
+ // Return a custom message skeleton during upload
1159
+ return (
1160
+ <View
1161
+ style={{
1162
+ padding: 10,
1163
+ marginBottom: 10,
1164
+ marginRight: 10,
1165
+ alignSelf: 'flex-end',
1166
+ borderRadius: 15,
1167
+ backgroundColor: colors.gray[100],
1168
+ maxWidth: '80%',
1169
+ }}
1170
+ >
1171
+ {props.currentMessage.text && props.currentMessage.text.trim() !== '' && (
1172
+ <Box
1173
+ style={{
1174
+ height: 15,
1175
+ borderRadius: 4,
1176
+ backgroundColor: colors.gray[200],
1177
+ overflow: 'hidden',
1178
+ marginBottom: 8,
1179
+ }}
1180
+ >
1181
+ <Skeleton variant="rounded" style={{ flex: 1 }} />
1182
+ </Box>
1183
+ )}
1184
+ <Box
1185
+ style={{
1186
+ height: 150,
1187
+ width: 150,
1188
+ borderRadius: 10,
1189
+ backgroundColor: colors.gray[200],
1190
+ overflow: 'hidden',
1191
+ }}
1192
+ >
1193
+ <Skeleton variant="rounded" style={{ flex: 1 }} />
1194
+ </Box>
1195
+ </View>
1196
+ );
1197
+ }
1198
+
1199
+ // Use memo to prevent unnecessary re-renders of each message
1200
+ return (
1201
+ <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1202
+ );
1203
+ },
1204
+ [isShowImageViewer, uploadingMessageId, loading],
1205
+ );
1206
+
1207
+ let onScroll = false;
1208
+
1209
+ // Optimize onMomentumScrollBegin for better scroll performance
1210
+ const onMomentumScrollBegin = async ({ nativeEvent }: any) => {
1211
+ // Set scroll state
1212
+ onScroll = true;
1213
+
1214
+ // Use the debounced fetch function to prevent excessive calls
1215
+ if (isCloseToTop(nativeEvent)) {
1216
+ onFetchOld();
1217
+ }
1218
+ };
1219
+
1220
+ const onEndReached = () => {
1221
+ if (!onScroll) return;
1222
+ onScroll = false;
1223
+ };
1224
+
1225
+ // Add a loader for when more messages are being loaded
1226
+ const renderLoadEarlier = useCallback(() => {
1227
+ return loadingOldMessages ? (
1228
+ <View
1229
+ style={{
1230
+ padding: 10,
1231
+ backgroundColor: 'rgba(255,255,255,0.8)',
1232
+ borderRadius: 10,
1233
+ marginTop: 10,
1234
+ }}
1235
+ >
1236
+ <Spinner size="small" color="#3b82f6" />
1237
+ </View>
1238
+ ) : null;
1239
+ }, [loadingOldMessages]);
1240
+
1241
+ // Add renderInputToolbar function
1242
+ const renderInputToolbar = useCallback((props) => {
1243
+ return (
1244
+ <InputToolbar
1245
+ {...props}
1246
+ containerStyle={{
1247
+ backgroundColor: 'white',
1248
+ borderTopWidth: 1,
1249
+ borderTopColor: colors.gray[200],
1250
+ paddingHorizontal: 4,
1251
+ paddingVertical: 0,
1252
+ paddingTop: 2,
1253
+ marginBottom: 0,
1254
+ marginTop: 0,
1255
+ }}
1256
+ primaryStyle={{
1257
+ alignItems: 'center',
1258
+ }}
1259
+ />
1260
+ );
1261
+ }, []);
1262
+
1263
+ // Create a memoized ImageViewerModal component
1264
+ const imageViewerModal = useMemo(
1265
+ () => (
1266
+ <ImageViewerModal isVisible={isShowImageViewer} setVisible={setImageViewer} modalContent={modalContent} />
1267
+ ),
1268
+ [isShowImageViewer, modalContent],
1269
+ );
1270
+
1271
+ // Create a memoized subscription handler component
1272
+ const subscriptionHandler = useMemo(
1273
+ () => (
1274
+ <SubscriptionHandler
1275
+ channelId={channelId?.toString()}
1276
+ subscribeToNewMessages={() =>
1277
+ subscribeToMore({
1278
+ document: CHAT_MESSAGE_ADDED,
1279
+ variables: {
1280
+ channelId: channelId?.toString(),
1281
+ },
1282
+ // updateQuery: (prev, { subscriptionData }: any) => {
1283
+ // try {
1284
+ // // Check if we have valid subscription data
1285
+ // if (!subscriptionData?.data?.chatMessageAdded) {
1286
+ // return prev;
1287
+ // }
1288
+
1289
+ // const newMessage = subscriptionData.data.chatMessageAdded;
1290
+
1291
+ // // Check if message is from current user - skip update as it's handled by optimistic UI
1292
+ // if (newMessage.author?.id === auth?.id) {
1293
+ // return prev;
1294
+ // }
1295
+
1296
+ // // Check if we already have this message to avoid duplicates
1297
+ // const currentMessages = prev?.messages?.data || [];
1298
+ // if (currentMessages.some((msg) => msg.id === newMessage.id)) {
1299
+ // return prev;
1300
+ // }
1301
+
1302
+ // // Update Apollo cache
1303
+ // const updatedData = {
1304
+ // ...prev,
1305
+ // messages: {
1306
+ // ...prev.messages,
1307
+ // data: [newMessage, ...currentMessages],
1308
+ // totalCount: (prev?.messages?.totalCount || 0) + 1,
1309
+ // },
1310
+ // };
1311
+
1312
+ // return updatedData;
1313
+ // } catch (error) {
1314
+ // return prev;
1315
+ // }
1316
+ // },
1317
+ })
1318
+ }
1319
+ />
1320
+ ),
1321
+ [channelId, subscribeToMore, auth?.id],
1322
+ );
1323
+
1324
+ // Create a memoized renderChatFooter function
1325
+ const renderChatFooter = useCallback(() => {
1326
+ return (
1327
+ <>
1328
+ {imageViewerModal}
1329
+ {subscriptionHandler}
1330
+ </>
1331
+ );
1332
+ }, [imageViewerModal, subscriptionHandler]);
1333
+
1334
+ // Add optimized listViewProps to reduce re-renders and improve list performance
1335
+ const listViewProps = useMemo(
1336
+ () => ({
1337
+ onEndReached: onEndReached,
1338
+ onEndReachedThreshold: 0.5,
1339
+ onMomentumScrollBegin: onMomentumScrollBegin,
1340
+ removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1341
+ initialNumToRender: 10, // Reduce initial render amount
1342
+ maxToRenderPerBatch: 7, // Reduce number in each render batch
1343
+ windowSize: 7, // Reduce the window size
1344
+ updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
1345
+ keyExtractor: (item) => item._id, // Add explicit key extractor
1346
+ }),
1347
+ [onEndReached, onMomentumScrollBegin],
1348
+ );
1349
+
1350
+ // Return optimized component with performance improvements
1351
+ return (
1352
+ <View
1353
+ style={{
1354
+ flex: 1,
1355
+ backgroundColor: 'white',
1356
+ position: 'relative',
1357
+ }}
1358
+ >
1359
+ {messageLoading && <Spinner color={'#3b82f6'} />}
1360
+
1361
+ {/* Render the image preview directly in the container so it's properly positioned */}
1362
+ {selectedImage ? renderAccessory() : null}
1363
+
1364
+ <GiftedChat
1365
+ ref={messageRootListRef}
1366
+ wrapInSafeArea={true}
1367
+ renderLoading={() => <Spinner color={'#3b82f6'} />}
1368
+ messages={messageList}
1369
+ listViewProps={{
1370
+ ...listViewProps,
1371
+ contentContainerStyle: {
1372
+ paddingBottom: selectedImage ? 90 : 0, // Add padding at the bottom when image is selected
1373
+ },
1374
+ }}
1375
+ onSend={handleSend}
1376
+ text={messageText || ' '}
1377
+ onInputTextChanged={(text) => setMessageText(text)}
1378
+ renderFooter={() => (loading && !images.length ? <Spinner color={'#3b82f6'} /> : null)}
1379
+ scrollToBottom
1380
+ user={{
1381
+ _id: auth?.id || '',
1382
+ }}
1383
+ isTyping={false} // Setting to false to reduce animations
1384
+ alwaysShowSend={true} // Always show send button regardless of text content
1385
+ renderSend={renderSend}
1386
+ renderMessageText={renderMessageText}
1387
+ renderInputToolbar={renderInputToolbar}
1388
+ minInputToolbarHeight={50}
1389
+ renderActions={channelId && renderActions}
1390
+ renderMessage={renderMessage}
1391
+ renderChatFooter={renderChatFooter}
1392
+ renderLoadEarlier={renderLoadEarlier}
1393
+ loadEarlier={totalCount > channelMessages.length}
1394
+ isLoadingEarlier={loadingOldMessages}
1395
+ bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0} // Adjust bottom offset based on image preview
1396
+ textInputProps={{
1397
+ style: {
1398
+ borderWidth: 1,
1399
+ borderColor: colors.gray[300],
1400
+ backgroundColor: '#f8f8f8',
1401
+ borderRadius: 20,
1402
+ minHeight: 36,
1403
+ maxHeight: 80,
1404
+ color: '#000',
1405
+ padding: 8,
1406
+ paddingHorizontal: 15,
1407
+ fontSize: 16,
1408
+ flex: 1,
1409
+ marginVertical: 2,
1410
+ marginBottom: 0,
1411
+ },
1412
+ multiline: true,
1413
+ returnKeyType: 'default',
1414
+ enablesReturnKeyAutomatically: true,
1415
+ placeholderTextColor: colors.gray[400],
1416
+ }}
1417
+ minComposerHeight={36}
1418
+ maxComposerHeight={100}
1419
+ isKeyboardInternallyHandled={true}
1420
+ placeholder="Type a message..."
1421
+ lightboxProps={{
1422
+ underlayColor: 'transparent',
1423
+ springConfig: { tension: 90000, friction: 90000 },
1424
+ disabled: true,
1425
+ }}
1426
+ infiniteScroll={false} // Disable automatic loading
1427
+ />
1428
+ </View>
1429
+ );
1430
+ };
1431
+
1432
+ const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptionHandlerProps) => {
1433
+ // Store the channelId in a ref to track changes
1434
+ const channelIdRef = useRef(channelId);
1435
+
1436
+ useEffect(() => {
1437
+ // Don't set up subscription if there's no channel ID
1438
+ if (!channelId) {
1439
+ return;
1440
+ }
1441
+
1442
+ // Call the subscribe function and store the unsubscribe function
1443
+ const unsubscribe = subscribeToNewMessages();
1444
+
1445
+ // Update the ref with the current channelId
1446
+ channelIdRef.current = channelId;
1447
+
1448
+ // Return cleanup function
1449
+ return () => {
1450
+ if (unsubscribe && typeof unsubscribe === 'function') {
1451
+ unsubscribe();
1452
+ }
1453
+ };
1454
+ }, [channelId, subscribeToNewMessages]);
1455
+
1456
+ return null;
1457
+ };
1458
+
1459
+ // Export with React.memo to prevent unnecessary re-renders
1460
+ export const ConversationView = React.memo(ConversationViewComponent, (prevProps, nextProps) => {
1461
+ // Only re-render if these critical props change
1462
+ return (
1463
+ prevProps.channelId === nextProps.channelId &&
1464
+ prevProps.role === nextProps.role &&
1465
+ prevProps.isShowThreadMessage === nextProps.isShowThreadMessage
1466
+ );
1467
+ });