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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/lib/compute.js +2 -3
  2. package/lib/compute.js.map +1 -1
  3. package/lib/index.js.map +1 -1
  4. package/lib/module.js.map +1 -1
  5. package/lib/queries/inboxQueries.js +65 -0
  6. package/lib/queries/inboxQueries.js.map +1 -0
  7. package/lib/routes.json +2 -3
  8. package/lib/screens/inbox/DialogMessages.js +8 -3
  9. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  10. package/lib/screens/inbox/DialogThreadMessages.js +6 -11
  11. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  12. package/lib/screens/inbox/DialogThreads.js +58 -20
  13. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  14. package/lib/screens/inbox/Inbox.js.map +1 -1
  15. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  16. package/lib/screens/inbox/components/CachedImage/index.js +125 -115
  17. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  18. package/lib/screens/inbox/components/DialogItem.js +160 -0
  19. package/lib/screens/inbox/components/DialogItem.js.map +1 -0
  20. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
  21. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  22. package/lib/screens/inbox/components/SlackMessageContainer/ImageViewerModal.js +2 -0
  23. package/lib/screens/inbox/components/SlackMessageContainer/ImageViewerModal.js.map +1 -1
  24. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js +194 -0
  25. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js.map +1 -0
  26. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +144 -32
  27. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  28. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +3 -4
  29. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  30. package/lib/screens/inbox/components/SubscriptionHandler.js +22 -0
  31. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  32. package/lib/screens/inbox/components/ThreadsViewItem.js +67 -47
  33. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  34. package/lib/screens/inbox/config/config.js +4 -2
  35. package/lib/screens/inbox/config/config.js.map +1 -1
  36. package/lib/screens/inbox/containers/ConversationView.js +1098 -1093
  37. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  38. package/lib/screens/inbox/containers/Dialogs.js +179 -333
  39. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  40. package/lib/screens/inbox/containers/ThreadConversationView.js +873 -866
  41. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  42. package/lib/screens/inbox/containers/ThreadsView.js +81 -54
  43. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  44. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  45. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  46. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  47. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  48. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  49. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  50. package/package.json +5 -5
  51. package/CHANGELOG.md +0 -156
  52. package/jest.config.js +0 -24
  53. package/lib/screens/inbox/components/DialogsListItem.js +0 -175
  54. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  55. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -165
  56. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
  57. package/lib/screens/inbox/containers/workflow/conversation-xstate.js +0 -380
  58. package/lib/screens/inbox/containers/workflow/conversation-xstate.js.map +0 -1
  59. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +0 -235
  60. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +0 -1
  61. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js +0 -438
  62. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js.map +0 -1
  63. package/rollup.config.mjs +0 -45
  64. package/src/components/index.ts +0 -0
  65. package/src/compute.ts +0 -63
  66. package/src/index.ts +0 -7
  67. package/src/module.ts +0 -10
  68. package/src/navigation/InboxNavigation.tsx +0 -102
  69. package/src/navigation/index.ts +0 -1
  70. package/src/screens/inbox/DialogMessages.tsx +0 -21
  71. package/src/screens/inbox/DialogThreadMessages.tsx +0 -97
  72. package/src/screens/inbox/DialogThreads.tsx +0 -129
  73. package/src/screens/inbox/Inbox.tsx +0 -17
  74. package/src/screens/inbox/components/CachedImage/consts.ts +0 -6
  75. package/src/screens/inbox/components/CachedImage/index.tsx +0 -223
  76. package/src/screens/inbox/components/DialogsHeader.tsx +0 -30
  77. package/src/screens/inbox/components/DialogsListItem.tsx +0 -302
  78. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +0 -287
  79. package/src/screens/inbox/components/SlackMessageContainer/ImageViewerModal.tsx +0 -113
  80. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +0 -313
  81. package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +0 -145
  82. package/src/screens/inbox/components/SlackMessageContainer/index.ts +0 -3
  83. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +0 -283
  84. package/src/screens/inbox/components/ThreadsViewItem.tsx +0 -321
  85. package/src/screens/inbox/config/config.ts +0 -15
  86. package/src/screens/inbox/config/index.ts +0 -1
  87. package/src/screens/inbox/containers/ConversationView.tsx +0 -1782
  88. package/src/screens/inbox/containers/Dialogs.tsx +0 -544
  89. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +0 -119
  90. package/src/screens/inbox/containers/ThreadConversationView.tsx +0 -1537
  91. package/src/screens/inbox/containers/ThreadsView.tsx +0 -305
  92. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +0 -20
  93. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +0 -313
  94. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +0 -196
  95. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +0 -401
  96. package/src/screens/index.ts +0 -4
  97. package/tsconfig.json +0 -13
  98. package/webpack.config.js +0 -58
@@ -1,1537 +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 } 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
- let date = new Date(value);
68
- if (isToday(date)) return 'Today';
69
- if (isYesterday(date)) return 'Yesterday';
70
- return format(new Date(value), 'MMM dd, yyyy');
71
- };
72
-
73
- // Create a safer version of useMachine to handle potential errors
74
- function useSafeMachine(machine) {
75
- // Define the state type
76
- interface SafeStateType {
77
- context: {
78
- channelId: any;
79
- postParentId: any;
80
- role: any;
81
- threadMessages: any[];
82
- totalCount: number;
83
- skip: number;
84
- loading: boolean;
85
- loadingOldMessages: boolean;
86
- error: any;
87
- selectedImage: string;
88
- files: any[];
89
- images: any[];
90
- messageText: string;
91
- imageLoading: boolean;
92
- postThread: any;
93
- threadPost: any[];
94
- isScrollToBottom: boolean;
95
- };
96
- value: string;
97
- matches?: (stateValue: string) => boolean;
98
- }
99
-
100
- // Initialize with default state
101
- const [state, setState] = useState<SafeStateType>({
102
- context: {
103
- channelId: null,
104
- postParentId: null,
105
- role: null,
106
- threadMessages: [],
107
- totalCount: 0,
108
- skip: 0,
109
- loading: false,
110
- loadingOldMessages: false,
111
- error: null,
112
- selectedImage: '',
113
- files: [],
114
- images: [],
115
- messageText: '',
116
- imageLoading: false,
117
- postThread: null,
118
- threadPost: [],
119
- isScrollToBottom: false,
120
- },
121
- value: 'idle',
122
- });
123
-
124
- // Create a safe send function
125
- const send = useCallback((event) => {
126
- try {
127
- // Log event for debugging
128
- console.log('Thread Event received:', event.type);
129
-
130
- // Handle specific events manually
131
- if (event.type === ThreadActions.INITIAL_CONTEXT) {
132
- setState((prev) => ({
133
- ...prev,
134
- context: {
135
- ...prev.context,
136
- channelId: event.data?.channelId || null,
137
- postParentId: event.data?.postParentId || null,
138
- role: event.data?.role || null,
139
- },
140
- value: BaseState.FetchThreadMessages,
141
- }));
142
- } else if (event.type === ThreadActions.SET_THREAD_MESSAGES) {
143
- setState((prev) => ({
144
- ...prev,
145
- context: {
146
- ...prev.context,
147
- threadMessages: event.data?.messages || [],
148
- totalCount: event.data?.totalCount || 0,
149
- loading: false,
150
- loadingOldMessages: false,
151
- threadPost: event.data?.threadPost || [],
152
- postThread: event.data?.postThread || null,
153
- },
154
- value: 'active',
155
- }));
156
- } else if (event.type === ThreadActions.CLEAR_MESSAGES) {
157
- setState((prev) => ({
158
- ...prev,
159
- context: {
160
- ...prev.context,
161
- threadMessages: [],
162
- totalCount: 0,
163
- },
164
- }));
165
- } else if (event.type === ThreadActions.SET_MESSAGE_TEXT) {
166
- setState((prev) => ({
167
- ...prev,
168
- context: {
169
- ...prev.context,
170
- messageText: event.data?.messageText || '',
171
- },
172
- }));
173
- } else if (event.type === ThreadActions.FETCH_MORE_MESSAGES) {
174
- setState((prev) => ({
175
- ...prev,
176
- context: {
177
- ...prev.context,
178
- loadingOldMessages: true,
179
- },
180
- value: MainState.FetchMoreMessages,
181
- }));
182
- } else if (event.type === ThreadActions.SET_IMAGE) {
183
- setState((prev) => ({
184
- ...prev,
185
- context: {
186
- ...prev.context,
187
- selectedImage: event.data?.image || '',
188
- images: event.data?.images || [],
189
- files: event.data?.files || [],
190
- imageLoading: false,
191
- },
192
- }));
193
- } else if (event.type === ThreadActions.CLEAR_IMAGE) {
194
- setState((prev) => ({
195
- ...prev,
196
- context: {
197
- ...prev.context,
198
- selectedImage: '',
199
- images: [],
200
- files: [],
201
- },
202
- }));
203
- } else if (event.type === ThreadActions.START_LOADING) {
204
- setState((prev) => ({
205
- ...prev,
206
- context: {
207
- ...prev.context,
208
- loading: true,
209
- },
210
- }));
211
- } else if (event.type === ThreadActions.STOP_LOADING) {
212
- setState((prev) => ({
213
- ...prev,
214
- context: {
215
- ...prev.context,
216
- loading: false,
217
- },
218
- }));
219
- } else if (event.type === ThreadActions.SEND_THREAD_MESSAGE) {
220
- console.log('Sending message event with text:', event.data?.messageText);
221
- setState((prev) => ({
222
- ...prev,
223
- context: {
224
- ...prev.context,
225
- loading: true,
226
- // Keep the message text until we're done sending
227
- messageText: event.data?.messageText || prev.context.messageText,
228
- },
229
- value: MainState.SendThreadMessage,
230
- }));
231
- } else if (event.type === ThreadActions.SEND_THREAD_MESSAGE_WITH_FILE) {
232
- console.log('Sending message with file event, text:', event.data?.messageText);
233
- setState((prev) => ({
234
- ...prev,
235
- context: {
236
- ...prev.context,
237
- loading: true,
238
- // Keep the message text until we're done sending
239
- messageText: event.data?.messageText || prev.context.messageText,
240
- },
241
- value: MainState.SendThreadMessageWithFile,
242
- }));
243
- } else if (
244
- event.type === 'SEND_THREAD_MESSAGE_SUCCESS' ||
245
- event.type === 'SEND_THREAD_MESSAGE_WITH_FILE_SUCCESS'
246
- ) {
247
- console.log('Handling send success event:', event.type, 'with message:', event.data?.message?.id);
248
- setState((prev) => {
249
- // Make sure we have the message data
250
- if (!event.data?.message) {
251
- console.warn('Send success event without message data');
252
- return {
253
- ...prev,
254
- context: {
255
- ...prev.context,
256
- loading: false,
257
- messageText: '', // Clear input
258
- images: [],
259
- selectedImage: '',
260
- files: [],
261
- },
262
- value: 'active',
263
- };
264
- }
265
-
266
- // Add the new message to our threadMessages
267
- const newMessage = event.data.message;
268
- const updatedMessages = [newMessage, ...prev.context.threadMessages];
269
-
270
- console.log('Updated thread messages list after send, now has', updatedMessages.length, 'messages');
271
-
272
- return {
273
- ...prev,
274
- context: {
275
- ...prev.context,
276
- loading: false,
277
- messageText: '', // Always clear input text after sending
278
- images: [],
279
- selectedImage: '',
280
- files: [],
281
- threadMessages: updatedMessages,
282
- totalCount: prev.context.totalCount + 1,
283
- },
284
- value: 'active',
285
- };
286
- });
287
- } else if (event.type === 'FETCH_MORE_MESSAGES_SUCCESS') {
288
- setState((prev) => {
289
- const newMessages = event.data?.messages || [];
290
- return {
291
- ...prev,
292
- context: {
293
- ...prev.context,
294
- loadingOldMessages: false,
295
- threadMessages: uniqBy([...prev.context.threadMessages, ...newMessages], ({ id }) => id),
296
- },
297
- value: 'active',
298
- };
299
- });
300
- } else if (event.type === 'ERROR') {
301
- setState((prev) => ({
302
- ...prev,
303
- context: {
304
- ...prev.context,
305
- loading: false,
306
- loadingOldMessages: false,
307
- error: event.data?.message || 'Unknown error',
308
- },
309
- value: 'error',
310
- }));
311
- }
312
- } catch (error) {
313
- console.error('Error in thread conversation send function:', error);
314
- }
315
- }, []);
316
-
317
- // Add a custom matches function to the state
318
- const stateWithMatches = useMemo(() => {
319
- return {
320
- ...state,
321
- matches: (checkState) => {
322
- return state.value === checkState;
323
- },
324
- };
325
- }, [state]);
326
-
327
- // Return as a tuple to match useMachine API
328
- return [stateWithMatches, send] as const;
329
- }
330
-
331
- interface IMessageProps extends IMessage {
332
- type: string;
333
- propsConfiguration?: any;
334
- }
335
-
336
- export interface AlertMessageAttachmentsInterface {
337
- title: string;
338
- isTitleHtml: boolean;
339
- icon: string;
340
- callToAction: {
341
- title: string;
342
- link: string;
343
- };
344
- }
345
-
346
- interface IThreadSubscriptionHandlerProps {
347
- subscribeToNewMessages: () => any;
348
- channelId: string;
349
- }
350
-
351
- const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any): JSX.Element => {
352
- const { params } = useRoute<any>();
353
- const [channelToTop, setChannelToTop] = useState(0);
354
-
355
- // Create a ref to track if component is mounted
356
- const isMountedRef = useRef(true);
357
-
358
- // Use our safer custom implementation instead of the problematic useMachine
359
- const [state, send] = useSafeMachine(threadConversationXstate);
360
-
361
- // Define safe functions first to avoid "used before declaration" errors
362
- const safeContext = useCallback(() => {
363
- try {
364
- return state?.context || {};
365
- } catch (error) {
366
- console.error('Error accessing state.context:', error);
367
- return {};
368
- }
369
- }, [state]);
370
-
371
- const safeContextProperty = useCallback(
372
- (property, defaultValue = null) => {
373
- try {
374
- return state?.context?.[property] ?? defaultValue;
375
- } catch (error) {
376
- console.error(`Error accessing state.context.${property}:`, error);
377
- return defaultValue;
378
- }
379
- },
380
- [state],
381
- );
382
-
383
- const safeMatches = useCallback(
384
- (stateValue) => {
385
- try {
386
- return state?.matches?.(stateValue) || false;
387
- } catch (error) {
388
- console.error(`Error calling state.matches with ${stateValue}:`, error);
389
- return false;
390
- }
391
- },
392
- [state],
393
- );
394
-
395
- const safeSend = useCallback(
396
- (event) => {
397
- try {
398
- send(event);
399
- } catch (error) {
400
- console.error('Error sending event to state machine:', error, event);
401
- }
402
- },
403
- [send],
404
- );
405
-
406
- // Use a ref to track the current machine snapshot for safer access
407
- const stateRef = useRef(state);
408
-
409
- // Keep the ref updated with the latest snapshot
410
- useEffect(() => {
411
- stateRef.current = state;
412
- }, [state]);
413
-
414
- const auth: any = useSelector(userSelector);
415
- const [selectedImage, setImage] = useState<string>('');
416
- const navigation = useNavigation<any>();
417
- const [files, setFiles] = useState<File[]>([]);
418
- const [images, setImages] = useState<ImagePicker.ImagePickerAsset[]>([]);
419
- const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
420
- const [imageObject, setImageObject] = useState<any>({});
421
- const [parentId, setParentId] = useState<any>(postParentId);
422
- const [expoTokens, setExpoTokens] = useState<any[]>([]);
423
- const threadMessageListRef = useRef<any>(null);
424
-
425
- // const [sendThreadMessage] = useSendThreadMessageMutation();
426
- const [sendThreadMessage] = useCreatePostThreadMutation();
427
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
428
- const { startUpload } = useUploadFilesNative();
429
-
430
- const [
431
- getThreadMessages,
432
- { data, loading: threadLoading, fetchMore: fetchMoreMessages, refetch: refetchThreadMessages, subscribeToMore },
433
- ] = useGetPostThreadLazyQuery({ fetchPolicy: 'cache-and-network' });
434
-
435
- useFocusEffect(
436
- React.useCallback(() => {
437
- // navigation?.setOptions({ title: params?.title ?? 'Thread' });
438
-
439
- navigation.setOptions({
440
- title: params?.title ?? 'Thread',
441
- headerLeft: (props: any) => (
442
- <Button className="bg-transparent active:bg-gray-200 " onPress={() => navigation.goBack()}>
443
- <MaterialIcons size={20} name="arrow-back-ios" color={'black'} />
444
- </Button>
445
- ),
446
- });
447
-
448
- // Set initial context when focused
449
- if (channelId && postParentId) {
450
- safeSend({
451
- type: ThreadActions.INITIAL_CONTEXT,
452
- data: {
453
- channelId,
454
- postParentId,
455
- role,
456
- },
457
- });
458
- }
459
-
460
- setParentId(postParentId);
461
-
462
- return () => {
463
- safeSend({ type: ThreadActions.CLEAR_MESSAGES });
464
- };
465
- }, [postParentId]),
466
- );
467
-
468
- // Effect for when in FetchThreadMessages state
469
- useEffect(() => {
470
- if (safeMatches(BaseState.FetchThreadMessages)) {
471
- fetchThreadMessages();
472
- }
473
- }, [state.value]);
474
-
475
- // Effect for when in FetchMoreMessages state
476
- useEffect(() => {
477
- if (safeMatches(MainState.FetchMoreMessages)) {
478
- onFetchOld();
479
- }
480
- }, [state.value]);
481
-
482
- // Effect for when in SendThreadMessage state
483
- useEffect(() => {
484
- if (safeMatches(MainState.SendThreadMessage)) {
485
- const messageText = safeContextProperty('messageText', '');
486
- console.log('Sending message from state transition, text:', messageText);
487
- sendThreadMessageHandler(messageText);
488
- }
489
- }, [state.value]);
490
-
491
- // Effect for when in SendThreadMessageWithFile state
492
- useEffect(() => {
493
- if (safeMatches(MainState.SendThreadMessageWithFile)) {
494
- const messageText = safeContextProperty('messageText', '');
495
- const images = safeContextProperty('images', []);
496
- sendThreadMessageWithFileHandler(messageText, images);
497
- }
498
- }, [state.value]);
499
-
500
- // Fetch thread messages function
501
- const fetchThreadMessages = useCallback(() => {
502
- if (channelId && parentId) {
503
- getThreadMessages({
504
- variables: {
505
- channelId: channelId?.toString(),
506
- role: role?.toString(),
507
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
508
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
509
- limit: MESSAGES_PER_PAGE,
510
- },
511
- })
512
- .then(({ data }) => {
513
- if (data?.getPostThread) {
514
- const threads: any = data.getPostThread;
515
- const threadPost = threads?.post ?? [];
516
- const threadReplies = threads?.replies ?? [];
517
- const messageTotalCount = threads?.replyCount ?? 0;
518
- const messages = [...threadReplies];
519
-
520
- safeSend({
521
- type: ThreadActions.SET_THREAD_MESSAGES,
522
- data: {
523
- messages,
524
- totalCount: messageTotalCount,
525
- threadPost: threadPost,
526
- postThread: threads,
527
- },
528
- });
529
- }
530
- })
531
- .catch((error) => {
532
- safeSend({
533
- type: 'ERROR',
534
- data: { message: error.message },
535
- });
536
- });
537
- }
538
- }, [channelId, parentId, role]);
539
-
540
- React.useEffect(() => {
541
- if (data?.getPostThread) {
542
- const threads: any = data.getPostThread;
543
- const threadPost = threads?.post ?? [];
544
- const threadReplies = threads?.replies ?? [];
545
- const messeageTotalCount = threads?.replyCount ?? 0;
546
- const messages = [...threadReplies];
547
-
548
- safeSend({
549
- type: ThreadActions.SET_THREAD_MESSAGES,
550
- data: {
551
- messages,
552
- totalCount: messeageTotalCount,
553
- threadPost: threadPost,
554
- postThread: threads,
555
- },
556
- });
557
- }
558
- }, [data]);
559
-
560
- React.useEffect(() => {
561
- if (safeContextProperty('selectedImage')) {
562
- safeSend({ type: ThreadActions.STOP_LOADING });
563
- }
564
- }, [safeContextProperty('selectedImage')]);
565
-
566
- const scrollToBottom = React.useCallback(() => {
567
- if (threadMessageListRef?.current) {
568
- threadMessageListRef.current.scrollToBottom();
569
- }
570
- }, [threadMessageListRef]);
571
-
572
- const onFetchOld = useCallback(() => {
573
- const totalCount = safeContextProperty('totalCount', 0);
574
- const threadMessages = safeContextProperty('threadMessages', []);
575
-
576
- if (totalCount > threadMessages.length && !safeContextProperty('loadingOldMessages', false)) {
577
- console.log('Loading more messages - current count:', threadMessages.length, 'of', totalCount);
578
-
579
- // Set the loading state specifically for old messages
580
- safeSend({
581
- type: ThreadActions.START_LOADING,
582
- data: { loadingOldMessages: true },
583
- });
584
-
585
- fetchMoreMessages({
586
- variables: {
587
- channelId: channelId?.toString(),
588
- role: role?.toString(),
589
- postParentId: parentId?.toString(),
590
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
591
- limit: MESSAGES_PER_PAGE,
592
- skip: threadMessages.length,
593
- },
594
- })
595
- .then((res: any) => {
596
- if (res?.data?.getPostThread) {
597
- const threads: any = res?.data?.getPostThread;
598
- const threadReplies = threads?.replies ?? [];
599
-
600
- console.log('Successfully loaded more messages:', threadReplies.length);
601
-
602
- safeSend({
603
- type: 'FETCH_MORE_MESSAGES_SUCCESS',
604
- data: {
605
- messages: threadReplies,
606
- loadingOldMessages: false,
607
- },
608
- });
609
- } else {
610
- console.log('No thread data returned when loading more messages');
611
- safeSend({
612
- type: ThreadActions.STOP_LOADING,
613
- data: { loadingOldMessages: false },
614
- });
615
- }
616
- })
617
- .catch((error: any) => {
618
- console.error('Error fetching more messages:', error);
619
- safeSend({
620
- type: 'ERROR',
621
- data: {
622
- message: error.message,
623
- loadingOldMessages: false,
624
- },
625
- });
626
- });
627
- } else {
628
- console.log('No more messages to load or already loading');
629
- }
630
- }, [parentId, channelId, state.context]);
631
-
632
- let onScroll = false;
633
-
634
- const handleScrollToTop = ({ nativeEvent }: any) => {
635
- // Check if we're near the top of the list
636
- if (isCloseToTop(nativeEvent)) {
637
- if (
638
- !safeContextProperty('loadingOldMessages', false) &&
639
- safeContextProperty('totalCount', 0) > safeContextProperty('threadMessages', []).length
640
- ) {
641
- console.log('Near top of list - loading older messages');
642
- safeSend({ type: ThreadActions.FETCH_MORE_MESSAGES });
643
- }
644
- }
645
- };
646
-
647
- const handleEndReached = () => {
648
- // This triggers when scrolled to the bottom
649
- console.log('Reached end of message list');
650
- };
651
-
652
- const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
653
- const paddingToTop = 80;
654
- return contentOffset.y <= paddingToTop;
655
- };
656
-
657
- const onSelectImages = async () => {
658
- try {
659
- safeSend({ type: ThreadActions.START_LOADING });
660
-
661
- const imageSource = await ImagePicker.launchImageLibraryAsync({
662
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
663
- allowsEditing: true,
664
- aspect: [4, 3],
665
- quality: 0.8, // Reduced from 1 for better performance
666
- base64: true,
667
- allowsMultipleSelection: false, // Set to true if you want to support multiple images
668
- });
669
-
670
- if (imageSource.canceled) {
671
- console.log('Image selection was canceled');
672
- safeSend({ type: ThreadActions.STOP_LOADING });
673
- return;
674
- }
675
-
676
- if (!imageSource.assets || imageSource.assets.length === 0 || !imageSource.assets[0]?.base64) {
677
- console.error('No valid image data received');
678
- safeSend({
679
- type: 'ERROR',
680
- data: { message: 'No valid image data received' },
681
- });
682
- return;
683
- }
684
-
685
- // Get the first asset
686
- const asset = imageSource.assets[0];
687
-
688
- // Derive file extension from mime type or default to jpg
689
- const fileExtension = asset.mimeType ? asset.mimeType.split('/').pop() || 'jpg' : 'jpg';
690
-
691
- // Create a more descriptive filename with timestamp
692
- const filename = `image_${Date.now()}.${fileExtension}`;
693
-
694
- // Create data URL with proper mime type
695
- const mimeType = asset.mimeType || 'image/jpeg';
696
- const image = `data:${mimeType};base64,${asset.base64}`;
697
-
698
- // Create file-like object suitable for React Native
699
- const fileData = {
700
- uri: asset.uri,
701
- type: mimeType,
702
- name: filename,
703
- base64: asset.base64,
704
- };
705
-
706
- console.log(`Selected image: ${filename}, type: ${mimeType}`);
707
-
708
- safeSend({
709
- type: ThreadActions.SET_IMAGE,
710
- data: {
711
- image,
712
- files: [fileData],
713
- images: [asset as ImagePicker.ImagePickerAsset],
714
- },
715
- });
716
- } catch (error) {
717
- console.error('Error selecting image:', error);
718
- safeSend({
719
- type: 'ERROR',
720
- data: { message: error.message || 'Failed to select image' },
721
- });
722
- }
723
- };
724
-
725
- // Define message sending handlers
726
- const sendThreadMessageHandler = useCallback(
727
- async (message: string) => {
728
- console.log('Sending message:', message);
729
-
730
- if (!channelId) {
731
- console.error('No channelId provided');
732
- return;
733
- }
734
-
735
- // Allow empty messages with spaces or blank content - GiftedChat sometimes sends these
736
- // But use the actual message if available
737
- const messageContent = message?.trim() || ' ';
738
- console.log('Using message content for sending:', messageContent);
739
-
740
- const postId = objectId();
741
- console.log('Generated postId:', postId);
742
-
743
- safeSend({ type: ThreadActions.START_LOADING });
744
-
745
- try {
746
- console.log('Sending mutation with variables:', {
747
- channelId,
748
- postThreadId: safeContextProperty('postThread')?.id,
749
- postParentId: !parentId || parentId == 0 ? null : parentId,
750
- message: messageContent,
751
- });
752
-
753
- const result = await sendThreadMessage({
754
- variables: {
755
- channelId,
756
- postThreadId: safeContextProperty('postThread') && safeContextProperty('postThread')?.id,
757
- postParentId: !parentId || parentId == 0 ? null : parentId,
758
- threadMessageInput: {
759
- content: messageContent,
760
- role,
761
- },
762
- },
763
- update: (cache, { data, errors }: any) => {
764
- console.log('Send message update callback - data:', data, 'errors:', errors);
765
-
766
- if (!data || errors) {
767
- console.error('Send message failed:', errors);
768
- safeSend({ type: ThreadActions.STOP_LOADING });
769
- return;
770
- }
771
-
772
- console.log('Message sent successfully:', data?.createPostThread?.lastMessage);
773
-
774
- // Add the new message to our local state
775
- const newMessage = data?.createPostThread?.lastMessage;
776
-
777
- // Reset the message text and add the new message
778
- safeSend({
779
- type: 'SEND_THREAD_MESSAGE_SUCCESS',
780
- data: {
781
- message: newMessage,
782
- messageText: '', // Clear the message text now
783
- },
784
- });
785
-
786
- if (!parentId || parentId == 0) {
787
- console.log('Setting new parentId:', data?.createPostThread?.lastMessage?.id);
788
- setParentId(data?.createPostThread?.lastMessage?.id);
789
- }
790
-
791
- setChannelToTop(channelToTop + 1);
792
-
793
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
794
- sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
795
- },
796
- });
797
-
798
- console.log('Send mutation result:', result);
799
- } catch (error) {
800
- console.error('Error sending message:', error);
801
- safeSend({
802
- type: 'ERROR',
803
- data: { message: error.message || 'Failed to send message' },
804
- });
805
- }
806
- },
807
- [channelId, parentId, state.context, role],
808
- );
809
-
810
- const sendThreadMessageWithFileHandler = useCallback(
811
- async (message: string, images: any[]) => {
812
- console.log('Sending message with file:', message, 'Images:', images.length);
813
-
814
- if (!channelId) {
815
- console.error('No channelId provided');
816
- return;
817
- }
818
-
819
- if (images.length === 0) {
820
- console.error('No images to send');
821
- return;
822
- }
823
-
824
- // Allow empty message content for file uploads
825
- // But use the actual message if available
826
- const messageContent = message?.trim() || ' ';
827
- console.log('Using message content for file send:', messageContent);
828
-
829
- const postId = objectId();
830
- console.log('Generated postId for file upload:', postId);
831
-
832
- try {
833
- // Prepare image assets in the format expected by the upload service
834
- const preparedImages = images.map((img) => ({
835
- uri: img.uri,
836
- type: img.mimeType || 'image/jpeg',
837
- name: img.fileName || `image_${Date.now()}.jpg`,
838
- base64: img.base64,
839
- width: img.width || 0,
840
- height: img.height || 0,
841
- })) as ImagePicker.ImagePickerAsset[];
842
-
843
- console.log('Starting file upload with prepared images:', preparedImages.length);
844
-
845
- const uploadResponse = await startUpload({
846
- file: preparedImages,
847
- saveUploadedFile: {
848
- variables: {
849
- postId,
850
- },
851
- },
852
- createUploadLink: {
853
- variables: {
854
- postId,
855
- },
856
- },
857
- });
858
-
859
- if (uploadResponse?.error) {
860
- console.error('File upload failed:', uploadResponse.error);
861
- safeSend({ type: ThreadActions.STOP_LOADING });
862
- return;
863
- }
864
-
865
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
866
- console.log('Files uploaded successfully:', uploadedFiles?.length);
867
-
868
- if (uploadResponse.data) {
869
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
870
- console.log('File IDs for message:', files);
871
-
872
- console.log('Sending message with attached files');
873
- const result = await sendThreadMessage({
874
- variables: {
875
- postId,
876
- channelId,
877
- postThreadId: safeContextProperty('postThread') && safeContextProperty('postThread')?.id,
878
- postParentId: !parentId || parentId == 0 ? null : parentId,
879
- threadMessageInput: {
880
- content: messageContent,
881
- files,
882
- role,
883
- },
884
- },
885
- update: (cache, { data, errors }: any) => {
886
- console.log('Send message with file update callback - data:', data, 'errors:', errors);
887
-
888
- if (!data || errors) {
889
- console.error('Send message with file failed:', errors);
890
- safeSend({ type: ThreadActions.STOP_LOADING });
891
- return;
892
- }
893
-
894
- console.log('Message with file sent successfully:', data?.createPostThread?.lastMessage);
895
-
896
- // Add the new message to our local state
897
- const newMessage = data?.createPostThread?.lastMessage;
898
-
899
- safeSend({
900
- type: 'SEND_THREAD_MESSAGE_WITH_FILE_SUCCESS',
901
- data: {
902
- message: newMessage,
903
- messageText: '', // Clear the message text now
904
- },
905
- });
906
-
907
- if (!parentId || parentId == 0) {
908
- console.log('Setting new parentId:', data?.createPostThread?.lastMessage?.id);
909
- setParentId(data?.createPostThread?.lastMessage?.id);
910
- }
911
-
912
- setChannelToTop(channelToTop + 1);
913
-
914
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
915
- sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
916
- },
917
- });
918
-
919
- console.log('Send with file mutation result:', result);
920
- }
921
- } catch (error) {
922
- console.error('Error sending message with file:', error);
923
- safeSend({
924
- type: 'ERROR',
925
- data: { message: error.message || 'Failed to send message with file' },
926
- });
927
- }
928
- },
929
- [channelId, parentId, state.context, role, startUpload],
930
- );
931
-
932
- const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
933
- const notificationData: IExpoNotificationData = {
934
- url: config.THREAD_MESSEGE_PATH,
935
- params: { channelId, title: params?.title ?? 'Thread', postParentId: parentId, hideTabBar: true },
936
- screen: 'DialogThreadMessages',
937
- thread: { id: threadId },
938
- other: { sound: Platform.OS === 'android' ? undefined : 'default' },
939
- };
940
- if (parentId || parentId == 0) {
941
- await sendExpoNotificationOnPostMutation({
942
- variables: {
943
- postId: messageId?.toString(),
944
- notificationData,
945
- },
946
- });
947
- }
948
- };
949
-
950
- const messageList = useMemo(() => {
951
- const threadMessages = safeContextProperty('threadMessages', []);
952
- console.log(`Creating message list from ${threadMessages.length} thread messages`);
953
-
954
- let res: any = [];
955
- if (threadMessages?.length) {
956
- // We need to convert the threadMessages into the format expected by GiftedChat
957
- // Use a Set to track IDs and prevent duplicates
958
- const messageIds = new Set();
959
-
960
- res = threadMessages
961
- .filter((msg) => {
962
- // Skip duplicate IDs
963
- if (!msg.id || messageIds.has(msg.id)) {
964
- console.log('Skipping duplicate message ID:', msg.id);
965
- return false;
966
- }
967
- messageIds.add(msg.id);
968
- return true;
969
- })
970
- .map((msg) => {
971
- // Generate a unique _id if needed by combining id and createdAt
972
- const uniqueId = msg.id || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
973
-
974
- let message: IMessageProps = {
975
- _id: uniqueId,
976
- text: msg.message || '',
977
- createdAt: new Date(msg.createdAt),
978
- user: {
979
- _id: msg?.author?.id ?? auth?.profile?.id,
980
- name:
981
- msg?.author?.givenName ??
982
- auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
983
- auth?.profile?.family_name,
984
- avatar: msg?.author?.picture ?? auth?.profile?.picture,
985
- },
986
- type: msg?.type || '',
987
- image: msg?.files?.data?.[0]?.url,
988
- sent: msg?.isDelivered || true,
989
- received: msg?.isRead || false,
990
- propsConfiguration: msg?.propsConfiguration,
991
- };
992
- return message;
993
- });
994
- }
995
-
996
- // Sort messages by date (newest first as required by GiftedChat)
997
- const sortedMessages = res.sort((a, b) => b.createdAt - a.createdAt);
998
- return sortedMessages;
999
- }, [safeContextProperty('threadMessages'), auth]);
1000
-
1001
- const renderSend = (props) => {
1002
- // Check if there's an image selected
1003
- const hasImage = safeContextProperty('selectedImage', '') !== '';
1004
-
1005
- // Enable send button if there's text OR an image
1006
- const isDisabled = !hasImage && (!props.text || props.text.trim().length === 0);
1007
-
1008
- return (
1009
- <Send
1010
- {...props}
1011
- containerStyle={{
1012
- alignItems: 'center',
1013
- justifyContent: 'center',
1014
- marginHorizontal: 4,
1015
- marginBottom: 0,
1016
- }}
1017
- disabled={isDisabled}
1018
- >
1019
- <Box
1020
- style={{
1021
- width: 32,
1022
- height: 32,
1023
- alignItems: 'center',
1024
- justifyContent: 'center',
1025
- }}
1026
- >
1027
- <MaterialCommunityIcons
1028
- name="send-circle"
1029
- size={30}
1030
- color={isDisabled ? colors.gray[400] : colors.blue[500]}
1031
- />
1032
- </Box>
1033
- </Send>
1034
- );
1035
- };
1036
-
1037
- const renderMessageText = (props: any) => {
1038
- const { currentMessage } = props;
1039
- if (currentMessage.type === 'ALERT') {
1040
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1041
- let action: string = '';
1042
- let actionId: any = '';
1043
- let params: any = {};
1044
- if (attachment?.callToAction?.extraParams) {
1045
- const extraParams: any = attachment?.callToAction?.extraParams;
1046
- const route: any = extraParams?.route ?? null;
1047
- let path: any = null;
1048
- let param: any = null;
1049
- if (role && role == PreDefinedRole.Guest) {
1050
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
1051
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
1052
- } else if (role && role == PreDefinedRole.Owner) {
1053
- path = route?.host?.name ? route?.host?.name ?? null : null;
1054
- param = route?.host?.params ? route?.host?.params ?? null : null;
1055
- } else {
1056
- path = route?.host?.name ? route?.host?.name ?? null : null;
1057
- param = route?.host?.params ? route?.host?.params ?? null : null;
1058
- }
1059
-
1060
- action = path;
1061
- params = { ...param };
1062
- } else if (attachment?.callToAction?.link) {
1063
- action = CALL_TO_ACTION_PATH;
1064
- actionId = attachment?.callToAction?.link.split('/').pop();
1065
- params = { reservationId: actionId };
1066
- }
1067
-
1068
- return (
1069
- <>
1070
- {attachment?.callToAction && action ? (
1071
- <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
1072
- <Button
1073
- variant={'outline'}
1074
- size={'sm'}
1075
- className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
1076
- onPress={() => action && params && navigation.navigate(action, params)}
1077
- >
1078
- <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
1079
- {attachment.callToAction.title}
1080
- </ButtonText>
1081
- </Button>
1082
- <MessageText
1083
- {...props}
1084
- textStyle={{
1085
- left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1086
- }}
1087
- />
1088
- </Box>
1089
- ) : (
1090
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1091
- )}
1092
- </>
1093
- );
1094
- } else {
1095
- return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
1096
- }
1097
- };
1098
-
1099
- const renderActions = (props) => {
1100
- return (
1101
- <Actions
1102
- {...props}
1103
- options={{
1104
- ['Choose from Library']: onSelectImages,
1105
- ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1106
- }}
1107
- optionTintColor="#000000"
1108
- cancelButtonIndex={1} // Set the Cancel option as the cancel button
1109
- icon={() => (
1110
- <Box
1111
- style={{
1112
- width: 32,
1113
- height: 32,
1114
- alignItems: 'center',
1115
- justifyContent: 'center',
1116
- }}
1117
- >
1118
- <Ionicons name="image" size={24} color={colors.blue[500]} />
1119
- </Box>
1120
- )}
1121
- containerStyle={{
1122
- alignItems: 'center',
1123
- justifyContent: 'center',
1124
- marginLeft: 8,
1125
- marginBottom: 0,
1126
- }}
1127
- />
1128
- );
1129
- };
1130
-
1131
- const renderAccessory = (props) => {
1132
- const selectedImage = safeContextProperty('selectedImage', '');
1133
-
1134
- if (!selectedImage) {
1135
- return null;
1136
- }
1137
-
1138
- return (
1139
- <View
1140
- style={{
1141
- height: 80,
1142
- padding: 10,
1143
- backgroundColor: 'white',
1144
- borderTopWidth: 1,
1145
- borderTopColor: '#e0e0e0',
1146
- flexDirection: 'row',
1147
- alignItems: 'center',
1148
- }}
1149
- >
1150
- <View
1151
- style={{
1152
- flex: 1,
1153
- flexDirection: 'row',
1154
- alignItems: 'center',
1155
- // justifyContent: 'space-between',
1156
- paddingHorizontal: 20,
1157
- }}
1158
- >
1159
- <Image
1160
- key={state?.context?.selectedImage}
1161
- alt={'selected image'}
1162
- source={{ uri: state?.context?.selectedImage }}
1163
- size={'xs'}
1164
- style={{
1165
- width: 5,
1166
- height: 5,
1167
- borderRadius: 5,
1168
- marginRight: 20,
1169
- }}
1170
- />
1171
-
1172
- <TouchableHighlight
1173
- underlayColor="#dddddd"
1174
- onPress={() => safeSend({ type: ThreadActions.CLEAR_IMAGE })}
1175
- style={{
1176
- backgroundColor: '#f44336',
1177
- paddingVertical: 2,
1178
- paddingHorizontal: 5,
1179
- borderRadius: 5,
1180
- marginLeft: 10,
1181
- elevation: 3,
1182
- shadowColor: '#000',
1183
- shadowOffset: { width: 0, height: 1 },
1184
- shadowOpacity: 0.3,
1185
- shadowRadius: 2,
1186
- }}
1187
- >
1188
- <Text style={{ color: 'white', fontWeight: 'bold' }}>X</Text>
1189
- </TouchableHighlight>
1190
- </View>
1191
- </View>
1192
- );
1193
- };
1194
-
1195
- const setImageViewerObject = (obj: any, v: boolean) => {
1196
- setImageObject(obj);
1197
- setImageViewer(v);
1198
- };
1199
-
1200
- const modalContent = React.useMemo(() => {
1201
- if (!imageObject) return <></>;
1202
- const { image, _id } = imageObject;
1203
- return (
1204
- <CachedImage
1205
- style={{ width: '100%', height: '100%' }}
1206
- resizeMode={'cover'}
1207
- cacheKey={`${_id}-slack-bubble-imageKey`}
1208
- source={{
1209
- uri: image,
1210
- expiresIn: 86400,
1211
- }}
1212
- alt={'image'}
1213
- />
1214
- );
1215
- }, [imageObject]);
1216
-
1217
- const renderMessage = (props: any) => {
1218
- return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
1219
- };
1220
-
1221
- // Define a memo that provides the current message text from state
1222
- const currentMessageText = useMemo(() => {
1223
- const text = safeContextProperty('messageText', '') || ' ';
1224
- return text;
1225
- }, [safeContextProperty('messageText')]);
1226
-
1227
- // Define a custom renderInputToolbar function
1228
- const renderInputToolbar = (props) => {
1229
- return (
1230
- <InputToolbar
1231
- {...props}
1232
- containerStyle={{
1233
- backgroundColor: 'white',
1234
- borderTopWidth: 1,
1235
- borderTopColor: colors.gray[200],
1236
- paddingHorizontal: 4,
1237
- paddingVertical: 4,
1238
- }}
1239
- primaryStyle={{
1240
- alignItems: 'center',
1241
- }}
1242
- />
1243
- );
1244
- };
1245
-
1246
- return (
1247
- <SafeAreaView style={{ flex: 1 }}>
1248
- {safeContextProperty('loadingOldMessages', false) && (
1249
- <Box className="absolute top-10 left-0 right-0 z-10 items-center">
1250
- <Box className="bg-blue-500/20 rounded-full px-4 py-2 flex-row items-center">
1251
- <Spinner color={colors.blue[500]} size="small" />
1252
- <Text className="text-sm font-medium color-blue-600 ml-2">Loading messages...</Text>
1253
- </Box>
1254
- </Box>
1255
- )}
1256
- {isPostParentIdThread && (
1257
- <>
1258
- {safeContextProperty('threadPost', [])?.length > 0 && (
1259
- <>
1260
- <VStack className="px-2 pt-2 pb-0" space={'sm'}>
1261
- <HStack space={'sm'} className="items-center">
1262
- <Avatar className="bg-transparent" size={'md'}>
1263
- <AvatarFallbackText>
1264
- {startCase(
1265
- safeContextProperty('threadPost')[0]?.author?.username?.charAt(0),
1266
- )}
1267
- </AvatarFallbackText>
1268
- <AvatarImage
1269
- alt="image"
1270
- style={{
1271
- borderRadius: 6,
1272
- borderWidth: 2,
1273
- borderColor: '#fff',
1274
- }}
1275
- source={{
1276
- uri: safeContextProperty('threadPost')[0]?.author?.picture,
1277
- }}
1278
- />
1279
- </Avatar>
1280
- <Box>
1281
- <Text className="font-bold color-black">
1282
- {safeContextProperty('threadPost')[0]?.author?.givenName ?? ''}{' '}
1283
- {safeContextProperty('threadPost')[0]?.author?.familyName ?? ''}
1284
- </Text>
1285
- <Text className="pl-0 color-gray-500">
1286
- {createdAtText(safeContextProperty('threadPost')[0]?.createdAt)} at{' '}
1287
- {format(
1288
- new Date(safeContextProperty('threadPost')[0]?.createdAt),
1289
- 'hh:ss:a',
1290
- )}
1291
- </Text>
1292
- </Box>
1293
- </HStack>
1294
- <HStack space={'sm'} className="px-2 items-center">
1295
- <Text>{safeContextProperty('threadPost')[0]?.message ?? ''}</Text>
1296
- </HStack>
1297
- </VStack>
1298
-
1299
- <Box className="py-4">
1300
- <Box className="px-4 py-2 border-t border-b border-gray-200">
1301
- <Text className="font-bold color-gray-600">
1302
- {safeContextProperty('threadPost')[0]?.replies?.totalCount}{' '}
1303
- {safeContextProperty('threadPost')[0]?.replies?.totalCount > 0
1304
- ? 'replies'
1305
- : 'reply'}
1306
- </Text>
1307
- </Box>
1308
- </Box>
1309
- </>
1310
- )}
1311
- </>
1312
- )}
1313
- <GiftedChat
1314
- ref={threadMessageListRef}
1315
- wrapInSafeArea={false}
1316
- renderLoading={() => <Spinner color={colors.blue[500]} />}
1317
- messages={messageList}
1318
- listViewProps={{
1319
- onScroll: handleScrollToTop,
1320
- onEndReached: handleEndReached,
1321
- onEndReachedThreshold: 0.2,
1322
- contentContainerStyle: {
1323
- paddingBottom: 10,
1324
- },
1325
- maintainVisibleContentPosition: {
1326
- minIndexForVisible: 0,
1327
- autoscrollToTopThreshold: 100,
1328
- },
1329
- scrollEventThrottle: 100,
1330
- keyboardDismissMode: 'on-drag',
1331
- keyboardShouldPersistTaps: 'handled',
1332
- }}
1333
- onSend={(messages) => {
1334
- if (!messages || messages.length === 0) {
1335
- console.log('No messages to send');
1336
- return;
1337
- }
1338
-
1339
- // Use the actual message text from the state, not the one from GiftedChat
1340
- // GiftedChat sometimes sends blank messages even when there's text in the input
1341
- const currentInputText = currentMessageText;
1342
- const messageToSend = currentInputText?.trim() || messages[0]?.text?.trim() || ' ';
1343
-
1344
- console.log('GiftedChat onSend triggered with text from state:', messageToSend);
1345
-
1346
- // Make sure we update the message text in state
1347
- safeSend({
1348
- type: ThreadActions.SET_MESSAGE_TEXT,
1349
- data: { messageText: '' },
1350
- });
1351
-
1352
- // Then send the message
1353
- if (safeContextProperty('images', []).length > 0) {
1354
- console.log(
1355
- 'Sending message with file:',
1356
- messageToSend,
1357
- 'Images:',
1358
- safeContextProperty('images', []).length,
1359
- );
1360
- safeSend({
1361
- type: ThreadActions.SEND_THREAD_MESSAGE_WITH_FILE,
1362
- data: { messageText: messageToSend },
1363
- });
1364
- } else {
1365
- console.log('Sending text message:', messageToSend);
1366
- safeSend({
1367
- type: ThreadActions.SEND_THREAD_MESSAGE,
1368
- data: { messageText: messageToSend },
1369
- });
1370
- }
1371
- }}
1372
- text={currentMessageText}
1373
- onInputTextChanged={(text) => {
1374
- // Don't log every keystroke to reduce console spam
1375
- if (text.length % 5 === 0 || text.length < 5) {
1376
- console.log('Input text changed:', text);
1377
- }
1378
-
1379
- // Set the text in the state
1380
- safeSend({
1381
- type: ThreadActions.SET_MESSAGE_TEXT,
1382
- data: { messageText: text },
1383
- });
1384
- }}
1385
- renderFooter={() =>
1386
- safeContextProperty('loading', false) && !safeContextProperty('loadingOldMessages', false) ? (
1387
- <Box className="w-full py-2 items-center">
1388
- <Spinner color={colors.blue[500]} />
1389
- </Box>
1390
- ) : safeContextProperty('imageLoading', false) ? (
1391
- <Box className="w-full py-2 items-center">
1392
- <Spinner color={colors.blue[500]} />
1393
- </Box>
1394
- ) : (
1395
- <></>
1396
- )
1397
- }
1398
- scrollToBottom
1399
- loadEarlier={false}
1400
- isLoadingEarlier={false}
1401
- user={{
1402
- _id: auth?.id || '',
1403
- }}
1404
- isTyping={true}
1405
- alwaysShowSend={safeContextProperty('loading', false) ? false : true}
1406
- infiniteScroll={true}
1407
- renderSend={renderSend}
1408
- renderInputToolbar={renderInputToolbar}
1409
- minInputToolbarHeight={50}
1410
- renderActions={renderActions}
1411
- renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
1412
- renderMessage={renderMessage}
1413
- maxInputLength={1000}
1414
- placeholder="Type a message..."
1415
- showUserAvatar={true}
1416
- showAvatarForEveryMessage={false}
1417
- inverted={true}
1418
- parsePatterns={(linkStyle) => [
1419
- {
1420
- type: 'url',
1421
- style: { ...linkStyle, color: colors.blue[500] },
1422
- onPress: (url) => Linking.openURL(url),
1423
- },
1424
- {
1425
- type: 'phone',
1426
- style: { ...linkStyle, color: colors.blue[500] },
1427
- onPress: (phone) => Linking.openURL(`tel:${phone}`),
1428
- },
1429
- {
1430
- type: 'email',
1431
- style: { ...linkStyle, color: colors.blue[500] },
1432
- onPress: (email) => Linking.openURL(`mailto:${email}`),
1433
- },
1434
- ]}
1435
- textInputProps={{
1436
- style: {
1437
- borderWidth: 1,
1438
- borderColor: colors.gray[300],
1439
- backgroundColor: '#f8f8f8',
1440
- borderRadius: 20,
1441
- minHeight: 40,
1442
- maxHeight: 80,
1443
- color: '#000',
1444
- padding: 10,
1445
- paddingHorizontal: 20,
1446
- fontSize: 16,
1447
- flex: 1,
1448
- },
1449
- multiline: true,
1450
- returnKeyType: 'default',
1451
- enablesReturnKeyAutomatically: true,
1452
- placeholderTextColor: colors.gray[400],
1453
- }}
1454
- minComposerHeight={44}
1455
- isKeyboardInternallyHandled={true}
1456
- bottomOffset={Platform.OS === 'ios' ? 20 : 0}
1457
- renderChatFooter={() => (
1458
- <>
1459
- <ImageViewerModal
1460
- isVisible={isShowImageViewer}
1461
- setVisible={setImageViewer}
1462
- modalContent={modalContent}
1463
- />
1464
- <SubscriptionHandler
1465
- channelId={channelId}
1466
- subscribeToNewMessages={() =>
1467
- subscribeToMore({
1468
- document: CHAT_MESSAGE_ADDED,
1469
- variables: {
1470
- channelId: channelId?.toString(),
1471
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
1472
- },
1473
- updateQuery: (prev, { subscriptionData }: any) => {
1474
- if (!subscriptionData.data) return prev;
1475
- const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
1476
- const prevReplyCount: any = prev?.getPostThread?.replyCount;
1477
- const newReplyCount = prevReplyCount || 0 + 1;
1478
- const replies = prev?.getPostThread?.replies || [];
1479
-
1480
- safeSend({
1481
- type: ThreadActions.SET_THREAD_MESSAGES,
1482
- data: {
1483
- messages: uniqBy(
1484
- [...safeContextProperty('threadMessages', []), newMessage],
1485
- ({ id }) => id,
1486
- ),
1487
- totalCount: newReplyCount,
1488
- threadPost: safeContextProperty('threadPost', []),
1489
- postThread: safeContextProperty('postThread', null),
1490
- },
1491
- });
1492
-
1493
- return Object.assign({}, prev, {
1494
- getPostThread: {
1495
- ...prev?.getPostThread,
1496
- lastReplyAt: newMessage.createdAt,
1497
- replies: [newMessage, ...replies],
1498
- replyCount: newReplyCount,
1499
- updatedAt: newMessage.createdAt,
1500
- },
1501
- });
1502
- },
1503
- })
1504
- }
1505
- />
1506
- </>
1507
- )}
1508
- messagesContainerStyle={messageList?.length == 0 && { transform: [{ scaleY: -1 }] }}
1509
- renderChatEmpty={() => (
1510
- <>
1511
- {!threadLoading && messageList && messageList?.length == 0 && (
1512
- <Box className="p-5">
1513
- <Center className="mt-6">
1514
- <Ionicons name="chatbubbles" size={30} />
1515
- <Text>You don't have any message yet!</Text>
1516
- </Center>
1517
- </Box>
1518
- )}
1519
- </>
1520
- )}
1521
- lightboxProps={{
1522
- underlayColor: 'transparent',
1523
- springConfig: { tension: 90000, friction: 90000 },
1524
- disabled: true,
1525
- }}
1526
- />
1527
- </SafeAreaView>
1528
- );
1529
- };
1530
-
1531
- const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: IThreadSubscriptionHandlerProps) => {
1532
- useEffect(() => subscribeToNewMessages(), [channelId]);
1533
- return <></>;
1534
- };
1535
-
1536
- export const ThreadConversationView = React.memo(ThreadConversationViewComponent);
1537
- // export const ThreadConversationView = ThreadConversationViewComponent;