@messenger-box/platform-mobile 10.0.3-alpha.34 → 10.0.3-alpha.37

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