@messenger-box/platform-mobile 10.0.3-alpha.20 → 10.0.3-alpha.201

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 (112) hide show
  1. package/lib/components/messages-container-ui/BuildModeView.js +428 -0
  2. package/lib/components/messages-container-ui/BuildModeView.js.map +1 -0
  3. package/lib/components/messages-container-ui/MessagesContainerUI.js +55 -0
  4. package/lib/components/messages-container-ui/MessagesContainerUI.js.map +1 -0
  5. package/lib/components/messages-container-ui/PlanModeView.js +336 -0
  6. package/lib/components/messages-container-ui/PlanModeView.js.map +1 -0
  7. package/lib/compute.js +2 -3
  8. package/lib/compute.js.map +1 -1
  9. package/lib/index.js +1 -1
  10. package/lib/index.js.map +1 -1
  11. package/lib/module.js.map +1 -1
  12. package/lib/queries/inboxQueries.js +62 -0
  13. package/lib/queries/inboxQueries.js.map +1 -0
  14. package/lib/routes.json +2 -3
  15. package/lib/screens/inbox/DialogMessages.js +8 -3
  16. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  17. package/lib/screens/inbox/DialogThreadMessages.js +6 -11
  18. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  19. package/lib/screens/inbox/DialogThreads.js +58 -20
  20. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  21. package/lib/screens/inbox/Inbox.js.map +1 -1
  22. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  23. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  24. package/lib/screens/inbox/components/CachedImage/index.js +125 -115
  25. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  26. package/lib/screens/inbox/components/DialogItem.js +160 -0
  27. package/lib/screens/inbox/components/DialogItem.js.map +1 -0
  28. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +315 -0
  29. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  30. package/lib/screens/inbox/components/SlackMessageContainer/ImageViewerModal.js +3 -1
  31. package/lib/screens/inbox/components/SlackMessageContainer/ImageViewerModal.js.map +1 -1
  32. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js +194 -0
  33. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js.map +1 -0
  34. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +149 -36
  35. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  36. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +4 -5
  37. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  38. package/lib/screens/inbox/components/SubscriptionHandler.js +22 -0
  39. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  40. package/lib/screens/inbox/components/ThreadsViewItem.js +67 -47
  41. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  42. package/lib/screens/inbox/config/config.js +4 -2
  43. package/lib/screens/inbox/config/config.js.map +1 -1
  44. package/lib/screens/inbox/containers/ConversationView.js +1099 -1094
  45. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  46. package/lib/screens/inbox/containers/Dialogs.js +132 -534
  47. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  48. package/lib/screens/inbox/containers/ThreadConversationView.js +876 -1357
  49. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  50. package/lib/screens/inbox/containers/ThreadsView.js +81 -54
  51. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  52. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  53. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  54. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  55. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  56. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  57. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  58. package/package.json +9 -7
  59. package/CHANGELOG.md +0 -164
  60. package/jest.config.js +0 -24
  61. package/lib/screens/inbox/components/DialogsListItem.js +0 -548
  62. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  63. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -489
  64. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
  65. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js +0 -175
  66. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js.map +0 -1
  67. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js +0 -191
  68. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js.map +0 -1
  69. package/lib/screens/inbox/containers/workflow/conversation-xstate.js +0 -380
  70. package/lib/screens/inbox/containers/workflow/conversation-xstate.js.map +0 -1
  71. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +0 -211
  72. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +0 -1
  73. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js +0 -438
  74. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js.map +0 -1
  75. package/rollup.config.mjs +0 -45
  76. package/src/components/index.ts +0 -0
  77. package/src/compute.ts +0 -63
  78. package/src/index.ts +0 -7
  79. package/src/module.ts +0 -10
  80. package/src/navigation/InboxNavigation.tsx +0 -102
  81. package/src/navigation/index.ts +0 -1
  82. package/src/screens/inbox/DialogMessages.tsx +0 -21
  83. package/src/screens/inbox/DialogThreadMessages.tsx +0 -97
  84. package/src/screens/inbox/DialogThreads.tsx +0 -129
  85. package/src/screens/inbox/Inbox.tsx +0 -17
  86. package/src/screens/inbox/components/CachedImage/consts.ts +0 -6
  87. package/src/screens/inbox/components/CachedImage/index.tsx +0 -223
  88. package/src/screens/inbox/components/DialogsHeader.tsx +0 -30
  89. package/src/screens/inbox/components/DialogsListItem.tsx +0 -819
  90. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +0 -679
  91. package/src/screens/inbox/components/SlackMessageContainer/ImageViewerModal.tsx +0 -113
  92. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +0 -313
  93. package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +0 -145
  94. package/src/screens/inbox/components/SlackMessageContainer/index.ts +0 -3
  95. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +0 -301
  96. package/src/screens/inbox/components/ThreadsViewItem.tsx +0 -321
  97. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +0 -145
  98. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +0 -159
  99. package/src/screens/inbox/config/config.ts +0 -15
  100. package/src/screens/inbox/config/index.ts +0 -1
  101. package/src/screens/inbox/containers/ConversationView.tsx +0 -1782
  102. package/src/screens/inbox/containers/Dialogs.tsx +0 -794
  103. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +0 -119
  104. package/src/screens/inbox/containers/ThreadConversationView.tsx +0 -2312
  105. package/src/screens/inbox/containers/ThreadsView.tsx +0 -305
  106. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +0 -20
  107. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +0 -313
  108. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +0 -196
  109. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +0 -401
  110. package/src/screens/index.ts +0 -4
  111. package/tsconfig.json +0 -13
  112. package/webpack.config.js +0 -58
@@ -1,1782 +0,0 @@
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
- } from '@admin-layout/gluestack-ui-mobile';
15
- import { Platform, TouchableHighlight, SafeAreaView, View } from 'react-native';
16
- import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native';
17
- import { navigationRef } from '@common-stack/client-react';
18
- import { useSelector } from 'react-redux';
19
- import { orderBy, startCase, uniqBy } from 'lodash-es';
20
- import * as ImagePicker from 'expo-image-picker';
21
- import { encode as atob } from 'base-64';
22
- import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
23
- import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
24
- import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo } from 'common';
25
- import {
26
- OnChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
27
- useMessagesQuery,
28
- useSendExpoNotificationOnPostMutation,
29
- useSendMessagesMutation,
30
- useViewChannelDetailQuery,
31
- useAddDirectChannelMutation,
32
- } from 'common/graphql';
33
- import { useUploadFilesNative } from '@messenger-box/platform-client';
34
- import { objectId } from '@messenger-box/core';
35
- import { userSelector } from '@adminide-stack/user-auth0-client';
36
- import { format, isToday, isYesterday } from 'date-fns';
37
- import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
38
- import CachedImage from '../components/CachedImage';
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
- }
54
-
55
- const {
56
- MESSAGES_PER_PAGE,
57
- CALL_TO_ACTION_BOX_BGCOLOR,
58
- CALL_TO_ACTION_PATH,
59
- CALL_TO_ACTION_BUTTON_BORDERCOLOR,
60
- CALL_TO_ACTION_TEXT_COLOR,
61
- } = config;
62
-
63
- const createdAtText = (value: string) => {
64
- if (!value) return '';
65
- let date = new Date(value);
66
- if (isToday(date)) return 'Today';
67
- if (isYesterday(date)) return 'Yesterday';
68
- return format(new Date(value), 'MMM dd, yyyy');
69
- };
70
-
71
- interface ISubscriptionHandlerProps {
72
- subscribeToNewMessages: () => any;
73
- channelId: string;
74
- }
75
-
76
- interface IMessageProps extends IMessage {
77
- type: string;
78
- propsConfiguration?: any;
79
- replies?: any;
80
- isShowThreadMessage?: boolean;
81
- }
82
-
83
- export interface AlertMessageAttachmentsInterface {
84
- title: string;
85
- isTitleHtml: boolean;
86
- icon: string;
87
- callToAction: {
88
- title: string;
89
- link: string;
90
- };
91
- }
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
-
317
- const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMessage, ...rest }: any) => {
318
- const [channelToTop, setChannelToTop] = useState(0);
319
-
320
- // Create a ref to track if component is mounted
321
- const isMountedRef = useRef(true);
322
-
323
- // Use our safer custom implementation instead of the problematic useMachine
324
- const [state, send] = useSafeMachine(conversationXstate);
325
-
326
- // Define safe functions first to avoid "used before declaration" errors
327
- const safeContext = useCallback(() => {
328
- try {
329
- return state?.context || {};
330
- } catch (error) {
331
- console.error('Error accessing state.context:', error);
332
- return {};
333
- }
334
- }, [state]);
335
-
336
- const safeContextProperty = useCallback(
337
- (property, defaultValue = null) => {
338
- try {
339
- return state?.context?.[property] ?? defaultValue;
340
- } catch (error) {
341
- console.error(`Error accessing state.context.${property}:`, error);
342
- return defaultValue;
343
- }
344
- },
345
- [state],
346
- );
347
-
348
- const safeMatches = useCallback(
349
- (stateValue) => {
350
- try {
351
- return state?.matches?.(stateValue) || false;
352
- } catch (error) {
353
- console.error(`Error calling state.matches with ${stateValue}:`, error);
354
- return false;
355
- }
356
- },
357
- [state],
358
- );
359
-
360
- const safeSend = useCallback(
361
- (event) => {
362
- try {
363
- send(event);
364
- } catch (error) {
365
- console.error('Error sending event to state machine:', error, event);
366
- }
367
- },
368
- [send],
369
- );
370
-
371
- // Immediately set initial context if needed
372
- useEffect(() => {
373
- if (ChannelId) {
374
- console.log('Setting initial channel ID on mount:', ChannelId);
375
- try {
376
- send({
377
- type: ConversationActions.INITIAL_CONTEXT,
378
- data: { channelId: ChannelId },
379
- });
380
- } catch (error) {
381
- console.error('Error sending initial context:', error);
382
- }
383
- }
384
- }, []);
385
-
386
- // Use a ref to track the current machine snapshot for safer access
387
- const stateRef = useRef(state);
388
-
389
- // Keep the ref updated with the latest snapshot
390
- useEffect(() => {
391
- stateRef.current = state;
392
- }, [state]);
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
-
423
- const auth: any = useSelector(userSelector);
424
- const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
425
- const navigation = useNavigation<any>();
426
- const [selectedImage, setImage] = useState<string>('');
427
- const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
428
- const [imageObject, setImageObject] = useState<any>({});
429
- const messageRootListRef = useRef<any>(null);
430
- const isFocused = useIsFocused();
431
-
432
- const [addDirectChannel] = useAddDirectChannelMutation();
433
- const { startUpload } = useUploadFilesNative();
434
- const [sendMsg] = useSendMessagesMutation();
435
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
436
-
437
- const {
438
- data,
439
- loading: messageLoading,
440
- refetch,
441
- fetchMore: fetchMoreMessages,
442
- subscribeToMore,
443
- }: any = useMessagesQuery({
444
- variables: {
445
- channelId: state.context.channelId?.toString(),
446
- parentId: null,
447
- limit: MESSAGES_PER_PAGE,
448
- skip: state.context.skip,
449
- },
450
- skip: !state.context.channelId,
451
- fetchPolicy: 'cache-and-network',
452
- nextFetchPolicy: 'cache-first',
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
- },
468
- });
469
-
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]);
811
-
812
- React.useEffect(() => {
813
- return () => {
814
- send({ type: ConversationActions.CLEAR_MESSAGES });
815
- };
816
- }, []);
817
-
818
- useFocusEffect(
819
- React.useCallback(() => {
820
- if (state.context.channelId) {
821
- send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: state.context.channelId } });
822
- }
823
- return () => {
824
- send({ type: ConversationActions.CLEAR_MESSAGES });
825
- };
826
- }, [state.context.channelId, isFocused]),
827
- );
828
-
829
- React.useEffect(() => {
830
- const currentChannelId = ChannelId || currentRoute?.params?.channelId;
831
- if (currentChannelId) {
832
- console.log('Setting initial channel ID:', currentChannelId);
833
- send({ type: ConversationActions.INITIAL_CONTEXT, data: { channelId: currentChannelId } });
834
- }
835
- }, [ChannelId, currentRoute]);
836
-
837
- React.useEffect(() => {
838
- if (state.context.selectedImage) {
839
- send({ type: ConversationActions.STOP_LOADING });
840
- }
841
- }, [state.context.selectedImage]);
842
-
843
- useEffect(() => {
844
- if (data?.messages?.data) {
845
- console.log('📩 QUERY DATA CHANGED - Messages received:', data.messages.data.length);
846
- const { data: messages, totalCount: messageTotalCount } = data.messages;
847
-
848
- if (messages && messages.length > 0) {
849
- console.log('📩 QUERY DATA - Setting channel messages, count:', messages.length);
850
-
851
- // First try dispatching the update through XState
852
- send({
853
- type: ConversationActions.SET_CHANNEL_MESSAGES,
854
- data: {
855
- messages: uniqBy([...messages, ...state.context.channelMessages], ({ id }) => id),
856
- totalCount: messageTotalCount,
857
- },
858
- });
859
-
860
- // Debug: Log the first message to verify data format
861
- if (messages[0]) {
862
- const sample = messages[0];
863
- console.log(
864
- '📩 SAMPLE MESSAGE:',
865
- JSON.stringify({
866
- id: sample.id,
867
- message: sample.message,
868
- author: {
869
- id: sample.author?.id,
870
- name: `${sample.author?.givenName} ${sample.author?.familyName}`,
871
- },
872
- createdAt: sample.createdAt,
873
- }),
874
- );
875
- }
876
-
877
- // Check if the state machine actually updated (debug only)
878
- setTimeout(() => {
879
- if (state.context.channelMessages?.length === 0) {
880
- console.warn('⚠️ STATE NOT UPDATED after message data received - may need fallback');
881
- }
882
- }, 500);
883
- }
884
- }
885
- }, [data]);
886
-
887
- // Optimize onFetchOld by adding debounce logic
888
- const onFetchOld = useCallback(() => {
889
- // Prevent multiple rapid calls
890
- if (fetchOldDebounceRef.current) return;
891
-
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);
907
- }
908
- }, [state?.context?.totalCount, state?.context?.channelMessages, state?.context?.loadingOldMessages]);
909
-
910
- // Add debounce ref
911
- const fetchOldDebounceRef = useRef(false);
912
-
913
- const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
914
- const paddingToTop = 60;
915
- return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
916
- };
917
-
918
- const dataURLtoFile = (dataurl: any, filename: any) => {
919
- var arr = dataurl.split(','),
920
- mime = arr[0].match(/:(.*?);/)[1],
921
- bstr = atob(arr[1]),
922
- n = bstr.length,
923
- u8arr = new Uint8Array(n);
924
- while (n--) {
925
- u8arr[n] = bstr.charCodeAt(n);
926
- }
927
- return new File([u8arr], filename, { type: mime });
928
- };
929
-
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;
936
-
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
- );
960
- },
961
- [state?.context?.channelId, state?.context?.images, rest?.isCreateNewChannel],
962
- );
963
-
964
- // Fix the handleSend function to properly handle image-only messages
965
- const handleSend = useCallback(
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);
971
-
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
- }
977
-
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;
981
-
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 });
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 });
1002
- } else {
1003
- console.log('Sending text-only message');
1004
- safeSend({ type: ConversationActions.SEND_MESSAGE });
1005
- }
1006
- },
1007
- [state.context.channelId, state.context.images, rest?.isCreateNewChannel, rest?.newChannelData?.type, safeSend],
1008
- );
1009
-
1010
- // Update fetchMessagesWithFallback to not use fallback state
1011
- const fetchMessagesWithFallback = useCallback(async () => {
1012
- if (!state.context.channelId) return;
1013
-
1014
- try {
1015
- console.log('🔄 DIRECT FETCH: Using direct approach for channel:', state.context.channelId);
1016
-
1017
- const response = await refetch({
1018
- channelId: state.context.channelId?.toString(),
1019
- parentId: null,
1020
- limit: MESSAGES_PER_PAGE,
1021
- skip: 0,
1022
- });
1023
-
1024
- if (response?.data?.messages?.data) {
1025
- const messages = response.data.messages.data;
1026
- console.log('✅ DIRECT FETCH: Got messages:', messages.length);
1027
-
1028
- // Skip fallback and send directly to state machine
1029
- send({
1030
- type: ConversationActions.SET_CHANNEL_MESSAGES,
1031
- data: {
1032
- messages,
1033
- totalCount: response.data.messages.totalCount,
1034
- },
1035
- });
1036
- }
1037
- } catch (error) {
1038
- console.error('❌ DIRECT FETCH ERROR:', error);
1039
- }
1040
- }, [state.context.channelId, refetch]);
1041
-
1042
- // Auto-trigger fallback if needed
1043
- useEffect(() => {
1044
- let timeoutId: NodeJS.Timeout;
1045
-
1046
- if (state.context.channelId && state.context.channelMessages.length === 0) {
1047
- timeoutId = setTimeout(() => {
1048
- console.log('⚠️ ACTIVATING FALLBACK - XState not updating after timeout');
1049
- fetchMessagesWithFallback();
1050
- }, 3000); // Wait 3 seconds for normal flow to work
1051
- }
1052
-
1053
- return () => {
1054
- if (timeoutId) clearTimeout(timeoutId);
1055
- };
1056
- }, [state.context.channelId, state.context.channelMessages, fetchMessagesWithFallback]);
1057
-
1058
- // Optimize the messageList calculation for better performance
1059
- const messageList = useMemo(() => {
1060
- // Only recalculate when these dependencies change
1061
- console.log('🔄 CALCULATING MESSAGE LIST - Optimized version');
1062
-
1063
- // Short-circuit if no messages to process
1064
- if (!state?.context?.channelMessages || state.context.channelMessages.length === 0) {
1065
- console.log('No messages to process');
1066
- return [];
1067
- }
1068
-
1069
- // 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',
1078
- }),
1079
- );
1080
- }
1081
-
1082
- // Use a more efficient approach - pre-filter messages once
1083
- const filteredMessages = uniqBy(state.context.channelMessages, ({ id }) => id);
1084
-
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);
1093
-
1094
- // Extract image URL from files data
1095
- let imageUrl = null;
1096
- if (msg.files?.data && msg.files.data.length > 0) {
1097
- const fileData = msg.files.data[0];
1098
- if (fileData && fileData.url) {
1099
- imageUrl = fileData.url;
1100
- console.log('📷 Found image URL for message', msg.id, ':', imageUrl);
1101
- }
1102
- }
1103
-
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
- });
1195
- }}
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>
1240
- </HStack>
1241
- )}
1242
- </>
1243
- </TouchableHighlight>
1244
- )}
1245
- </>
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
- );
1309
-
1310
- const renderActions = (props) => {
1311
- return (
1312
- <Actions
1313
- {...props}
1314
- options={{
1315
- ['Choose from Library']: onSelectImages,
1316
- ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1317
- }}
1318
- optionTintColor="#000000"
1319
- cancelButtonIndex={1} // Set the Cancel option as the cancel button
1320
- icon={() => (
1321
- <Box
1322
- style={{
1323
- width: 32,
1324
- height: 32,
1325
- alignItems: 'center',
1326
- justifyContent: 'center',
1327
- }}
1328
- >
1329
- <Ionicons name="image" size={24} color={colors.blue[500]} />
1330
- </Box>
1331
- )}
1332
- containerStyle={{
1333
- alignItems: 'center',
1334
- justifyContent: 'center',
1335
- marginLeft: 8,
1336
- marginBottom: 0,
1337
- }}
1338
- />
1339
- );
1340
- };
1341
-
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
- >
1379
- <Image
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
- }}
1389
- size={'xs'}
1390
- />
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,
1406
- }}
1407
- >
1408
- <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1409
- </TouchableHighlight>
1410
- </View>
1411
- </View>
1412
- );
1413
- },
1414
- [state?.context?.selectedImage, safeSend],
1415
- );
1416
-
1417
- const setImageViewerObject = (obj: any, v: boolean) => {
1418
- setImageObject(obj);
1419
- setImageViewer(v);
1420
- };
1421
-
1422
- const modalContent = React.useMemo(() => {
1423
- if (!imageObject) return <></>;
1424
- const { image, _id } = imageObject;
1425
- return (
1426
- <CachedImage
1427
- style={{ width: '100%', height: '100%' }}
1428
- resizeMode={'cover'}
1429
- cacheKey={`${_id}-slack-bubble-imageKey`}
1430
- source={{
1431
- uri: image,
1432
- expiresIn: 86400,
1433
- }}
1434
- alt={'image'}
1435
- />
1436
- );
1437
- }, [imageObject]);
1438
-
1439
- const renderMessage = useCallback(
1440
- (props: any) => {
1441
- // Use memo to prevent unnecessary re-renders of each message
1442
- return (
1443
- <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1444
- );
1445
- },
1446
- [isShowImageViewer],
1447
- );
1448
-
1449
- let onScroll = false;
1450
-
1451
- // Optimize onMomentumScrollBegin for better scroll performance
1452
- const onMomentumScrollBegin = async ({ nativeEvent }: any) => {
1453
- // Set scroll state
1454
- onScroll = true;
1455
-
1456
- // Use the debounced fetch function to prevent excessive calls
1457
- if (isCloseToTop(nativeEvent)) {
1458
- onFetchOld();
1459
- }
1460
- };
1461
-
1462
- const onEndReached = () => {
1463
- console.log('on end reached');
1464
- if (!onScroll) return;
1465
- onScroll = false;
1466
- };
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
1688
- return (
1689
- <View
1690
- style={{
1691
- flex: 1,
1692
- backgroundColor: 'white',
1693
- }}
1694
- >
1695
- {state?.matches && state.matches(BaseState.FetchMessages) && <Spinner color={'#3b82f6'} />}
1696
-
1697
- <GiftedChat
1698
- ref={messageRootListRef}
1699
- wrapInSafeArea={true}
1700
- renderLoading={() => <Spinner color={'#3b82f6'} />}
1701
- messages={messageList}
1702
- listViewProps={{
1703
- ...listViewProps,
1704
- contentContainerStyle: {
1705
- paddingBottom: 10,
1706
- },
1707
- keyboardShouldPersistTaps: 'handled',
1708
- }}
1709
- onSend={handleSend}
1710
- text={safeContextProperty('messageText', ' ') || ' '}
1711
- onInputTextChanged={(text) =>
1712
- safeSend({ type: ConversationActions.SET_MESSAGE_TEXT, data: { messageText: text } })
1713
- }
1714
- renderFooter={() =>
1715
- safeContextProperty('loading') ? (
1716
- <Spinner color={'#3b82f6'} />
1717
- ) : safeContextProperty('imageLoading') ? (
1718
- <Spinner color={'#3b82f6'} />
1719
- ) : (
1720
- ''
1721
- )
1722
- }
1723
- scrollToBottom
1724
- user={{
1725
- _id: auth?.id || '',
1726
- }}
1727
- isTyping={false} // Setting to false to reduce animations
1728
- alwaysShowSend={true} // Always show send button regardless of text content
1729
- renderSend={renderSend}
1730
- renderMessageText={renderMessageText}
1731
- renderInputToolbar={renderInputToolbar}
1732
- minInputToolbarHeight={50}
1733
- renderActions={safeContextProperty('channelId') && renderActions}
1734
- renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
1735
- renderMessage={renderMessage}
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..."
1766
- lightboxProps={{
1767
- underlayColor: 'transparent',
1768
- springConfig: { tension: 90000, friction: 90000 },
1769
- disabled: true,
1770
- }}
1771
- infiniteScroll={false} // Disable automatic loading
1772
- />
1773
- </View>
1774
- );
1775
- };
1776
-
1777
- const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptionHandlerProps) => {
1778
- useEffect(() => subscribeToNewMessages(), [channelId]);
1779
- return <></>;
1780
- };
1781
-
1782
- export const ConversationView = React.memo(ConversationViewComponent);