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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/routes.json +14 -1
  3. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  4. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  5. package/lib/screens/inbox/components/CachedImage/index.js +125 -16
  6. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  7. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +32 -21
  8. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  9. package/lib/screens/inbox/containers/ConversationView.js +1175 -400
  10. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  11. package/lib/screens/inbox/containers/Dialogs.js +290 -21
  12. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  13. package/lib/screens/inbox/containers/ThreadConversationView.js +858 -351
  14. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  15. package/lib/screens/inbox/containers/workflow/conversation-xstate.js +380 -0
  16. package/lib/screens/inbox/containers/workflow/conversation-xstate.js.map +1 -0
  17. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +235 -0
  18. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +1 -0
  19. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js +438 -0
  20. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js.map +1 -0
  21. package/package.json +4 -4
  22. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  23. package/src/screens/inbox/components/CachedImage/index.tsx +137 -17
  24. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +35 -9
  25. package/src/screens/inbox/containers/ConversationView.tsx +1510 -641
  26. package/src/screens/inbox/containers/Dialogs.tsx +415 -123
  27. package/src/screens/inbox/containers/ThreadConversationView.tsx +1053 -288
  28. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  29. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  30. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  31. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
@@ -14,14 +14,14 @@ import {
14
14
  Spinner,
15
15
  Text,
16
16
  } from '@admin-layout/gluestack-ui-mobile';
17
- import { Platform } from 'react-native';
17
+ import { Platform, Linking, SafeAreaView, View, TouchableHighlight } from 'react-native';
18
18
  import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
19
19
  import { useSelector } from 'react-redux';
20
20
  import { orderBy, startCase, uniqBy } from 'lodash-es';
21
21
  import * as ImagePicker from 'expo-image-picker';
22
22
  import { encode as atob } from 'base-64';
23
23
  import { Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
24
- import { Actions, GiftedChat, IMessage, MessageText, Send } from 'react-native-gifted-chat';
24
+ import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
25
25
  import { IPost, IPostThread, PreDefinedRole, IExpoNotificationData, IFileInfo } from 'common';
26
26
  import {
27
27
  OnThreadChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
@@ -40,6 +40,19 @@ import { config } from '../config';
40
40
  import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
41
41
  import CachedImage from '../components/CachedImage';
42
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
+ }
43
56
 
44
57
  const {
45
58
  MESSAGES_PER_PAGE,
@@ -57,6 +70,264 @@ const createdAtText = (value: string) => {
57
70
  return format(new Date(value), 'MMM dd, yyyy');
58
71
  };
59
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
+
60
331
  interface IMessageProps extends IMessage {
61
332
  type: string;
62
333
  propsConfiguration?: any;
@@ -77,34 +348,84 @@ interface IThreadSubscriptionHandlerProps {
77
348
  channelId: string;
78
349
  }
79
350
 
80
- const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any) => {
351
+ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any): JSX.Element => {
81
352
  const { params } = useRoute<any>();
82
353
  const [channelToTop, setChannelToTop] = useState(0);
83
- const [channelMessages, setChannelMessages] = useState<any>([]);
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
+
84
414
  const auth: any = useSelector(userSelector);
85
- const [totalCount, setTotalCount] = useState<any>(0);
86
415
  const [selectedImage, setImage] = useState<string>('');
87
- const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
88
- const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
89
416
  const navigation = useNavigation<any>();
90
417
  const [files, setFiles] = useState<File[]>([]);
91
418
  const [images, setImages] = useState<ImagePicker.ImagePickerAsset[]>([]);
92
- const [msg, setMsg] = useState<string>('');
93
- const [loading, setLoading] = useState(false);
94
- const [imageLoading, setImageLoading] = useState(false);
95
- const [expoTokens, setExpoTokens] = useState<any[]>([]);
96
419
  const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
97
420
  const [imageObject, setImageObject] = useState<any>({});
98
421
  const [parentId, setParentId] = useState<any>(postParentId);
99
- const [postThread, setPostThread] = useState<IPostThread>();
100
- const { startUpload } = useUploadFilesNative();
101
- const [threadPost, setThreadPost] = useState<any[]>([]);
102
- const [isScrollToBottom, setIsScrollToBottom] = useState(false);
422
+ const [expoTokens, setExpoTokens] = useState<any[]>([]);
103
423
  const threadMessageListRef = useRef<any>(null);
104
424
 
105
425
  // const [sendThreadMessage] = useSendThreadMessageMutation();
106
426
  const [sendThreadMessage] = useCreatePostThreadMutation();
107
427
  const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
428
+ const { startUpload } = useUploadFilesNative();
108
429
 
109
430
  const [
110
431
  getThreadMessages,
@@ -123,27 +444,61 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
123
444
  </Button>
124
445
  ),
125
446
  });
126
- if (postParentId) {
127
- refetchThreadMessages({
128
- channelId: channelId?.toString(),
129
- role: role?.toString(),
130
- postParentId: postParentId?.toString(),
131
- selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
132
- limit: MESSAGES_PER_PAGE,
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
+ },
133
457
  });
134
458
  }
459
+
135
460
  setParentId(postParentId);
136
461
 
137
462
  return () => {
138
- setTotalCount(0);
139
- setChannelMessages([]);
140
- setThreadPost([]);
463
+ safeSend({ type: ThreadActions.CLEAR_MESSAGES });
141
464
  };
142
465
  }, [postParentId]),
143
466
  );
144
467
 
468
+ // Effect for when in FetchThreadMessages state
145
469
  useEffect(() => {
146
- //if (parentId && parentId == 0) {
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(() => {
147
502
  if (channelId && parentId) {
148
503
  getThreadMessages({
149
504
  variables: {
@@ -153,9 +508,34 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
153
508
  selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
154
509
  limit: MESSAGES_PER_PAGE,
155
510
  },
156
- });
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
+ });
157
537
  }
158
- }, [parentId]);
538
+ }, [channelId, parentId, role]);
159
539
 
160
540
  React.useEffect(() => {
161
541
  if (data?.getPostThread) {
@@ -163,42 +543,45 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
163
543
  const threadPost = threads?.post ?? [];
164
544
  const threadReplies = threads?.replies ?? [];
165
545
  const messeageTotalCount = threads?.replyCount ?? 0;
166
- const messages = [threadPost, ...threadReplies];
546
+ const messages = [...threadReplies];
167
547
 
168
- if (
169
- (messages && messages.length > 0 && messeageTotalCount > totalCount) ||
170
- (messages && messages.length > 0 && channelMessages.length === 0)
171
- ) {
172
- setThreadMessages(messages, messeageTotalCount);
173
- }
548
+ safeSend({
549
+ type: ThreadActions.SET_THREAD_MESSAGES,
550
+ data: {
551
+ messages,
552
+ totalCount: messeageTotalCount,
553
+ threadPost: threadPost,
554
+ postThread: threads,
555
+ },
556
+ });
174
557
  }
175
- if (isScrollToBottom && channelMessages) scrollToBottom();
176
- // scrollToBottom();
177
- // if (!isPostParentIdThread) {
178
- // // setTotalCount((pc: any) => pc + threadTotalCount);
179
- // setChannelMessages((oldMessages: any) => uniqBy([...threadMessage, ...oldMessages], ({ id }) => id));
180
- // }
181
- }, [data, channelMessages, loadingOldMessages, totalCount, isPostParentIdThread, isScrollToBottom]);
182
-
183
- const setThreadMessages = (messages: any, messagesTotalCount: number) => {
184
- setChannelMessages((oldMessages: any) => uniqBy([...messages, ...oldMessages], ({ id }) => id));
185
- setTotalCount(messagesTotalCount);
186
- };
558
+ }, [data]);
187
559
 
188
560
  React.useEffect(() => {
189
- if (selectedImage) setImageLoading(false);
190
- }, [selectedImage]);
561
+ if (safeContextProperty('selectedImage')) {
562
+ safeSend({ type: ThreadActions.STOP_LOADING });
563
+ }
564
+ }, [safeContextProperty('selectedImage')]);
191
565
 
192
566
  const scrollToBottom = React.useCallback(() => {
193
567
  if (threadMessageListRef?.current) {
194
- setIsScrollToBottom(false);
195
- threadMessageListRef.current.scrollTop = threadMessageListRef.current.scrollHeight;
568
+ threadMessageListRef.current.scrollToBottom();
196
569
  }
197
570
  }, [threadMessageListRef]);
198
571
 
199
572
  const onFetchOld = useCallback(() => {
200
- if (totalCount > channelMessages.length && !loadingOldMessages) {
201
- setLoadEarlierMsg(true);
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
+
202
585
  fetchMoreMessages({
203
586
  variables: {
204
587
  channelId: channelId?.toString(),
@@ -206,79 +589,261 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
206
589
  postParentId: parentId?.toString(),
207
590
  selectedFields: 'id channel post replies replyCount lastReplyAt createdAt updatedAt',
208
591
  limit: MESSAGES_PER_PAGE,
209
- skip: channelMessages.length - 1,
592
+ skip: threadMessages.length,
210
593
  },
211
594
  })
212
595
  .then((res: any) => {
213
596
  if (res?.data?.getPostThread) {
214
597
  const threads: any = res?.data?.getPostThread;
215
598
  const threadReplies = threads?.replies ?? [];
216
- const messeageTotalCount = threads?.replyCount ?? 0;
217
- setThreadMessages(threadReplies, messeageTotalCount);
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
+ });
218
615
  }
219
616
  })
220
- .finally(() => {
221
- setLoadEarlierMsg(false);
222
- setLoadingOldMessages(false);
223
- })
224
617
  .catch((error: any) => {
225
- setLoadEarlierMsg(false);
226
- setLoadingOldMessages(false);
618
+ console.error('Error fetching more messages:', error);
619
+ safeSend({
620
+ type: 'ERROR',
621
+ data: {
622
+ message: error.message,
623
+ loadingOldMessages: false,
624
+ },
625
+ });
227
626
  });
627
+ } else {
628
+ console.log('No more messages to load or already loading');
228
629
  }
229
- }, [parentId, channelId, totalCount, channelMessages]);
630
+ }, [parentId, channelId, state.context]);
230
631
 
231
- // const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
232
- // return contentOffset.y <= 100; // 100px from top
233
- // };
632
+ let onScroll = false;
234
633
 
235
- const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
236
- const paddingToTop = 60;
237
- return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
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
+ }
238
645
  };
239
646
 
240
- const dataURLtoFile = (dataurl: any, filename: any) => {
241
- var arr = dataurl.split(','),
242
- mime = arr[0].match(/:(.*?);/)[1],
243
- bstr = atob(arr[1]),
244
- n = bstr.length,
245
- u8arr = new Uint8Array(n);
246
- while (n--) {
247
- u8arr[n] = bstr.charCodeAt(n);
248
- }
249
- return new File([u8arr], filename, { type: mime });
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;
250
655
  };
251
656
 
252
657
  const onSelectImages = async () => {
253
- setImageLoading(true);
254
- let imageSource: any = await ImagePicker.launchImageLibraryAsync({
255
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
256
- allowsEditing: true,
257
- aspect: [4, 3],
258
- quality: 1,
259
- base64: true,
260
- });
261
- if (!imageSource.canceled) {
262
- const image = 'data:image/jpeg;base64,' + imageSource.assets[0]?.base64;
263
- setImage(image);
264
- const file = dataURLtoFile(image, 'inputImage.jpg');
265
- setFiles((files) => files.concat(file));
266
- setImages((images) => images.concat(imageSource.assets[0] as ImagePicker.ImagePickerAsset));
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
+ });
267
722
  }
268
- if (imageSource.canceled) setLoading(false);
269
723
  };
270
724
 
271
- const handleSend = useCallback(
725
+ // Define message sending handlers
726
+ const sendThreadMessageHandler = useCallback(
272
727
  async (message: string) => {
273
- if (!channelId) return;
274
- if (!message && message != ' ' && images.length == 0) return;
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);
275
739
 
276
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);
277
844
 
278
- if (images && images.length > 0) {
279
- setLoading(true);
280
845
  const uploadResponse = await startUpload({
281
- file: images,
846
+ file: preparedImages,
282
847
  saveUploadedFile: {
283
848
  variables: {
284
849
  postId,
@@ -290,77 +855,78 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
290
855
  },
291
856
  },
292
857
  });
293
- if (uploadResponse?.error) setLoading(false);
858
+
859
+ if (uploadResponse?.error) {
860
+ console.error('File upload failed:', uploadResponse.error);
861
+ safeSend({ type: ThreadActions.STOP_LOADING });
862
+ return;
863
+ }
864
+
294
865
  const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
866
+ console.log('Files uploaded successfully:', uploadedFiles?.length);
867
+
295
868
  if (uploadResponse.data) {
296
- setImage('');
297
- setFiles([]);
298
- setImages([]);
299
- //setLoading(false);
300
869
  const files = uploadedFiles?.map((f: any) => f.id) ?? null;
301
- await sendThreadMessage({
870
+ console.log('File IDs for message:', files);
871
+
872
+ console.log('Sending message with attached files');
873
+ const result = await sendThreadMessage({
302
874
  variables: {
303
875
  postId,
304
876
  channelId,
305
- postThreadId: postThread && postThread?.id,
877
+ postThreadId: safeContextProperty('postThread') && safeContextProperty('postThread')?.id,
306
878
  postParentId: !parentId || parentId == 0 ? null : parentId,
307
879
  threadMessageInput: {
308
- content: message,
880
+ content: messageContent,
309
881
  files,
310
882
  role,
311
883
  },
312
884
  },
313
885
  update: (cache, { data, errors }: any) => {
886
+ console.log('Send message with file update callback - data:', data, 'errors:', errors);
887
+
314
888
  if (!data || errors) {
315
- setLoading(false);
889
+ console.error('Send message with file failed:', errors);
890
+ safeSend({ type: ThreadActions.STOP_LOADING });
316
891
  return;
317
892
  }
318
- setPostThread(data?.createPostThread?.data);
319
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
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
+
320
907
  if (!parentId || parentId == 0) {
908
+ console.log('Setting new parentId:', data?.createPostThread?.lastMessage?.id);
321
909
  setParentId(data?.createPostThread?.lastMessage?.id);
322
910
  }
323
911
 
324
912
  setChannelToTop(channelToTop + 1);
325
- setLoading(false);
326
- setMsg('');
327
- const msg = message == '' ? 'Send a file' : message;
913
+
914
+ const lastMessageId = data?.createPostThread?.lastMessage?.id;
328
915
  sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
329
916
  },
330
917
  });
331
- }
332
- } else {
333
- setLoading(true);
334
- await sendThreadMessage({
335
- variables: {
336
- channelId,
337
- postThreadId: postThread && postThread?.id,
338
- postParentId: !parentId || parentId == 0 ? null : parentId,
339
- threadMessageInput: {
340
- content: message,
341
- role,
342
- },
343
- },
344
- update: (cache, { data, errors }: any) => {
345
- if (!data || errors) {
346
- setLoading(false);
347
- return;
348
- }
349
- setPostThread(data?.createPostThread.data);
350
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
351
- if (!parentId || parentId == 0) {
352
- setParentId(data?.createPostThread?.lastMessage?.id);
353
- }
354
- setChannelToTop(channelToTop + 1);
355
- setLoading(false);
356
- setMsg('');
357
918
 
358
- sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
359
- },
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' },
360
926
  });
361
927
  }
362
928
  },
363
- [setChannelMessages, channelId, images, parentId, expoTokens],
929
+ [channelId, parentId, state.context, role, startUpload],
364
930
  );
365
931
 
366
932
  const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
@@ -382,56 +948,86 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
382
948
  };
383
949
 
384
950
  const messageList = useMemo(() => {
385
- let currentDate = '';
951
+ const threadMessages = safeContextProperty('threadMessages', []);
952
+ console.log(`Creating message list from ${threadMessages.length} thread messages`);
953
+
386
954
  let res: any = [];
387
- const filteredMessages =
388
- channelMessages && channelMessages?.length > 0 ? uniqBy([...channelMessages], ({ id }: any) => id) : [];
389
- if (filteredMessages?.length) {
390
- orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
391
- let message: IMessageProps = {
392
- _id: '',
393
- text: '',
394
- createdAt: 0,
395
- user: {
396
- _id: '',
397
- name: '',
398
- avatar: '',
399
- },
400
- type: '',
401
- };
402
- const date = new Date(msg.createdAt);
403
- message._id = msg.id;
404
- message.text = msg.message;
405
- message.createdAt = date;
406
- (message.user = {
407
- _id: msg?.author?.id ?? auth?.profile?.id,
408
- name:
409
- msg?.author?.givenName ??
410
- auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
411
- auth?.profile?.family_name,
412
- avatar: msg?.author?.picture ?? auth?.profile?.picture,
413
- }),
414
- (message.image = msg.files?.data[0]?.url),
415
- (message.sent = msg?.isDelivered),
416
- (message.received = msg?.isRead);
417
- message.type = msg?.type;
418
- message.propsConfiguration = msg?.propsConfiguration;
419
- res.push(message);
420
- });
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
+ });
421
994
  }
422
- return res?.length > 0 ? uniqBy([...res], ({ _id }: any) => _id) : [];
423
- //return res;
424
- }, [channelMessages]);
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]);
425
1000
 
426
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
+
427
1008
  return (
428
- <Send {...props}>
429
- <Box>
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
+ >
430
1027
  <MaterialCommunityIcons
431
1028
  name="send-circle"
432
- style={{ marginBottom: 5, marginRight: 5 }}
433
- size={32}
434
- color="#2e64e5"
1029
+ size={30}
1030
+ color={isDisabled ? colors.gray[400] : colors.blue[500]}
435
1031
  />
436
1032
  </Box>
437
1033
  </Send>
@@ -469,11 +1065,6 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
469
1065
  params = { reservationId: actionId };
470
1066
  }
471
1067
 
472
- // if (attachment?.callToAction?.link?.includes('my-reservation-details')) {
473
- // action = CALL_TO_ACTION_PATH;
474
- // actionId = attachment?.callToAction?.link.split('/').pop();
475
- // }
476
-
477
1068
  return (
478
1069
  <>
479
1070
  {attachment?.callToAction && action ? (
@@ -483,7 +1074,6 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
483
1074
  size={'sm'}
484
1075
  className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
485
1076
  onPress={() => action && params && navigation.navigate(action, params)}
486
- //onPress={() => navigation.navigate(action, { reservationId: actionId })}
487
1077
  >
488
1078
  <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
489
1079
  {attachment.callToAction.title}
@@ -499,10 +1089,6 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
499
1089
  ) : (
500
1090
  <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
501
1091
  )}
502
- {/* <MessageText
503
- {...props}
504
- textStyle={{ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 } }}
505
- /> */}
506
1092
  </>
507
1093
  );
508
1094
  } else {
@@ -514,37 +1100,95 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
514
1100
  return (
515
1101
  <Actions
516
1102
  {...props}
517
- icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
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
+ }}
518
1127
  />
519
1128
  );
520
1129
  };
521
1130
 
522
1131
  const renderAccessory = (props) => {
1132
+ const selectedImage = safeContextProperty('selectedImage', '');
1133
+
1134
+ if (!selectedImage) {
1135
+ return null;
1136
+ }
1137
+
523
1138
  return (
524
- <Box>
525
- {selectedImage !== '' ? (
526
- <HStack className="items-center">
527
- <Image
528
- className="ml-3"
529
- key={selectedImage}
530
- alt={'image'}
531
- source={{ uri: selectedImage }}
532
- size={'xs'}
533
- />
534
- <Button
535
- variant={'solid'}
536
- className="bg-transparent"
537
- onPress={() => {
538
- setFiles([]);
539
- setImage('');
540
- setImages([]);
541
- }}
542
- >
543
- <ButtonText className="color-black">Cancel</ButtonText>
544
- </Button>
545
- </HStack>
546
- ) : null}
547
- </Box>
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>
548
1192
  );
549
1193
  };
550
1194
 
@@ -560,11 +1204,9 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
560
1204
  <CachedImage
561
1205
  style={{ width: '100%', height: '100%' }}
562
1206
  resizeMode={'cover'}
563
- // cacheKey={`${_id}-conversation-modal-image-key`}
564
1207
  cacheKey={`${_id}-slack-bubble-imageKey`}
565
1208
  source={{
566
1209
  uri: image,
567
- //headers: `Authorization: Bearer ${token}`,
568
1210
  expiresIn: 86400,
569
1211
  }}
570
1212
  alt={'image'}
@@ -576,37 +1218,52 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
576
1218
  return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
577
1219
  };
578
1220
 
579
- let onScroll = false;
580
-
581
- const onMomentumScrollBegin = ({ nativeEvent }: any) => {
582
- onScroll = true;
583
- console.log('scroll top');
584
- if (!loadingOldMessages && isCloseToTop(nativeEvent) && totalCount > channelMessages?.length) {
585
- onFetchOld();
586
- }
587
- };
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')]);
588
1226
 
589
- const onEndReached = () => {
590
- console.log('on end reached');
591
- if (!onScroll) return;
592
- // load messages, show ActivityIndicator
593
- onScroll = false;
594
- // setLoadingOldMessages(true);
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
+ );
595
1244
  };
596
1245
 
597
1246
  return (
598
- <>
599
- {(loadingOldMessages || loadEarlierMsg) && <Spinner color={colors.blue[500]} />}
600
- {/* {loadEarlierMsg && <Spinner />} */}
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
+ )}
601
1256
  {isPostParentIdThread && (
602
1257
  <>
603
- {threadPost?.length > 0 && (
1258
+ {safeContextProperty('threadPost', [])?.length > 0 && (
604
1259
  <>
605
1260
  <VStack className="px-2 pt-2 pb-0" space={'sm'}>
606
1261
  <HStack space={'sm'} className="items-center">
607
1262
  <Avatar className="bg-transparent" size={'md'}>
608
1263
  <AvatarFallbackText>
609
- {startCase(threadPost[0]?.author?.username?.charAt(0))}
1264
+ {startCase(
1265
+ safeContextProperty('threadPost')[0]?.author?.username?.charAt(0),
1266
+ )}
610
1267
  </AvatarFallbackText>
611
1268
  <AvatarImage
612
1269
  alt="image"
@@ -616,31 +1273,36 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
616
1273
  borderColor: '#fff',
617
1274
  }}
618
1275
  source={{
619
- uri: threadPost[0]?.author?.picture,
1276
+ uri: safeContextProperty('threadPost')[0]?.author?.picture,
620
1277
  }}
621
1278
  />
622
1279
  </Avatar>
623
1280
  <Box>
624
1281
  <Text className="font-bold color-black">
625
- {threadPost[0]?.author?.givenName ?? ''}{' '}
626
- {threadPost[0]?.author?.familyName ?? ''}
1282
+ {safeContextProperty('threadPost')[0]?.author?.givenName ?? ''}{' '}
1283
+ {safeContextProperty('threadPost')[0]?.author?.familyName ?? ''}
627
1284
  </Text>
628
1285
  <Text className="pl-0 color-gray-500">
629
- {createdAtText(threadPost[0]?.createdAt)} at{' '}
630
- {format(new Date(threadPost[0]?.createdAt), 'hh:ss:a')}
1286
+ {createdAtText(safeContextProperty('threadPost')[0]?.createdAt)} at{' '}
1287
+ {format(
1288
+ new Date(safeContextProperty('threadPost')[0]?.createdAt),
1289
+ 'hh:ss:a',
1290
+ )}
631
1291
  </Text>
632
1292
  </Box>
633
1293
  </HStack>
634
1294
  <HStack space={'sm'} className="px-2 items-center">
635
- <Text>{threadPost[0]?.message ?? ''}</Text>
1295
+ <Text>{safeContextProperty('threadPost')[0]?.message ?? ''}</Text>
636
1296
  </HStack>
637
1297
  </VStack>
638
1298
 
639
1299
  <Box className="py-4">
640
1300
  <Box className="px-4 py-2 border-t border-b border-gray-200">
641
1301
  <Text className="font-bold color-gray-600">
642
- {threadPost[0]?.replies?.totalCount}{' '}
643
- {threadPost[0]?.replies?.totalCount > 0 ? 'replies' : 'reply'}
1302
+ {safeContextProperty('threadPost')[0]?.replies?.totalCount}{' '}
1303
+ {safeContextProperty('threadPost')[0]?.replies?.totalCount > 0
1304
+ ? 'replies'
1305
+ : 'reply'}
644
1306
  </Text>
645
1307
  </Box>
646
1308
  </Box>
@@ -654,51 +1316,144 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
654
1316
  renderLoading={() => <Spinner color={colors.blue[500]} />}
655
1317
  messages={messageList}
656
1318
  listViewProps={{
657
- onEndReached: onEndReached,
658
- onEndReachedThreshold: 0.5,
659
- onMomentumScrollBegin: onMomentumScrollBegin,
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
+ });
660
1384
  }}
661
- onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
662
- text={msg ? msg : ' '}
663
- onInputTextChanged={(text) => setMsg(text)}
664
1385
  renderFooter={() =>
665
- loading ? (
666
- <Spinner color={colors.blue[500]} />
667
- ) : imageLoading ? (
668
- <Spinner color={colors.blue[500]} />
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>
669
1394
  ) : (
670
- ''
1395
+ <></>
671
1396
  )
672
1397
  }
673
1398
  scrollToBottom
1399
+ loadEarlier={false}
1400
+ isLoadingEarlier={false}
674
1401
  user={{
675
1402
  _id: auth?.id || '',
676
1403
  }}
677
1404
  isTyping={true}
678
- alwaysShowSend={loading ? false : true}
679
- // onLoadEarlier={onFetchOld}
1405
+ alwaysShowSend={safeContextProperty('loading', false) ? false : true}
680
1406
  infiniteScroll={true}
681
1407
  renderSend={renderSend}
682
- // loadEarlier={data?.messages?.totalCount > channelMessages.length}
683
- // isLoadingEarlier={loadEarlierMsg}
684
- // renderLoadEarlier={() =>
685
- // !loadEarlierMsg && (
686
- // <Center py={2}>
687
- // <Button
688
- // onPress={() => onFetchOld(channelMessages?.length)}
689
- // variant={'outline'}
690
- // _text={{ color: 'black', fontSize: 15, fontWeight: 'bold' }}
691
- // >
692
- // Load earlier messages
693
- // </Button>
694
- // </Center>
695
- // )
696
- // }
697
- renderMessageText={renderMessageText}
1408
+ renderInputToolbar={renderInputToolbar}
698
1409
  minInputToolbarHeight={50}
699
1410
  renderActions={renderActions}
700
- renderAccessory={renderAccessory}
1411
+ renderAccessory={!!state?.context?.selectedImage ? renderAccessory : undefined}
701
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}
702
1457
  renderChatFooter={() => (
703
1458
  <>
704
1459
  <ImageViewerModal
@@ -721,10 +1476,20 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
721
1476
  const prevReplyCount: any = prev?.getPostThread?.replyCount;
722
1477
  const newReplyCount = prevReplyCount || 0 + 1;
723
1478
  const replies = prev?.getPostThread?.replies || [];
724
- setChannelMessages((oldMessages: any) =>
725
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
726
- );
727
- setTotalCount(newReplyCount);
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
+
728
1493
  return Object.assign({}, prev, {
729
1494
  getPostThread: {
730
1495
  ...prev?.getPostThread,
@@ -759,7 +1524,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
759
1524
  disabled: true,
760
1525
  }}
761
1526
  />
762
- </>
1527
+ </SafeAreaView>
763
1528
  );
764
1529
  };
765
1530
 
@@ -768,5 +1533,5 @@ const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: IThreadSubsc
768
1533
  return <></>;
769
1534
  };
770
1535
 
771
- export const ThreadConversationView = ThreadConversationViewComponent;
772
- // export const ThreadConversationView = React.memo(ThreadConversationViewComponent);
1536
+ export const ThreadConversationView = React.memo(ThreadConversationViewComponent);
1537
+ // export const ThreadConversationView = ThreadConversationViewComponent;