@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,2312 +0,0 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import {
3
- Center,
4
- Avatar,
5
- AvatarFallbackText,
6
- AvatarImage,
7
- Box,
8
- Button,
9
- ButtonText,
10
- VStack,
11
- HStack,
12
- Icon,
13
- Image,
14
- Spinner,
15
- Text,
16
- } from '@admin-layout/gluestack-ui-mobile';
17
- import { Platform, Linking, SafeAreaView, View, TouchableHighlight, Alert } from 'react-native';
18
- import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
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, MaterialIcons } from '@expo/vector-icons';
24
- import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
25
- import { IPost, IPostThread, PreDefinedRole, IExpoNotificationData, IFileInfo } from 'common';
26
- import {
27
- OnThreadChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
28
- useCreatePostThreadMutation,
29
- useOnThreadChatMessageAddedSubscription,
30
- useSendExpoNotificationOnPostMutation,
31
- useSendThreadMessageMutation,
32
- useThreadMessagesLazyQuery,
33
- useGetPostThreadLazyQuery,
34
- } from 'common/graphql';
35
- import { useUploadFilesNative } from '@messenger-box/platform-client';
36
- import { objectId } from '@messenger-box/core';
37
- import { format, isToday, isYesterday } from 'date-fns';
38
- import { userSelector } from '@adminide-stack/user-auth0-client';
39
- import { config } from '../config';
40
- import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
41
- import CachedImage from '../components/CachedImage';
42
- import colors from 'tailwindcss/colors';
43
- import {
44
- threadConversationXstate,
45
- Actions as ThreadActions,
46
- BaseState,
47
- MainState,
48
- } from './workflow/thread-conversation-xstate';
49
-
50
- // Define an extended interface for ImagePickerAsset with url property
51
- interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
52
- url?: string;
53
- fileName?: string;
54
- mimeType?: string;
55
- }
56
-
57
- const {
58
- MESSAGES_PER_PAGE,
59
- CALL_TO_ACTION_BOX_BGCOLOR,
60
- CALL_TO_ACTION_PATH,
61
- CALL_TO_ACTION_BUTTON_BORDERCOLOR,
62
- CALL_TO_ACTION_TEXT_COLOR,
63
- } = config;
64
-
65
- const createdAtText = (value: string) => {
66
- if (!value) return '';
67
-
68
- try {
69
- // Validate the date before processing
70
- const timestamp = new Date(value).getTime();
71
- if (isNaN(timestamp)) {
72
- console.warn(`Invalid date value in createdAtText: ${value}`);
73
- return 'Unknown date';
74
- }
75
-
76
- let date = new Date(value);
77
- if (isToday(date)) return 'Today';
78
- if (isYesterday(date)) return 'Yesterday';
79
- return format(date, 'MMM dd, yyyy');
80
- } catch (error) {
81
- console.error(`Error processing date in createdAtText: ${value}`, error);
82
- return 'Unknown date';
83
- }
84
- };
85
-
86
- // Create a safer version of useMachine to handle potential errors
87
- function useSafeMachine(machine) {
88
- // Define the state type
89
- interface SafeStateType {
90
- context: {
91
- channelId: any;
92
- postParentId: any;
93
- role: any;
94
- threadMessages: any[];
95
- totalCount: number;
96
- skip: number;
97
- loading: boolean;
98
- loadingOldMessages: boolean;
99
- error: any;
100
- selectedImage: string;
101
- files: any[];
102
- images: any[];
103
- messageText: string;
104
- imageLoading: boolean;
105
- postThread: any;
106
- threadPost: any[];
107
- isScrollToBottom: boolean;
108
- };
109
- value: string;
110
- matches?: (stateValue: string) => boolean;
111
- }
112
-
113
- // Initialize with default state
114
- const [state, setState] = useState<SafeStateType>({
115
- context: {
116
- channelId: null,
117
- postParentId: null,
118
- role: null,
119
- threadMessages: [],
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
- postThread: null,
131
- threadPost: [],
132
- isScrollToBottom: false,
133
- },
134
- value: 'idle',
135
- });
136
-
137
- // Create a safe send function
138
- const send = useCallback((event) => {
139
- try {
140
- // Log event for debugging
141
- console.log('Thread Event received:', event.type);
142
-
143
- // Handle specific events manually
144
- if (event.type === ThreadActions.INITIAL_CONTEXT) {
145
- setState((prev) => ({
146
- ...prev,
147
- context: {
148
- ...prev.context,
149
- channelId: event.data?.channelId || null,
150
- postParentId: event.data?.postParentId || null,
151
- role: event.data?.role || null,
152
- },
153
- value: BaseState.FetchThreadMessages,
154
- }));
155
- } else if (event.type === ThreadActions.SET_THREAD_MESSAGES) {
156
- setState((prev) => ({
157
- ...prev,
158
- context: {
159
- ...prev.context,
160
- threadMessages: event.data?.messages || [],
161
- totalCount: event.data?.totalCount || 0,
162
- loading: false,
163
- loadingOldMessages: false,
164
- threadPost: event.data?.threadPost || [],
165
- postThread: event.data?.postThread || null,
166
- },
167
- value: 'active',
168
- }));
169
- } else if (event.type === ThreadActions.CLEAR_MESSAGES) {
170
- setState((prev) => ({
171
- ...prev,
172
- context: {
173
- ...prev.context,
174
- threadMessages: [],
175
- totalCount: 0,
176
- },
177
- }));
178
- } else if (event.type === ThreadActions.SET_MESSAGE_TEXT) {
179
- setState((prev) => ({
180
- ...prev,
181
- context: {
182
- ...prev.context,
183
- messageText: event.data?.messageText || '',
184
- },
185
- }));
186
- } else if (event.type === ThreadActions.FETCH_MORE_MESSAGES) {
187
- setState((prev) => ({
188
- ...prev,
189
- context: {
190
- ...prev.context,
191
- loadingOldMessages: true,
192
- },
193
- value: MainState.FetchMoreMessages,
194
- }));
195
- } else if (event.type === ThreadActions.SET_IMAGE) {
196
- setState((prev) => ({
197
- ...prev,
198
- context: {
199
- ...prev.context,
200
- selectedImage: event.data?.image || '',
201
- images: event.data?.images || [],
202
- files: event.data?.files || [],
203
- imageLoading: false,
204
- },
205
- }));
206
- } else if (event.type === ThreadActions.CLEAR_IMAGE) {
207
- setState((prev) => ({
208
- ...prev,
209
- context: {
210
- ...prev.context,
211
- selectedImage: '',
212
- images: [],
213
- files: [],
214
- },
215
- }));
216
- } else if (event.type === ThreadActions.START_LOADING) {
217
- setState((prev) => ({
218
- ...prev,
219
- context: {
220
- ...prev.context,
221
- loading: true,
222
- },
223
- }));
224
- } else if (event.type === ThreadActions.STOP_LOADING) {
225
- setState((prev) => ({
226
- ...prev,
227
- context: {
228
- ...prev.context,
229
- loading: false,
230
- loadingOldMessages:
231
- event.data?.loadingOldMessages === false ? false : prev.context.loadingOldMessages,
232
- },
233
- }));
234
- } else if (event.type === ThreadActions.SEND_THREAD_MESSAGE) {
235
- console.log('Sending message event with text:', event.data?.messageText);
236
- setState((prev) => ({
237
- ...prev,
238
- context: {
239
- ...prev.context,
240
- loading: true,
241
- // Keep the message text until we're done sending
242
- messageText: event.data?.messageText || prev.context.messageText,
243
- },
244
- value: MainState.SendThreadMessage,
245
- }));
246
- } else if (event.type === ThreadActions.SEND_THREAD_MESSAGE_WITH_FILE) {
247
- console.log('Sending message with file event, text:', event.data?.messageText);
248
- setState((prev) => ({
249
- ...prev,
250
- context: {
251
- ...prev.context,
252
- loading: true,
253
- // Keep the message text until we're done sending
254
- messageText: event.data?.messageText || prev.context.messageText,
255
- },
256
- value: MainState.SendThreadMessageWithFile,
257
- }));
258
- } else if (
259
- event.type === 'SEND_THREAD_MESSAGE_SUCCESS' ||
260
- event.type === 'SEND_THREAD_MESSAGE_WITH_FILE_SUCCESS'
261
- ) {
262
- console.log('Handling send success event:', event.type, 'with message:', event.data?.message?.id);
263
- setState((prev) => {
264
- // Make sure we have the message data
265
- if (!event.data?.message) {
266
- console.warn('Send success event without message data');
267
- return {
268
- ...prev,
269
- context: {
270
- ...prev.context,
271
- loading: false,
272
- messageText: '', // Clear input
273
- images: [],
274
- selectedImage: '',
275
- files: [],
276
- },
277
- value: 'active',
278
- };
279
- }
280
-
281
- // Add the new message to our threadMessages
282
- const newMessage = event.data.message;
283
- const updatedMessages = [newMessage, ...prev.context.threadMessages];
284
-
285
- console.log('Updated thread messages list after send, now has', updatedMessages.length, 'messages');
286
-
287
- return {
288
- ...prev,
289
- context: {
290
- ...prev.context,
291
- loading: false,
292
- messageText: '', // Always clear input text after sending
293
- images: [],
294
- selectedImage: '',
295
- files: [],
296
- threadMessages: updatedMessages,
297
- totalCount: prev.context.totalCount + 1,
298
- },
299
- value: 'active',
300
- };
301
- });
302
- } else if (event.type === 'FETCH_MORE_MESSAGES_SUCCESS') {
303
- setState((prev) => {
304
- const newMessages = event.data?.messages || [];
305
- const apiTotalCount = event.data?.totalCount;
306
- console.log(
307
- `Merging ${newMessages.length} older messages with ${prev.context.threadMessages.length} existing messages`,
308
- );
309
-
310
- // Debug: Log the dates of the messages for troubleshooting
311
- if (newMessages.length > 0) {
312
- try {
313
- console.log(
314
- 'First new message date:',
315
- !isNaN(new Date(newMessages[0].createdAt).getTime())
316
- ? new Date(newMessages[0].createdAt).toISOString()
317
- : 'Invalid date',
318
- );
319
- console.log(
320
- 'Last new message date:',
321
- !isNaN(new Date(newMessages[newMessages.length - 1].createdAt).getTime())
322
- ? new Date(newMessages[newMessages.length - 1].createdAt).toISOString()
323
- : 'Invalid date',
324
- );
325
- } catch (error) {
326
- console.error('Error logging new message dates:', error);
327
- }
328
- }
329
-
330
- if (prev.context.threadMessages.length > 0) {
331
- try {
332
- console.log(
333
- 'First existing message date:',
334
- !isNaN(new Date(prev.context.threadMessages[0].createdAt).getTime())
335
- ? new Date(prev.context.threadMessages[0].createdAt).toISOString()
336
- : 'Invalid date',
337
- );
338
- console.log(
339
- 'Last existing message date:',
340
- !isNaN(
341
- new Date(
342
- prev.context.threadMessages[prev.context.threadMessages.length - 1].createdAt,
343
- ).getTime(),
344
- )
345
- ? new Date(
346
- prev.context.threadMessages[prev.context.threadMessages.length - 1].createdAt,
347
- ).toISOString()
348
- : 'Invalid date',
349
- );
350
- } catch (error) {
351
- console.error('Error logging existing message dates:', error);
352
- }
353
- }
354
-
355
- // If no new messages returned but total count says there should be more
356
- if (newMessages.length === 0 && prev.context.totalCount > prev.context.threadMessages.length) {
357
- console.log('No new messages found despite totalCount indicating more should exist');
358
-
359
- // Adjust totalCount to match reality
360
- return {
361
- ...prev,
362
- context: {
363
- ...prev.context,
364
- loadingOldMessages: false,
365
- totalCount: prev.context.threadMessages.length,
366
- },
367
- value: 'active',
368
- };
369
- }
370
-
371
- // Make sure we don't add duplicate messages
372
- const combinedMessages = uniqBy([...prev.context.threadMessages, ...newMessages], 'id');
373
-
374
- // GiftedChat expects messages sorted by date (newest first)
375
- const sortedMessages = orderBy(
376
- combinedMessages,
377
- [
378
- (msg) => {
379
- try {
380
- // Safely access and convert date
381
- return !isNaN(new Date(msg.createdAt).getTime())
382
- ? new Date(msg.createdAt).getTime()
383
- : 0; // Default to oldest if invalid
384
- } catch (error) {
385
- console.error(`Error sorting message by date: ${msg.id}`, error);
386
- return 0; // Default to oldest if error
387
- }
388
- },
389
- ],
390
- ['desc'],
391
- );
392
-
393
- // Use the total count from the API response if available, otherwise keep the current count
394
- const newTotalCount =
395
- typeof apiTotalCount === 'number'
396
- ? apiTotalCount
397
- : Math.max(sortedMessages.length, prev.context.totalCount);
398
-
399
- console.log(
400
- `Total messages after merge and sort: ${sortedMessages.length}, totalCount: ${newTotalCount}`,
401
- );
402
-
403
- return {
404
- ...prev,
405
- context: {
406
- ...prev.context,
407
- loadingOldMessages: false,
408
- threadMessages: sortedMessages,
409
- totalCount: newTotalCount,
410
- },
411
- value: 'active',
412
- };
413
- });
414
- } else if (event.type === 'ERROR') {
415
- setState((prev) => ({
416
- ...prev,
417
- context: {
418
- ...prev.context,
419
- loading: false,
420
- loadingOldMessages: false,
421
- error: event.data?.message || 'Unknown error',
422
- },
423
- value: 'error',
424
- }));
425
- }
426
- } catch (error) {
427
- console.error('Error in thread conversation send function:', error);
428
- }
429
- }, []);
430
-
431
- // Add a custom matches function to the state
432
- const stateWithMatches = useMemo(() => {
433
- return {
434
- ...state,
435
- matches: (checkState) => {
436
- return state.value === checkState;
437
- },
438
- };
439
- }, [state]);
440
-
441
- // Return as a tuple to match useMachine API
442
- return [stateWithMatches, send] as const;
443
- }
444
-
445
- interface IMessageProps extends IMessage {
446
- type: string;
447
- propsConfiguration?: any;
448
- }
449
-
450
- export interface AlertMessageAttachmentsInterface {
451
- title: string;
452
- isTitleHtml: boolean;
453
- icon: string;
454
- callToAction: {
455
- title: string;
456
- link: string;
457
- };
458
- }
459
-
460
- interface IThreadSubscriptionHandlerProps {
461
- subscribeToNewMessages: () => any;
462
- channelId: string;
463
- }
464
-
465
- const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any): JSX.Element => {
466
- const { params } = useRoute<any>();
467
- const [channelToTop, setChannelToTop] = useState(0);
468
-
469
- // Create a ref to track if component is mounted
470
- const isMountedRef = useRef(true);
471
-
472
- // Use our safer custom implementation instead of the problematic useMachine
473
- const [state, send] = useSafeMachine(threadConversationXstate);
474
-
475
- // Define safe functions first to avoid "used before declaration" errors
476
- const safeContext = useCallback(() => {
477
- try {
478
- return state?.context || {};
479
- } catch (error) {
480
- console.error('Error accessing state.context:', error);
481
- return {};
482
- }
483
- }, [state]);
484
-
485
- const safeContextProperty = useCallback(
486
- (property, defaultValue = null) => {
487
- try {
488
- return state?.context?.[property] ?? defaultValue;
489
- } catch (error) {
490
- console.error(`Error accessing state.context.${property}:`, error);
491
- return defaultValue;
492
- }
493
- },
494
- [state],
495
- );
496
-
497
- const safeMatches = useCallback(
498
- (stateValue) => {
499
- try {
500
- return state?.matches?.(stateValue) || false;
501
- } catch (error) {
502
- console.error(`Error calling state.matches with ${stateValue}:`, error);
503
- return false;
504
- }
505
- },
506
- [state],
507
- );
508
-
509
- const safeSend = useCallback(
510
- (event) => {
511
- try {
512
- send(event);
513
- } catch (error) {
514
- console.error('Error sending event to state machine:', error, event);
515
- }
516
- },
517
- [send],
518
- );
519
-
520
- // Use a ref to track the current machine snapshot for safer access
521
- const stateRef = useRef(state);
522
-
523
- // Keep the ref updated with the latest snapshot
524
- useEffect(() => {
525
- stateRef.current = state;
526
- }, [state]);
527
-
528
- const auth: any = useSelector(userSelector);
529
- const [selectedImage, setImage] = useState<string>('');
530
- const navigation = useNavigation<any>();
531
- const [files, setFiles] = useState<File[]>([]);
532
- const [images, setImages] = useState<ImagePicker.ImagePickerAsset[]>([]);
533
- const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
534
- const [imageObject, setImageObject] = useState<any>({});
535
- const [parentId, setParentId] = useState<any>(postParentId);
536
- const [expoTokens, setExpoTokens] = useState<any[]>([]);
537
- const threadMessageListRef = useRef<any>(null);
538
-
539
- // const [sendThreadMessage] = useSendThreadMessageMutation();
540
- const [sendThreadMessage] = useCreatePostThreadMutation();
541
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
542
- const { startUpload } = useUploadFilesNative();
543
-
544
- const [
545
- getThreadMessages,
546
- { data, loading: threadLoading, fetchMore: fetchMoreMessages, refetch: refetchThreadMessages, subscribeToMore },
547
- ] = useGetPostThreadLazyQuery({ fetchPolicy: 'cache-and-network' });
548
-
549
- // Add a function to force refresh all messages
550
- const forceRefreshMessages = useCallback(() => {
551
- console.log('Force refreshing all messages');
552
-
553
- // Clear the current messages first
554
- safeSend({
555
- type: ThreadActions.CLEAR_MESSAGES,
556
- });
557
-
558
- // Then trigger a refetch
559
- if (channelId && parentId) {
560
- safeSend({
561
- type: ThreadActions.START_LOADING,
562
- data: { loading: true },
563
- });
564
-
565
- getThreadMessages({
566
- variables: {
567
- channelId: channelId?.toString(),
568
- role: role?.toString(),
569
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
570
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
571
- limit: 50, // Use larger limit that proved to work
572
- },
573
- })
574
- .then(({ data }) => {
575
- if (data?.getPostThread) {
576
- const threads: any = data.getPostThread;
577
- const threadPost = threads?.post ?? [];
578
- const threadReplies = threads?.replies ?? [];
579
- const messageTotalCount = threads?.replyCount ?? 0;
580
- const messages = [...threadReplies];
581
-
582
- console.log(
583
- `Force refresh complete. Got ${messages.length} messages of ${messageTotalCount} total`,
584
- );
585
-
586
- safeSend({
587
- type: ThreadActions.SET_THREAD_MESSAGES,
588
- data: {
589
- messages,
590
- totalCount: messageTotalCount,
591
- threadPost: threadPost,
592
- postThread: threads,
593
- },
594
- });
595
- }
596
- })
597
- .catch((error) => {
598
- console.error('Error during force refresh:', error);
599
- safeSend({
600
- type: 'ERROR',
601
- data: { message: error.message },
602
- });
603
- });
604
- }
605
- }, [channelId, parentId, getThreadMessages, safeSend]);
606
-
607
- useFocusEffect(
608
- React.useCallback(() => {
609
- // navigation?.setOptions({ title: params?.title ?? 'Thread' });
610
-
611
- navigation.setOptions({
612
- title: params?.title ?? 'Thread',
613
- headerLeft: (props: any) => (
614
- <Button className="bg-transparent active:bg-gray-200 " onPress={() => navigation.goBack()}>
615
- <MaterialIcons size={20} name="arrow-back-ios" color={'black'} />
616
- </Button>
617
- ),
618
- headerRight: () => (
619
- <Button className="bg-transparent active:bg-gray-200 mr-2" onPress={forceRefreshMessages}>
620
- <MaterialIcons size={20} name="refresh" color={'black'} />
621
- </Button>
622
- ),
623
- });
624
-
625
- // Set initial context when focused
626
- if (channelId && postParentId) {
627
- safeSend({
628
- type: ThreadActions.INITIAL_CONTEXT,
629
- data: {
630
- channelId,
631
- postParentId,
632
- role,
633
- },
634
- });
635
- }
636
-
637
- setParentId(postParentId);
638
-
639
- return () => {
640
- safeSend({ type: ThreadActions.CLEAR_MESSAGES });
641
- };
642
- }, [postParentId, forceRefreshMessages]),
643
- );
644
-
645
- // Effect for when in FetchThreadMessages state
646
- useEffect(() => {
647
- if (safeMatches(BaseState.FetchThreadMessages)) {
648
- fetchThreadMessages();
649
- }
650
- }, [state.value]);
651
-
652
- // Effect for when in FetchMoreMessages state
653
- useEffect(() => {
654
- if (safeMatches(MainState.FetchMoreMessages)) {
655
- onFetchOld();
656
- }
657
- }, [state.value]);
658
-
659
- // Effect for when in SendThreadMessage state
660
- useEffect(() => {
661
- if (safeMatches(MainState.SendThreadMessage)) {
662
- const messageText = safeContextProperty('messageText', '');
663
- console.log('Sending message from state transition, text:', messageText);
664
- sendThreadMessageHandler(messageText);
665
- }
666
- }, [state.value]);
667
-
668
- // Effect for when in SendThreadMessageWithFile state
669
- useEffect(() => {
670
- if (safeMatches(MainState.SendThreadMessageWithFile)) {
671
- const messageText = safeContextProperty('messageText', '');
672
- const images = safeContextProperty('images', []);
673
- sendThreadMessageWithFileHandler(messageText, images);
674
- }
675
- }, [state.value]);
676
-
677
- // Fetch thread messages function
678
- const fetchThreadMessages = useCallback(() => {
679
- if (channelId && parentId) {
680
- console.log('Initial fetch of thread messages using larger limit (50)');
681
- getThreadMessages({
682
- variables: {
683
- channelId: channelId?.toString(),
684
- role: role?.toString(),
685
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
686
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
687
- limit: 50, // Use larger limit that proved to work
688
- },
689
- })
690
- .then(({ data }) => {
691
- if (data?.getPostThread) {
692
- const threads: any = data.getPostThread;
693
- const threadPost = threads?.post ?? [];
694
- const threadReplies = threads?.replies ?? [];
695
- const messageTotalCount = threads?.replyCount ?? 0;
696
- const messages = [...threadReplies];
697
-
698
- console.log(
699
- `Initial fetch complete. Got ${messages.length} messages of ${messageTotalCount} total`,
700
- );
701
-
702
- safeSend({
703
- type: ThreadActions.SET_THREAD_MESSAGES,
704
- data: {
705
- messages,
706
- totalCount: messageTotalCount,
707
- threadPost: threadPost,
708
- postThread: threads,
709
- },
710
- });
711
- }
712
- })
713
- .catch((error) => {
714
- safeSend({
715
- type: 'ERROR',
716
- data: { message: error.message },
717
- });
718
- });
719
- }
720
- }, [channelId, parentId, role]);
721
-
722
- React.useEffect(() => {
723
- if (data?.getPostThread) {
724
- const threads: any = data.getPostThread;
725
- const threadPost = threads?.post ?? [];
726
- const threadReplies = threads?.replies ?? [];
727
- const messeageTotalCount = threads?.replyCount ?? 0;
728
- const messages = [...threadReplies];
729
-
730
- safeSend({
731
- type: ThreadActions.SET_THREAD_MESSAGES,
732
- data: {
733
- messages,
734
- totalCount: messeageTotalCount,
735
- threadPost: threadPost,
736
- postThread: threads,
737
- },
738
- });
739
- }
740
- }, [data]);
741
-
742
- React.useEffect(() => {
743
- if (safeContextProperty('selectedImage')) {
744
- safeSend({ type: ThreadActions.STOP_LOADING });
745
- }
746
- }, [safeContextProperty('selectedImage')]);
747
-
748
- // Add a safety timeout to clear loading state if it gets stuck
749
- React.useEffect(() => {
750
- const isLoading = safeContextProperty('loadingOldMessages', false);
751
-
752
- if (isLoading) {
753
- console.log('Message loading timeout safety started');
754
- const timeoutId = setTimeout(() => {
755
- // Check if we're still loading after 10 seconds
756
- if (safeContextProperty('loadingOldMessages', false)) {
757
- console.log('Message loading timed out - resetting state');
758
- safeSend({
759
- type: ThreadActions.STOP_LOADING,
760
- data: { loadingOldMessages: false },
761
- });
762
- }
763
- }, 10000); // 10 second timeout
764
-
765
- return () => clearTimeout(timeoutId);
766
- }
767
- }, [safeContextProperty('loadingOldMessages')]);
768
-
769
- // Add a safety counter to detect and fix incorrect message counts automatically
770
- const failedLoadAttemptsRef = useRef(0);
771
-
772
- // Track failed attempts to load more messages
773
- const registerLoadAttemptResult = useCallback((success: boolean) => {
774
- if (!success) {
775
- failedLoadAttemptsRef.current += 1;
776
- console.log(`Failed load attempt registered, count: ${failedLoadAttemptsRef.current}`);
777
- } else {
778
- // Reset counter on successful load
779
- failedLoadAttemptsRef.current = 0;
780
- }
781
- }, []);
782
-
783
- // Watch for failed load attempts and auto-fix the count after 3 consecutive failures
784
- React.useEffect(() => {
785
- const isLoading = safeContextProperty('loadingOldMessages', false);
786
- const totalCount = safeContextProperty('totalCount', 0);
787
- const messagesCount = safeContextProperty('threadMessages', []).length;
788
-
789
- // If we're not loading and there's a discrepancy in message counts
790
- if (!isLoading && totalCount > messagesCount) {
791
- // If we've had 3 consecutive failed attempts, fix the count
792
- if (failedLoadAttemptsRef.current >= 2) {
793
- console.log(
794
- `Auto-fixing incorrect message count after ${
795
- failedLoadAttemptsRef.current + 1
796
- } failed load attempts`,
797
- );
798
- console.log(`Adjusting totalCount from ${totalCount} to ${messagesCount}`);
799
-
800
- safeSend({
801
- type: ThreadActions.SET_THREAD_MESSAGES,
802
- data: {
803
- messages: safeContextProperty('threadMessages', []),
804
- totalCount: messagesCount,
805
- threadPost: safeContextProperty('threadPost', []),
806
- postThread: safeContextProperty('postThread', null),
807
- },
808
- });
809
-
810
- // Reset the counter
811
- failedLoadAttemptsRef.current = 0;
812
- }
813
- }
814
- }, [safeContextProperty('loadingOldMessages'), safeContextProperty('threadMessages')]);
815
-
816
- const scrollToBottom = React.useCallback(() => {
817
- if (threadMessageListRef?.current) {
818
- threadMessageListRef.current.scrollToBottom();
819
- }
820
- }, [threadMessageListRef]);
821
-
822
- const onFetchOld = useCallback(() => {
823
- const totalCount = safeContextProperty('totalCount', 0);
824
- const threadMessages = safeContextProperty('threadMessages', []);
825
-
826
- if (totalCount > threadMessages.length && !safeContextProperty('loadingOldMessages', false)) {
827
- console.log('Loading more messages - current count:', threadMessages.length, 'of', totalCount);
828
-
829
- // Set the loading state specifically for old messages
830
- safeSend({
831
- type: ThreadActions.FETCH_MORE_MESSAGES,
832
- data: { loadingOldMessages: true },
833
- });
834
-
835
- // Since Skip=0, Limit=50 worked well, let's use that approach
836
- console.log('Using proven approach: Skip=0, Limit=50');
837
-
838
- // Try with a larger limit and skip=0, which we know works
839
- const queryVariables = {
840
- channelId: channelId?.toString(),
841
- role: role?.toString(),
842
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
843
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
844
- limit: 50, // Use larger limit that proved to work
845
- skip: 0, // Start from the beginning
846
- };
847
-
848
- console.log('Query variables:', JSON.stringify(queryVariables));
849
-
850
- fetchMoreMessages({
851
- variables: queryVariables,
852
- })
853
- .then((res: any) => {
854
- console.log('API response received:', JSON.stringify(res?.data, null, 2));
855
-
856
- if (res?.data?.getPostThread) {
857
- const threads: any = res?.data?.getPostThread;
858
- const threadReplies = threads?.replies ?? [];
859
- // Get the actual total count from the response
860
- const actualTotalCount = threads?.replyCount ?? 0;
861
-
862
- console.log('API response details:');
863
- console.log('- replyCount:', threads?.replyCount);
864
- console.log('- replies array length:', threadReplies.length);
865
- console.log(
866
- '- replies structure:',
867
- threadReplies.length > 0
868
- ? `First item has fields: ${Object.keys(threadReplies[0]).join(', ')}`
869
- : 'No items',
870
- );
871
-
872
- if (threadReplies.length > 0) {
873
- console.log('- Sample message:', JSON.stringify(threadReplies[0], null, 2));
874
- }
875
-
876
- console.log(
877
- 'Successfully loaded more messages:',
878
- threadReplies.length,
879
- 'of total:',
880
- actualTotalCount,
881
- );
882
- console.log('Thread reply IDs:', threadReplies.map((msg) => msg.id).join(', '));
883
-
884
- // Compare with our existing messages to find the ones we're missing
885
- const existingIds = new Set(threadMessages.map((msg) => msg.id));
886
- const newUniqueMessages = threadReplies.filter((msg) => !existingIds.has(msg.id));
887
-
888
- console.log(
889
- `Found ${newUniqueMessages.length} unique new messages out of ${threadReplies.length} received`,
890
- );
891
-
892
- // If we've received all messages but our count doesn't match, update the total count
893
- if (actualTotalCount !== totalCount) {
894
- console.log(
895
- `Updating totalCount from ${totalCount} to ${actualTotalCount} based on API response`,
896
- );
897
- // This will be applied below when we process the messages
898
- }
899
-
900
- // If no new unique messages, it means we already have everything
901
- if (newUniqueMessages.length === 0) {
902
- console.log('No new unique messages found, adjusting total count');
903
-
904
- // Register this as a failed load attempt
905
- registerLoadAttemptResult(false);
906
-
907
- // Adjust total count to match what we have
908
- safeSend({
909
- type: ThreadActions.SET_THREAD_MESSAGES,
910
- data: {
911
- messages: threadMessages,
912
- totalCount: threadMessages.length,
913
- threadPost: safeContextProperty('threadPost', []),
914
- postThread: safeContextProperty('postThread', null),
915
- },
916
- });
917
- return;
918
- }
919
-
920
- // Register success since we found new messages
921
- registerLoadAttemptResult(true);
922
-
923
- console.log(`Adding ${newUniqueMessages.length} new messages to thread`);
924
-
925
- safeSend({
926
- type: 'FETCH_MORE_MESSAGES_SUCCESS',
927
- data: {
928
- messages: newUniqueMessages,
929
- totalCount: actualTotalCount,
930
- loadingOldMessages: false,
931
- },
932
- });
933
- } else {
934
- console.log('No thread data returned when loading more messages');
935
- registerLoadAttemptResult(false);
936
- safeSend({
937
- type: ThreadActions.STOP_LOADING,
938
- data: { loadingOldMessages: false },
939
- });
940
- }
941
- })
942
- .catch((error: any) => {
943
- console.error('Error fetching more messages:', error);
944
- registerLoadAttemptResult(false);
945
- safeSend({
946
- type: 'ERROR',
947
- data: {
948
- message: error.message,
949
- loadingOldMessages: false,
950
- },
951
- });
952
- });
953
- } else {
954
- console.log('No more messages to load or already loading');
955
- // Make sure we're not stuck in loading state
956
- if (safeContextProperty('loadingOldMessages', false)) {
957
- safeSend({
958
- type: ThreadActions.STOP_LOADING,
959
- data: { loadingOldMessages: false },
960
- });
961
- }
962
- }
963
- }, [parentId, channelId, state.context, registerLoadAttemptResult]);
964
-
965
- let onScroll = false;
966
-
967
- const handleScrollToTop = ({ nativeEvent }: any) => {
968
- // Check if we're near the top of the list
969
- if (isCloseToTop(nativeEvent)) {
970
- const isLoading = safeContextProperty('loadingOldMessages', false);
971
- const totalCount = safeContextProperty('totalCount', 0);
972
- const currentCount = safeContextProperty('threadMessages', []).length;
973
- const hasMoreMessages = totalCount > currentCount;
974
-
975
- console.log(
976
- `Scroll near top - Loading state: ${isLoading}, Messages: ${currentCount}/${totalCount}, Has more: ${hasMoreMessages}`,
977
- );
978
-
979
- if (!isLoading && hasMoreMessages) {
980
- console.log('Near top of list - loading older messages');
981
- safeSend({ type: ThreadActions.FETCH_MORE_MESSAGES });
982
- }
983
- }
984
- };
985
-
986
- const handleEndReached = () => {
987
- // This triggers when scrolled to the bottom
988
- console.log('Reached end of message list');
989
- };
990
-
991
- const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
992
- // We consider being "close to top" when scrolled to top 15% of visible area
993
- const visibleHeight = layoutMeasurement.height;
994
- const topThreshold = Math.min(80, visibleHeight * 0.15);
995
-
996
- // For debugging
997
- const distanceFromTop = contentOffset.y;
998
- const totalContentHeight = contentSize.height;
999
-
1000
- console.log(
1001
- `Scroll position: ${distanceFromTop.toFixed(0)}px from top, threshold: ${topThreshold.toFixed(
1002
- 0,
1003
- )}px, content height: ${totalContentHeight.toFixed(0)}px`,
1004
- );
1005
-
1006
- return contentOffset.y <= topThreshold;
1007
- };
1008
-
1009
- const onSelectImages = async () => {
1010
- try {
1011
- safeSend({ type: ThreadActions.START_LOADING });
1012
-
1013
- const imageSource = await ImagePicker.launchImageLibraryAsync({
1014
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
1015
- allowsEditing: true,
1016
- aspect: [4, 3],
1017
- quality: 0.8, // Reduced from 1 for better performance
1018
- base64: true,
1019
- allowsMultipleSelection: false, // Set to true if you want to support multiple images
1020
- });
1021
-
1022
- if (imageSource.canceled) {
1023
- console.log('Image selection was canceled');
1024
- safeSend({ type: ThreadActions.STOP_LOADING });
1025
- return;
1026
- }
1027
-
1028
- if (!imageSource.assets || imageSource.assets.length === 0 || !imageSource.assets[0]?.base64) {
1029
- console.error('No valid image data received');
1030
- safeSend({
1031
- type: 'ERROR',
1032
- data: { message: 'No valid image data received' },
1033
- });
1034
- return;
1035
- }
1036
-
1037
- // Get the first asset
1038
- const asset = imageSource.assets[0];
1039
-
1040
- // Derive file extension from mime type or default to jpg
1041
- const fileExtension = asset.mimeType ? asset.mimeType.split('/').pop() || 'jpg' : 'jpg';
1042
-
1043
- // Create a more descriptive filename with timestamp
1044
- const filename = `image_${Date.now()}.${fileExtension}`;
1045
-
1046
- // Create data URL with proper mime type
1047
- const mimeType = asset.mimeType || 'image/jpeg';
1048
- const image = `data:${mimeType};base64,${asset.base64}`;
1049
-
1050
- // Create file-like object suitable for React Native
1051
- const fileData = {
1052
- uri: asset.uri,
1053
- type: mimeType,
1054
- name: filename,
1055
- base64: asset.base64,
1056
- };
1057
-
1058
- console.log(`Selected image: ${filename}, type: ${mimeType}`);
1059
-
1060
- safeSend({
1061
- type: ThreadActions.SET_IMAGE,
1062
- data: {
1063
- image,
1064
- files: [fileData],
1065
- images: [asset as ImagePicker.ImagePickerAsset],
1066
- },
1067
- });
1068
- } catch (error) {
1069
- console.error('Error selecting image:', error);
1070
- safeSend({
1071
- type: 'ERROR',
1072
- data: { message: error.message || 'Failed to select image' },
1073
- });
1074
- }
1075
- };
1076
-
1077
- // Define message sending handlers
1078
- const sendThreadMessageHandler = useCallback(
1079
- async (message: string) => {
1080
- console.log('Sending message:', message);
1081
-
1082
- if (!channelId) {
1083
- console.error('No channelId provided');
1084
- return;
1085
- }
1086
-
1087
- // Allow empty messages with spaces or blank content - GiftedChat sometimes sends these
1088
- // But use the actual message if available
1089
- const messageContent = message?.trim() || ' ';
1090
- console.log('Using message content for sending:', messageContent);
1091
-
1092
- const postId = objectId();
1093
- console.log('Generated postId:', postId);
1094
-
1095
- safeSend({ type: ThreadActions.START_LOADING });
1096
-
1097
- try {
1098
- console.log('Sending mutation with variables:', {
1099
- channelId,
1100
- postThreadId: safeContextProperty('postThread')?.id,
1101
- postParentId: !parentId || parentId == 0 ? null : parentId,
1102
- message: messageContent,
1103
- });
1104
-
1105
- const result = await sendThreadMessage({
1106
- variables: {
1107
- channelId,
1108
- postThreadId: safeContextProperty('postThread') && safeContextProperty('postThread')?.id,
1109
- postParentId: !parentId || parentId == 0 ? null : parentId,
1110
- threadMessageInput: {
1111
- content: messageContent,
1112
- role,
1113
- },
1114
- },
1115
- update: (cache, { data, errors }: any) => {
1116
- console.log('Send message update callback - data:', data, 'errors:', errors);
1117
-
1118
- if (!data || errors) {
1119
- console.error('Send message failed:', errors);
1120
- safeSend({ type: ThreadActions.STOP_LOADING });
1121
- return;
1122
- }
1123
-
1124
- console.log('Message sent successfully:', data?.createPostThread?.lastMessage);
1125
-
1126
- // Add the new message to our local state
1127
- const newMessage = data?.createPostThread?.lastMessage;
1128
-
1129
- // Reset the message text and add the new message
1130
- safeSend({
1131
- type: 'SEND_THREAD_MESSAGE_SUCCESS',
1132
- data: {
1133
- message: newMessage,
1134
- messageText: '', // Clear the message text now
1135
- },
1136
- });
1137
-
1138
- if (!parentId || parentId == 0) {
1139
- console.log('Setting new parentId:', data?.createPostThread?.lastMessage?.id);
1140
- setParentId(data?.createPostThread?.lastMessage?.id);
1141
- }
1142
-
1143
- setChannelToTop(channelToTop + 1);
1144
-
1145
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
1146
- sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
1147
- },
1148
- });
1149
-
1150
- console.log('Send mutation result:', result);
1151
- } catch (error) {
1152
- console.error('Error sending message:', error);
1153
- safeSend({
1154
- type: 'ERROR',
1155
- data: { message: error.message || 'Failed to send message' },
1156
- });
1157
- }
1158
- },
1159
- [channelId, parentId, state.context, role],
1160
- );
1161
-
1162
- const sendThreadMessageWithFileHandler = useCallback(
1163
- async (message: string, images: any[]) => {
1164
- console.log('Sending message with file:', message, 'Images:', images.length);
1165
-
1166
- if (!channelId) {
1167
- console.error('No channelId provided');
1168
- return;
1169
- }
1170
-
1171
- if (images.length === 0) {
1172
- console.error('No images to send');
1173
- return;
1174
- }
1175
-
1176
- // Allow empty message content for file uploads
1177
- // But use the actual message if available
1178
- const messageContent = message?.trim() || ' ';
1179
- console.log('Using message content for file send:', messageContent);
1180
-
1181
- const postId = objectId();
1182
- console.log('Generated postId for file upload:', postId);
1183
-
1184
- try {
1185
- // Prepare image assets in the format expected by the upload service
1186
- const preparedImages = images.map((img) => ({
1187
- uri: img.uri,
1188
- type: img.mimeType || 'image/jpeg',
1189
- name: img.fileName || `image_${Date.now()}.jpg`,
1190
- base64: img.base64,
1191
- width: img.width || 0,
1192
- height: img.height || 0,
1193
- })) as ImagePicker.ImagePickerAsset[];
1194
-
1195
- console.log('Starting file upload with prepared images:', preparedImages.length);
1196
-
1197
- const uploadResponse = await startUpload({
1198
- file: preparedImages,
1199
- saveUploadedFile: {
1200
- variables: {
1201
- postId,
1202
- },
1203
- },
1204
- createUploadLink: {
1205
- variables: {
1206
- postId,
1207
- },
1208
- },
1209
- });
1210
-
1211
- if (uploadResponse?.error) {
1212
- console.error('File upload failed:', uploadResponse.error);
1213
- safeSend({ type: ThreadActions.STOP_LOADING });
1214
- return;
1215
- }
1216
-
1217
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
1218
- console.log('Files uploaded successfully:', uploadedFiles?.length);
1219
-
1220
- if (uploadResponse.data) {
1221
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
1222
- console.log('File IDs for message:', files);
1223
-
1224
- console.log('Sending message with attached files');
1225
- const result = await sendThreadMessage({
1226
- variables: {
1227
- postId,
1228
- channelId,
1229
- postThreadId: safeContextProperty('postThread') && safeContextProperty('postThread')?.id,
1230
- postParentId: !parentId || parentId == 0 ? null : parentId,
1231
- threadMessageInput: {
1232
- content: messageContent,
1233
- files,
1234
- role,
1235
- },
1236
- },
1237
- update: (cache, { data, errors }: any) => {
1238
- console.log('Send message with file update callback - data:', data, 'errors:', errors);
1239
-
1240
- if (!data || errors) {
1241
- console.error('Send message with file failed:', errors);
1242
- safeSend({ type: ThreadActions.STOP_LOADING });
1243
- return;
1244
- }
1245
-
1246
- console.log('Message with file sent successfully:', data?.createPostThread?.lastMessage);
1247
-
1248
- // Add the new message to our local state
1249
- const newMessage = data?.createPostThread?.lastMessage;
1250
-
1251
- safeSend({
1252
- type: 'SEND_THREAD_MESSAGE_WITH_FILE_SUCCESS',
1253
- data: {
1254
- message: newMessage,
1255
- messageText: '', // Clear the message text now
1256
- },
1257
- });
1258
-
1259
- if (!parentId || parentId == 0) {
1260
- console.log('Setting new parentId:', data?.createPostThread?.lastMessage?.id);
1261
- setParentId(data?.createPostThread?.lastMessage?.id);
1262
- }
1263
-
1264
- setChannelToTop(channelToTop + 1);
1265
-
1266
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
1267
- sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
1268
- },
1269
- });
1270
-
1271
- console.log('Send with file mutation result:', result);
1272
- }
1273
- } catch (error) {
1274
- console.error('Error sending message with file:', error);
1275
- safeSend({
1276
- type: 'ERROR',
1277
- data: { message: error.message || 'Failed to send message with file' },
1278
- });
1279
- }
1280
- },
1281
- [channelId, parentId, state.context, role, startUpload],
1282
- );
1283
-
1284
- const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
1285
- const notificationData: IExpoNotificationData = {
1286
- url: config.THREAD_MESSEGE_PATH,
1287
- params: { channelId, title: params?.title ?? 'Thread', postParentId: parentId, hideTabBar: true },
1288
- screen: 'DialogThreadMessages',
1289
- thread: { id: threadId },
1290
- other: { sound: Platform.OS === 'android' ? undefined : 'default' },
1291
- };
1292
- if (parentId || parentId == 0) {
1293
- await sendExpoNotificationOnPostMutation({
1294
- variables: {
1295
- postId: messageId?.toString(),
1296
- notificationData,
1297
- },
1298
- });
1299
- }
1300
- };
1301
-
1302
- const messageList = useMemo(() => {
1303
- const threadMessages = safeContextProperty('threadMessages', []);
1304
- console.log(`Creating message list from ${threadMessages.length} thread messages`);
1305
-
1306
- let res: any = [];
1307
- if (threadMessages?.length) {
1308
- // We need to convert the threadMessages into the format expected by GiftedChat
1309
- // Use a Set to track IDs and prevent duplicates
1310
- const messageIds = new Set();
1311
-
1312
- res = threadMessages
1313
- .filter((msg) => {
1314
- // Skip duplicate IDs
1315
- if (!msg.id || messageIds.has(msg.id)) {
1316
- console.log('Skipping duplicate message ID:', msg.id);
1317
- return false;
1318
- }
1319
- messageIds.add(msg.id);
1320
- return true;
1321
- })
1322
- .map((msg) => {
1323
- // Generate a unique _id if needed by combining id and createdAt
1324
- const uniqueId = msg.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1325
-
1326
- // Safely create a Date object with validation
1327
- let messageDate;
1328
- try {
1329
- // Check if createdAt is a valid date string
1330
- if (msg.createdAt && !isNaN(new Date(msg.createdAt).getTime())) {
1331
- messageDate = new Date(msg.createdAt);
1332
- } else {
1333
- // Use current time as fallback if date is invalid
1334
- console.warn(`Invalid date value for message ${msg.id}: ${msg.createdAt}`);
1335
- messageDate = new Date();
1336
- }
1337
- } catch (error) {
1338
- console.error(`Error creating date for message ${msg.id}:`, error);
1339
- messageDate = new Date(); // Fallback to current time
1340
- }
1341
-
1342
- // Extract image URL from files data
1343
- let imageUrl = null;
1344
- if (msg.files?.data && msg.files.data.length > 0) {
1345
- const fileData = msg.files.data[0];
1346
- if (fileData && fileData.url) {
1347
- imageUrl = fileData.url;
1348
- console.log('📷 Found image URL for message', msg.id, ':', imageUrl);
1349
- }
1350
- }
1351
-
1352
- // Get the message text without adding "File attachment" for image-only messages
1353
- let messageText = msg.message || '';
1354
-
1355
- let message: IMessageProps = {
1356
- _id: uniqueId,
1357
- text: messageText,
1358
- createdAt: messageDate,
1359
- user: {
1360
- _id: msg?.author?.id ?? auth?.profile?.id,
1361
- name:
1362
- msg?.author?.givenName ??
1363
- auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
1364
- auth?.profile?.family_name,
1365
- avatar: msg?.author?.picture ?? auth?.profile?.picture,
1366
- },
1367
- type: msg?.type || '',
1368
- image: imageUrl,
1369
- sent: msg?.isDelivered || true,
1370
- received: msg?.isRead || false,
1371
- propsConfiguration: msg?.propsConfiguration,
1372
- };
1373
- return message;
1374
- });
1375
- }
1376
-
1377
- // Sort messages by date (newest first as required by GiftedChat)
1378
- // Use a safer getTime() approach with error handling
1379
- const sortedMessages = orderBy(
1380
- res,
1381
- [
1382
- (msg) => {
1383
- try {
1384
- return msg.createdAt instanceof Date ? msg.createdAt.getTime() : new Date().getTime();
1385
- } catch (error) {
1386
- console.error('Error sorting message by date:', error);
1387
- return 0; // Fallback to a default value
1388
- }
1389
- },
1390
- ],
1391
- ['desc'],
1392
- );
1393
-
1394
- // Log message dates to help with debugging
1395
- if (sortedMessages.length > 0) {
1396
- try {
1397
- const firstMsg = sortedMessages[0];
1398
- const lastMsg = sortedMessages[sortedMessages.length - 1];
1399
-
1400
- console.log(
1401
- 'Message date range:',
1402
- lastMsg.createdAt instanceof Date ? lastMsg.createdAt.toISOString() : 'invalid date',
1403
- 'to',
1404
- firstMsg.createdAt instanceof Date ? firstMsg.createdAt.toISOString() : 'invalid date',
1405
- );
1406
- } catch (error) {
1407
- console.error('Error logging message date range:', error);
1408
- }
1409
- }
1410
-
1411
- return sortedMessages;
1412
- }, [safeContextProperty('threadMessages'), auth]);
1413
-
1414
- const renderSend = (props) => {
1415
- // Check if there's an image selected
1416
- const hasImage = safeContextProperty('selectedImage', '') !== '';
1417
-
1418
- // Enable send button if there's text OR an image
1419
- const isDisabled = !hasImage && (!props.text || props.text.trim().length === 0);
1420
-
1421
- return (
1422
- <Send
1423
- {...props}
1424
- containerStyle={{
1425
- alignItems: 'center',
1426
- justifyContent: 'center',
1427
- marginHorizontal: 4,
1428
- marginBottom: 0,
1429
- }}
1430
- disabled={isDisabled}
1431
- >
1432
- <Box
1433
- style={{
1434
- width: 32,
1435
- height: 32,
1436
- alignItems: 'center',
1437
- justifyContent: 'center',
1438
- }}
1439
- >
1440
- <MaterialCommunityIcons
1441
- name="send-circle"
1442
- size={30}
1443
- color={isDisabled ? colors.gray[400] : colors.blue[500]}
1444
- />
1445
- </Box>
1446
- </Send>
1447
- );
1448
- };
1449
-
1450
- const renderMessageText = (props: any) => {
1451
- const { currentMessage } = props;
1452
-
1453
- // For ALERT type messages with call to action
1454
- if (currentMessage.type === 'ALERT') {
1455
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1456
- let action: string = '';
1457
- let actionId: any = '';
1458
- let params: any = {};
1459
- if (attachment?.callToAction?.extraParams) {
1460
- const extraParams: any = attachment?.callToAction?.extraParams;
1461
- const route: any = extraParams?.route ?? null;
1462
- let path: any = null;
1463
- let param: any = null;
1464
- if (role && role == PreDefinedRole.Guest) {
1465
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
1466
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
1467
- } else if (role && role == PreDefinedRole.Owner) {
1468
- path = route?.host?.name ? route?.host?.name ?? null : null;
1469
- param = route?.host?.params ? route?.host?.params ?? null : null;
1470
- } else {
1471
- path = route?.host?.name ? route?.host?.name ?? null : null;
1472
- param = route?.host?.params ? route?.host?.params ?? null : null;
1473
- }
1474
-
1475
- action = path;
1476
- params = { ...param };
1477
- } else if (attachment?.callToAction?.link) {
1478
- action = CALL_TO_ACTION_PATH;
1479
- actionId = attachment?.callToAction?.link.split('/').pop();
1480
- params = { reservationId: actionId };
1481
- }
1482
-
1483
- return (
1484
- <>
1485
- {attachment?.callToAction && action ? (
1486
- <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
1487
- <Button
1488
- variant={'outline'}
1489
- size={'sm'}
1490
- className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
1491
- onPress={() => action && params && navigation.navigate(action, params)}
1492
- >
1493
- <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
1494
- {attachment.callToAction.title}
1495
- </ButtonText>
1496
- </Button>
1497
- <MessageText
1498
- {...props}
1499
- textStyle={{
1500
- left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1501
- }}
1502
- />
1503
- </Box>
1504
- ) : (
1505
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1506
- )}
1507
- </>
1508
- );
1509
- }
1510
- // For file attachment messages, don't show the "File attachment" text
1511
- else if (currentMessage.text === '📎 File attachment') {
1512
- // Return null to not render any text for these messages
1513
- return null;
1514
- }
1515
- // Default text rendering
1516
- else {
1517
- return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
1518
- }
1519
- };
1520
-
1521
- const renderActions = (props) => {
1522
- return (
1523
- <Actions
1524
- {...props}
1525
- options={{
1526
- ['Choose from Library']: onSelectImages,
1527
- ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1528
- }}
1529
- optionTintColor="#000000"
1530
- cancelButtonIndex={1} // Set the Cancel option as the cancel button
1531
- icon={() => (
1532
- <Box
1533
- style={{
1534
- width: 32,
1535
- height: 32,
1536
- alignItems: 'center',
1537
- justifyContent: 'center',
1538
- }}
1539
- >
1540
- <Ionicons name="image" size={24} color={colors.blue[500]} />
1541
- </Box>
1542
- )}
1543
- containerStyle={{
1544
- alignItems: 'center',
1545
- justifyContent: 'center',
1546
- marginLeft: 8,
1547
- marginBottom: 0,
1548
- }}
1549
- />
1550
- );
1551
- };
1552
-
1553
- const renderAccessory = (props) => {
1554
- const selectedImage = safeContextProperty('selectedImage', '');
1555
-
1556
- if (!selectedImage) {
1557
- return null;
1558
- }
1559
-
1560
- return (
1561
- <View
1562
- style={{
1563
- height: 80,
1564
- padding: 10,
1565
- backgroundColor: 'white',
1566
- borderTopWidth: 1,
1567
- borderTopColor: '#e0e0e0',
1568
- flexDirection: 'row',
1569
- alignItems: 'center',
1570
- }}
1571
- >
1572
- <View
1573
- style={{
1574
- flex: 1,
1575
- flexDirection: 'row',
1576
- alignItems: 'center',
1577
- // justifyContent: 'space-between',
1578
- paddingHorizontal: 20,
1579
- }}
1580
- >
1581
- <Image
1582
- key={state?.context?.selectedImage}
1583
- alt={'selected image'}
1584
- source={{ uri: state?.context?.selectedImage }}
1585
- size={'xs'}
1586
- style={{
1587
- width: 5,
1588
- height: 5,
1589
- borderRadius: 5,
1590
- marginRight: 20,
1591
- }}
1592
- />
1593
-
1594
- <TouchableHighlight
1595
- underlayColor="#dddddd"
1596
- onPress={() => safeSend({ type: ThreadActions.CLEAR_IMAGE })}
1597
- style={{
1598
- backgroundColor: '#f44336',
1599
- paddingVertical: 2,
1600
- paddingHorizontal: 5,
1601
- borderRadius: 5,
1602
- marginLeft: 10,
1603
- elevation: 3,
1604
- shadowColor: '#000',
1605
- shadowOffset: { width: 0, height: 1 },
1606
- shadowOpacity: 0.3,
1607
- shadowRadius: 2,
1608
- }}
1609
- >
1610
- <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1611
- </TouchableHighlight>
1612
- </View>
1613
- </View>
1614
- );
1615
- };
1616
-
1617
- const setImageViewerObject = (obj: any, v: boolean) => {
1618
- setImageObject(obj);
1619
- setImageViewer(v);
1620
- };
1621
-
1622
- const modalContent = React.useMemo(() => {
1623
- if (!imageObject) return <></>;
1624
- const { image, _id } = imageObject;
1625
- return (
1626
- <CachedImage
1627
- style={{ width: '100%', height: '100%' }}
1628
- resizeMode={'cover'}
1629
- cacheKey={`${_id}-slack-bubble-imageKey`}
1630
- source={{
1631
- uri: image,
1632
- expiresIn: 86400,
1633
- }}
1634
- alt={'image'}
1635
- />
1636
- );
1637
- }, [imageObject]);
1638
-
1639
- const renderMessage = (props: any) => {
1640
- return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
1641
- };
1642
-
1643
- // Define a memo that provides the current message text from state
1644
- const currentMessageText = useMemo(() => {
1645
- const text = safeContextProperty('messageText', '') || ' ';
1646
- return text;
1647
- }, [safeContextProperty('messageText')]);
1648
-
1649
- // Define a custom renderInputToolbar function
1650
- const renderInputToolbar = (props) => {
1651
- return (
1652
- <InputToolbar
1653
- {...props}
1654
- containerStyle={{
1655
- backgroundColor: 'white',
1656
- borderTopWidth: 1,
1657
- borderTopColor: colors.gray[200],
1658
- paddingHorizontal: 4,
1659
- paddingVertical: 4,
1660
- }}
1661
- primaryStyle={{
1662
- alignItems: 'center',
1663
- }}
1664
- />
1665
- );
1666
- };
1667
-
1668
- // Add a function to load messages with specific skip value for debugging
1669
- const forceLoadMessages = useCallback(
1670
- (skipValue: number) => {
1671
- console.log(`Force loading messages with explicit skip=${skipValue}, limit=50`);
1672
-
1673
- if (channelId && parentId) {
1674
- safeSend({
1675
- type: ThreadActions.START_LOADING,
1676
- data: { loading: true },
1677
- });
1678
-
1679
- getThreadMessages({
1680
- variables: {
1681
- channelId: channelId?.toString(),
1682
- role: role?.toString(),
1683
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
1684
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
1685
- skip: skipValue,
1686
- limit: 50, // Use larger limit that proved to work
1687
- },
1688
- })
1689
- .then(({ data }) => {
1690
- if (data?.getPostThread) {
1691
- const threads: any = data.getPostThread;
1692
- const threadPost = threads?.post ?? [];
1693
- const threadReplies = threads?.replies ?? [];
1694
- const messageTotalCount = threads?.replyCount ?? 0;
1695
- const messages = [...threadReplies];
1696
-
1697
- console.log(
1698
- `Force load with skip=${skipValue} complete. Got ${messages.length} messages of ${messageTotalCount} total`,
1699
- );
1700
-
1701
- safeSend({
1702
- type: ThreadActions.SET_THREAD_MESSAGES,
1703
- data: {
1704
- messages,
1705
- totalCount: messageTotalCount,
1706
- threadPost: threadPost,
1707
- postThread: threads,
1708
- },
1709
- });
1710
- }
1711
- })
1712
- .catch((error) => {
1713
- console.error('Error during force load:', error);
1714
- safeSend({
1715
- type: 'ERROR',
1716
- data: { message: error.message },
1717
- });
1718
- });
1719
- }
1720
- },
1721
- [channelId, parentId, getThreadMessages, safeSend],
1722
- );
1723
-
1724
- return (
1725
- <SafeAreaView style={{ flex: 1 }}>
1726
- {safeContextProperty('loadingOldMessages', false) === true && (
1727
- <Box className="absolute top-10 left-0 right-0 z-10 items-center">
1728
- <Box className="bg-blue-500/20 rounded-full px-4 py-2 flex-row items-center">
1729
- <Spinner color={colors.blue[500]} size="small" />
1730
- <Text className="text-sm font-medium color-blue-600 ml-2">Loading messages...</Text>
1731
- </Box>
1732
- </Box>
1733
- )}
1734
- {!safeContextProperty('loadingOldMessages', false) &&
1735
- safeContextProperty('totalCount', 0) > safeContextProperty('threadMessages', []).length && (
1736
- <Box className="absolute top-10 left-0 right-0 z-10 items-center">
1737
- <HStack space={2} className="px-2">
1738
- <TouchableHighlight
1739
- onPress={() => {
1740
- console.log('Manual load more pressed');
1741
- Alert.alert('Load Options', 'Choose loading method', [
1742
- {
1743
- text: 'Normal Load',
1744
- onPress: () => safeSend({ type: ThreadActions.FETCH_MORE_MESSAGES }),
1745
- },
1746
- {
1747
- text: 'Try Skip=0',
1748
- onPress: () => forceLoadMessages(0),
1749
- },
1750
- {
1751
- text: 'Try Skip=0, Limit=50',
1752
- onPress: () => {
1753
- // Try with a larger limit
1754
- console.log('Force loading with explicit skip=0, limit=50');
1755
-
1756
- safeSend({
1757
- type: ThreadActions.START_LOADING,
1758
- data: { loadingOldMessages: true },
1759
- });
1760
-
1761
- fetchMoreMessages({
1762
- variables: {
1763
- channelId: channelId?.toString(),
1764
- role: role?.toString(),
1765
- postParentId:
1766
- !parentId || parentId == 0 ? null : parentId?.toString(),
1767
- selectedFields:
1768
- 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
1769
- limit: 50, // Try a larger limit
1770
- skip: 0,
1771
- },
1772
- })
1773
- .then((res: any) => {
1774
- console.log(
1775
- 'LARGE LIMIT response:',
1776
- JSON.stringify(res?.data, null, 2),
1777
- );
1778
-
1779
- if (res?.data?.getPostThread) {
1780
- const threads: any = res?.data?.getPostThread;
1781
- const threadReplies = threads?.replies ?? [];
1782
- const actualTotalCount = threads?.replyCount ?? 0;
1783
-
1784
- console.log(
1785
- `Large limit load complete. Got ${threadReplies.length} messages of ${actualTotalCount} total`,
1786
- );
1787
-
1788
- if (threadReplies.length > 0) {
1789
- // Reset our message list with all available messages
1790
- safeSend({
1791
- type: ThreadActions.SET_THREAD_MESSAGES,
1792
- data: {
1793
- messages: threadReplies,
1794
- totalCount: actualTotalCount,
1795
- threadPost: safeContextProperty(
1796
- 'threadPost',
1797
- [],
1798
- ),
1799
- postThread: threads,
1800
- },
1801
- });
1802
- } else {
1803
- // Reset count if no messages found
1804
- safeSend({
1805
- type: ThreadActions.SET_THREAD_MESSAGES,
1806
- data: {
1807
- messages: safeContextProperty(
1808
- 'threadMessages',
1809
- [],
1810
- ),
1811
- totalCount: safeContextProperty(
1812
- 'threadMessages',
1813
- [],
1814
- ).length,
1815
- threadPost: safeContextProperty(
1816
- 'threadPost',
1817
- [],
1818
- ),
1819
- postThread: safeContextProperty(
1820
- 'postThread',
1821
- null,
1822
- ),
1823
- },
1824
- });
1825
- }
1826
- } else {
1827
- safeSend({
1828
- type: ThreadActions.STOP_LOADING,
1829
- data: { loadingOldMessages: false },
1830
- });
1831
- }
1832
- })
1833
- .catch((error) => {
1834
- console.error('Error in large limit load:', error);
1835
- safeSend({
1836
- type: ThreadActions.STOP_LOADING,
1837
- data: { loadingOldMessages: false },
1838
- });
1839
- });
1840
- },
1841
- },
1842
- {
1843
- text: 'Try Direct Fetch',
1844
- onPress: () => {
1845
- // Get current info
1846
- const currentCount = safeContextProperty('threadMessages', []).length;
1847
- const totalCount = safeContextProperty('totalCount', 0);
1848
- const missingCount = totalCount - currentCount;
1849
-
1850
- if (missingCount <= 0) {
1851
- Alert.alert('Info', 'No missing messages to fetch');
1852
- return;
1853
- }
1854
-
1855
- console.log(
1856
- `Attempting direct fetch of missing ${missingCount} messages`,
1857
- );
1858
-
1859
- // Try explicit query with exact parameters
1860
- safeSend({
1861
- type: ThreadActions.START_LOADING,
1862
- data: { loadingOldMessages: true },
1863
- });
1864
-
1865
- // Special query directly for the missing messages
1866
- getThreadMessages({
1867
- variables: {
1868
- channelId: channelId?.toString(),
1869
- role: role?.toString(),
1870
- postParentId:
1871
- !parentId || parentId == 0 ? null : parentId?.toString(),
1872
- selectedFields:
1873
- 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
1874
- limit: missingCount, // Only get the exact number we need
1875
- skip: 0, // Start from the beginning
1876
- },
1877
- })
1878
- .then(({ data }) => {
1879
- console.log(
1880
- 'DIRECT FETCH response:',
1881
- JSON.stringify(data, null, 2),
1882
- );
1883
-
1884
- if (data?.getPostThread) {
1885
- const threads: any = data.getPostThread;
1886
- const threadReplies = threads?.replies ?? [];
1887
- const actualTotalCount = threads?.replyCount ?? 0;
1888
-
1889
- console.log(
1890
- `Direct fetch complete. Got ${threadReplies.length} messages of ${actualTotalCount} total`,
1891
- );
1892
- console.log(
1893
- 'Message IDs:',
1894
- threadReplies.map((msg) => msg.id).join(', '),
1895
- );
1896
-
1897
- // Compare with our existing messages to find the ones we're missing
1898
- const existingIds = new Set(
1899
- safeContextProperty('threadMessages', []).map(
1900
- (msg) => msg.id,
1901
- ),
1902
- );
1903
- const newMessages = threadReplies.filter(
1904
- (msg) => !existingIds.has(msg.id),
1905
- );
1906
-
1907
- console.log(
1908
- `Found ${newMessages.length} new messages that we didn't have before`,
1909
- );
1910
-
1911
- if (newMessages.length > 0) {
1912
- safeSend({
1913
- type: 'FETCH_MORE_MESSAGES_SUCCESS',
1914
- data: {
1915
- messages: newMessages,
1916
- totalCount: actualTotalCount,
1917
- loadingOldMessages: false,
1918
- },
1919
- });
1920
-
1921
- // Show the results
1922
- Alert.alert(
1923
- 'Success',
1924
- `Found ${newMessages.length} new messages`,
1925
- );
1926
- } else {
1927
- // If we didn't find any new messages, adjust the total count
1928
- console.log('No new messages found, adjusting count');
1929
- safeSend({
1930
- type: ThreadActions.SET_THREAD_MESSAGES,
1931
- data: {
1932
- messages: safeContextProperty(
1933
- 'threadMessages',
1934
- [],
1935
- ),
1936
- totalCount: safeContextProperty(
1937
- 'threadMessages',
1938
- [],
1939
- ).length,
1940
- threadPost: safeContextProperty(
1941
- 'threadPost',
1942
- [],
1943
- ),
1944
- postThread: safeContextProperty(
1945
- 'postThread',
1946
- null,
1947
- ),
1948
- },
1949
- });
1950
-
1951
- // Notify the user
1952
- Alert.alert(
1953
- 'Info',
1954
- 'No new messages found. Count has been adjusted.',
1955
- );
1956
- }
1957
- } else {
1958
- Alert.alert('Error', 'Could not fetch thread messages');
1959
- safeSend({
1960
- type: ThreadActions.STOP_LOADING,
1961
- data: { loadingOldMessages: false },
1962
- });
1963
- }
1964
- })
1965
- .catch((error) => {
1966
- console.error('Error in direct fetch:', error);
1967
- Alert.alert(
1968
- 'Error',
1969
- 'Failed to fetch messages: ' + error.message,
1970
- );
1971
- safeSend({
1972
- type: ThreadActions.STOP_LOADING,
1973
- data: { loadingOldMessages: false },
1974
- });
1975
- });
1976
- },
1977
- },
1978
- {
1979
- text: 'Reset Count',
1980
- style: 'destructive',
1981
- onPress: () => {
1982
- console.log('Resetting message count to match reality');
1983
- safeSend({
1984
- type: ThreadActions.SET_THREAD_MESSAGES,
1985
- data: {
1986
- messages: safeContextProperty('threadMessages', []),
1987
- totalCount: safeContextProperty('threadMessages', []).length,
1988
- threadPost: safeContextProperty('threadPost', []),
1989
- postThread: safeContextProperty('postThread', null),
1990
- },
1991
- });
1992
- },
1993
- },
1994
- {
1995
- text: 'Cancel',
1996
- style: 'cancel',
1997
- },
1998
- ]);
1999
- }}
2000
- underlayColor="#e6e6e6"
2001
- >
2002
- <Box className="bg-gray-200 rounded-full px-4 py-2 flex-row items-center">
2003
- <MaterialIcons name="arrow-upward" size={16} color={colors.blue[500]} />
2004
- <Text className="text-sm font-medium color-blue-600 ml-2">
2005
- Load More (
2006
- {safeContextProperty('totalCount', 0) -
2007
- safeContextProperty('threadMessages', []).length}
2008
- )
2009
- </Text>
2010
- </Box>
2011
- </TouchableHighlight>
2012
-
2013
- <TouchableHighlight onPress={forceRefreshMessages} underlayColor="#e6e6e6">
2014
- <Box className="bg-gray-200 rounded-full px-4 py-2 flex-row items-center">
2015
- <MaterialIcons name="refresh" size={16} color={colors.blue[500]} />
2016
- <Text className="text-sm font-medium color-blue-600 ml-2">Refresh All</Text>
2017
- </Box>
2018
- </TouchableHighlight>
2019
- </HStack>
2020
- </Box>
2021
- )}
2022
- {isPostParentIdThread && (
2023
- <>
2024
- {safeContextProperty('threadPost', [])?.length > 0 && (
2025
- <>
2026
- <VStack className="px-2 pt-2 pb-0" space={'sm'}>
2027
- <HStack space={'sm'} className="items-center">
2028
- <Avatar className="bg-transparent" size={'md'}>
2029
- <AvatarFallbackText>
2030
- {startCase(
2031
- safeContextProperty('threadPost')[0]?.author?.username?.charAt(0),
2032
- )}
2033
- </AvatarFallbackText>
2034
- <AvatarImage
2035
- alt="image"
2036
- style={{
2037
- borderRadius: 6,
2038
- borderWidth: 2,
2039
- borderColor: '#fff',
2040
- }}
2041
- source={{
2042
- uri: safeContextProperty('threadPost')[0]?.author?.picture,
2043
- }}
2044
- />
2045
- </Avatar>
2046
- <Box>
2047
- <Text className="font-bold color-black">
2048
- {safeContextProperty('threadPost')[0]?.author?.givenName ?? ''}{' '}
2049
- {safeContextProperty('threadPost')[0]?.author?.familyName ?? ''}
2050
- </Text>
2051
- <Text className="pl-0 color-gray-500">
2052
- {createdAtText(safeContextProperty('threadPost')[0]?.createdAt)} at{' '}
2053
- {(() => {
2054
- try {
2055
- const createdAt = safeContextProperty('threadPost')[0]?.createdAt;
2056
- if (createdAt && !isNaN(new Date(createdAt).getTime())) {
2057
- return format(new Date(createdAt), 'hh:mm:a');
2058
- } else {
2059
- return 'unknown time';
2060
- }
2061
- } catch (error) {
2062
- console.error('Error formatting thread post time:', error);
2063
- return 'unknown time';
2064
- }
2065
- })()}
2066
- </Text>
2067
- </Box>
2068
- </HStack>
2069
- <HStack space={'sm'} className="px-2 items-center">
2070
- <Text>{safeContextProperty('threadPost')[0]?.message ?? ''}</Text>
2071
- </HStack>
2072
- </VStack>
2073
-
2074
- <Box className="py-4">
2075
- <Box className="px-4 py-2 border-t border-b border-gray-200">
2076
- <Text className="font-bold color-gray-600">
2077
- {safeContextProperty('threadPost')[0]?.replies?.totalCount}{' '}
2078
- {safeContextProperty('threadPost')[0]?.replies?.totalCount > 0
2079
- ? 'replies'
2080
- : 'reply'}
2081
- </Text>
2082
- </Box>
2083
- </Box>
2084
- </>
2085
- )}
2086
- </>
2087
- )}
2088
- <GiftedChat
2089
- ref={threadMessageListRef}
2090
- wrapInSafeArea={false}
2091
- renderLoading={() => <Spinner color={colors.blue[500]} />}
2092
- messages={messageList}
2093
- listViewProps={{
2094
- onScroll: handleScrollToTop,
2095
- onEndReached: handleEndReached,
2096
- onEndReachedThreshold: 0.2,
2097
- contentContainerStyle: {
2098
- paddingBottom: 10,
2099
- },
2100
- maintainVisibleContentPosition: {
2101
- minIndexForVisible: 0,
2102
- autoscrollToTopThreshold: 100,
2103
- },
2104
- scrollEventThrottle: 16,
2105
- keyboardDismissMode: 'on-drag',
2106
- keyboardShouldPersistTaps: 'handled',
2107
- }}
2108
- onSend={(messages) => {
2109
- if (!messages || messages.length === 0) {
2110
- console.log('No messages to send');
2111
- return;
2112
- }
2113
-
2114
- // Use the actual message text from the state, not the one from GiftedChat
2115
- // GiftedChat sometimes sends blank messages even when there's text in the input
2116
- const currentInputText = currentMessageText;
2117
- const messageToSend = currentInputText?.trim() || messages[0]?.text?.trim() || ' ';
2118
-
2119
- console.log('GiftedChat onSend triggered with text from state:', messageToSend);
2120
-
2121
- // Make sure we update the message text in state
2122
- safeSend({
2123
- type: ThreadActions.SET_MESSAGE_TEXT,
2124
- data: { messageText: '' },
2125
- });
2126
-
2127
- // Then send the message
2128
- if (safeContextProperty('images', []).length > 0) {
2129
- console.log(
2130
- 'Sending message with file:',
2131
- messageToSend,
2132
- 'Images:',
2133
- safeContextProperty('images', []).length,
2134
- );
2135
- safeSend({
2136
- type: ThreadActions.SEND_THREAD_MESSAGE_WITH_FILE,
2137
- data: { messageText: messageToSend },
2138
- });
2139
- } else {
2140
- console.log('Sending text message:', messageToSend);
2141
- safeSend({
2142
- type: ThreadActions.SEND_THREAD_MESSAGE,
2143
- data: { messageText: messageToSend },
2144
- });
2145
- }
2146
- }}
2147
- text={currentMessageText}
2148
- onInputTextChanged={(text) => {
2149
- // Don't log every keystroke to reduce console spam
2150
- if (text.length % 5 === 0 || text.length < 5) {
2151
- console.log('Input text changed:', text);
2152
- }
2153
-
2154
- // Set the text in the state
2155
- safeSend({
2156
- type: ThreadActions.SET_MESSAGE_TEXT,
2157
- data: { messageText: text },
2158
- });
2159
- }}
2160
- renderFooter={() =>
2161
- safeContextProperty('loading', false) && !safeContextProperty('loadingOldMessages', false) ? (
2162
- <Box className="w-full py-2 items-center">
2163
- <Spinner color={colors.blue[500]} />
2164
- </Box>
2165
- ) : safeContextProperty('imageLoading', false) ? (
2166
- <Box className="w-full py-2 items-center">
2167
- <Spinner color={colors.blue[500]} />
2168
- </Box>
2169
- ) : (
2170
- <></>
2171
- )
2172
- }
2173
- scrollToBottom
2174
- loadEarlier={false}
2175
- isLoadingEarlier={false}
2176
- user={{
2177
- _id: auth?.id || '',
2178
- }}
2179
- isTyping={true}
2180
- alwaysShowSend={safeContextProperty('loading', false) ? false : true}
2181
- infiniteScroll={true}
2182
- renderSend={renderSend}
2183
- renderInputToolbar={renderInputToolbar}
2184
- minInputToolbarHeight={50}
2185
- renderActions={renderActions}
2186
- renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
2187
- renderMessage={renderMessage}
2188
- maxInputLength={1000}
2189
- placeholder="Type a message..."
2190
- showUserAvatar={true}
2191
- showAvatarForEveryMessage={false}
2192
- inverted={true}
2193
- parsePatterns={(linkStyle) => [
2194
- {
2195
- type: 'url',
2196
- style: { ...linkStyle, color: colors.blue[500] },
2197
- onPress: (url) => Linking.openURL(url),
2198
- },
2199
- {
2200
- type: 'phone',
2201
- style: { ...linkStyle, color: colors.blue[500] },
2202
- onPress: (phone) => Linking.openURL(`tel:${phone}`),
2203
- },
2204
- {
2205
- type: 'email',
2206
- style: { ...linkStyle, color: colors.blue[500] },
2207
- onPress: (email) => Linking.openURL(`mailto:${email}`),
2208
- },
2209
- ]}
2210
- textInputProps={{
2211
- style: {
2212
- borderWidth: 1,
2213
- borderColor: colors.gray[300],
2214
- backgroundColor: '#f8f8f8',
2215
- borderRadius: 20,
2216
- minHeight: 40,
2217
- maxHeight: 80,
2218
- color: '#000',
2219
- padding: 10,
2220
- paddingHorizontal: 20,
2221
- fontSize: 16,
2222
- flex: 1,
2223
- },
2224
- multiline: true,
2225
- returnKeyType: 'default',
2226
- enablesReturnKeyAutomatically: true,
2227
- placeholderTextColor: colors.gray[400],
2228
- }}
2229
- minComposerHeight={44}
2230
- isKeyboardInternallyHandled={true}
2231
- bottomOffset={Platform.OS === 'ios' ? 20 : 0}
2232
- renderChatFooter={() => (
2233
- <>
2234
- <ImageViewerModal
2235
- isVisible={isShowImageViewer}
2236
- setVisible={setImageViewer}
2237
- modalContent={modalContent}
2238
- />
2239
- <SubscriptionHandler
2240
- channelId={channelId}
2241
- subscribeToNewMessages={() =>
2242
- subscribeToMore({
2243
- document: CHAT_MESSAGE_ADDED,
2244
- variables: {
2245
- channelId: channelId?.toString(),
2246
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
2247
- },
2248
- updateQuery: (prev, { subscriptionData }: any) => {
2249
- if (!subscriptionData.data) return prev;
2250
- const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
2251
- const prevReplyCount: any = prev?.getPostThread?.replyCount;
2252
- const newReplyCount = prevReplyCount || 0 + 1;
2253
- const replies = prev?.getPostThread?.replies || [];
2254
-
2255
- safeSend({
2256
- type: ThreadActions.SET_THREAD_MESSAGES,
2257
- data: {
2258
- messages: uniqBy(
2259
- [...safeContextProperty('threadMessages', []), newMessage],
2260
- ({ id }) => id,
2261
- ),
2262
- totalCount: newReplyCount,
2263
- threadPost: safeContextProperty('threadPost', []),
2264
- postThread: safeContextProperty('postThread', null),
2265
- },
2266
- });
2267
-
2268
- return Object.assign({}, prev, {
2269
- getPostThread: {
2270
- ...prev?.getPostThread,
2271
- lastReplyAt: newMessage.createdAt,
2272
- replies: [newMessage, ...replies],
2273
- replyCount: newReplyCount,
2274
- updatedAt: newMessage.createdAt,
2275
- },
2276
- });
2277
- },
2278
- })
2279
- }
2280
- />
2281
- </>
2282
- )}
2283
- messagesContainerStyle={messageList?.length == 0 && { transform: [{ scaleY: -1 }] }}
2284
- renderChatEmpty={() => (
2285
- <>
2286
- {!threadLoading && messageList && messageList?.length == 0 && (
2287
- <Box className="p-5">
2288
- <Center className="mt-6">
2289
- <Ionicons name="chatbubbles" size={30} />
2290
- <Text>You don't have any message yet!</Text>
2291
- </Center>
2292
- </Box>
2293
- )}
2294
- </>
2295
- )}
2296
- lightboxProps={{
2297
- underlayColor: 'transparent',
2298
- springConfig: { tension: 90000, friction: 90000 },
2299
- disabled: true,
2300
- }}
2301
- />
2302
- </SafeAreaView>
2303
- );
2304
- };
2305
-
2306
- const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: IThreadSubscriptionHandlerProps) => {
2307
- useEffect(() => subscribeToNewMessages(), [channelId]);
2308
- return <></>;
2309
- };
2310
-
2311
- export const ThreadConversationView = React.memo(ThreadConversationViewComponent);
2312
- // export const ThreadConversationView = ThreadConversationViewComponent;