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

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 (95) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/lib/compute.js +2 -3
  3. package/lib/compute.js.map +1 -1
  4. package/lib/index.js.map +1 -1
  5. package/lib/module.js.map +1 -1
  6. package/lib/queries/inboxQueries.js +65 -0
  7. package/lib/queries/inboxQueries.js.map +1 -0
  8. package/lib/routes.json +13 -1
  9. package/lib/screens/inbox/DialogMessages.js +5 -3
  10. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  11. package/lib/screens/inbox/DialogThreadMessages.js +3 -7
  12. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  13. package/lib/screens/inbox/DialogThreads.js +57 -18
  14. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  15. package/lib/screens/inbox/Inbox.js.map +1 -1
  16. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  17. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  18. package/lib/screens/inbox/components/CachedImage/index.js +168 -46
  19. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  20. package/lib/screens/inbox/components/DialogItem.js +169 -0
  21. package/lib/screens/inbox/components/DialogItem.js.map +1 -0
  22. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
  23. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  24. package/lib/screens/inbox/components/SlackMessageContainer/ImageViewerModal.js.map +1 -1
  25. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js +197 -0
  26. package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js.map +1 -0
  27. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +141 -31
  28. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  29. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  30. package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
  31. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  32. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -44
  33. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  34. package/lib/screens/inbox/config/config.js +2 -2
  35. package/lib/screens/inbox/config/config.js.map +1 -1
  36. package/lib/screens/inbox/containers/ConversationView.js +1205 -432
  37. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  38. package/lib/screens/inbox/containers/Dialogs.js +193 -80
  39. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  40. package/lib/screens/inbox/containers/ThreadConversationView.js +724 -207
  41. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  42. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  43. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  44. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  45. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  46. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  47. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  48. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  49. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  50. package/package.json +5 -5
  51. package/src/index.ts +2 -0
  52. package/src/queries/inboxQueries.ts +299 -0
  53. package/src/queries/index.d.ts +2 -0
  54. package/src/queries/index.ts +1 -0
  55. package/src/screens/inbox/DialogMessages.tsx +2 -1
  56. package/src/screens/inbox/DialogThreadMessages.tsx +3 -11
  57. package/src/screens/inbox/DialogThreads.tsx +53 -60
  58. package/src/screens/inbox/components/Actionsheet.tsx +30 -0
  59. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  60. package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
  61. package/src/screens/inbox/components/DialogItem.tsx +306 -0
  62. package/src/screens/inbox/components/DialogsListItem.tsx +230 -170
  63. package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
  64. package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
  65. package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
  66. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +330 -171
  67. package/src/screens/inbox/components/SlackInput.tsx +23 -0
  68. package/src/screens/inbox/components/SlackMessageContainer/PaymentMessage.tsx +199 -0
  69. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
  70. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  71. package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
  72. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +41 -24
  73. package/src/screens/inbox/components/ThreadsViewItem.tsx +177 -265
  74. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  75. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  76. package/src/screens/inbox/config/config.ts +2 -2
  77. package/src/screens/inbox/containers/ConversationView.tsx +2004 -698
  78. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  79. package/src/screens/inbox/containers/Dialogs.tsx +402 -196
  80. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +2 -2
  81. package/src/screens/inbox/containers/ThreadConversationView.tsx +1337 -301
  82. package/src/screens/inbox/containers/ThreadsView.tsx +105 -186
  83. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  84. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  85. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  86. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
  87. package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
  88. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  89. package/src/screens/inbox/index.ts +37 -0
  90. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  91. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
  92. package/lib/screens/inbox/components/DialogsListItem.js +0 -175
  93. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  94. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -165
  95. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
@@ -13,25 +13,45 @@ import {
13
13
  Image,
14
14
  Spinner,
15
15
  Text,
16
+ Skeleton,
17
+ Toast,
18
+ ToastTitle,
19
+ ToastDescription,
20
+ useToast,
21
+ Divider,
16
22
  } from '@admin-layout/gluestack-ui-mobile';
17
- import { Platform } from 'react-native';
23
+ import {
24
+ Platform,
25
+ TouchableHighlight,
26
+ View,
27
+ TouchableOpacity,
28
+ ScrollView,
29
+ Animated,
30
+ Keyboard,
31
+ RefreshControl,
32
+ } from 'react-native';
18
33
  import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
19
34
  import { useSelector } from 'react-redux';
20
35
  import { orderBy, startCase, uniqBy } from 'lodash-es';
21
36
  import * as ImagePicker from 'expo-image-picker';
22
37
  import { encode as atob } from 'base-64';
23
38
  import { Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
24
- import { Actions, GiftedChat, IMessage, MessageText, Send } from 'react-native-gifted-chat';
25
- import { IPost, IPostThread, PreDefinedRole, IExpoNotificationData, IFileInfo } from 'common';
39
+ import { Actions, GiftedChat, IMessage, MessageText, Send, InputToolbar, Composer } from 'react-native-gifted-chat';
40
+ import {
41
+ IPost,
42
+ IPostThread,
43
+ PreDefinedRole,
44
+ IExpoNotificationData,
45
+ IFileInfo,
46
+ FileRefType,
47
+ PostTypeEnum,
48
+ } from 'common';
26
49
  import {
27
- OnThreadChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
28
- useCreatePostThreadMutation,
29
- useOnThreadChatMessageAddedSubscription,
30
- useSendExpoNotificationOnPostMutation,
31
- useSendThreadMessageMutation,
32
- useThreadMessagesLazyQuery,
33
- useGetPostThreadLazyQuery,
34
- } from 'common/graphql';
50
+ CHAT_MESSAGE_ADDED,
51
+ useCreatePostThread,
52
+ useSendExpoNotification,
53
+ useGetPostThreadLazy,
54
+ } from '../../../queries/inboxQueries';
35
55
  import { useUploadFilesNative } from '@messenger-box/platform-client';
36
56
  import { objectId } from '@messenger-box/core';
37
57
  import { format, isToday, isYesterday } from 'date-fns';
@@ -40,6 +60,8 @@ import { config } from '../config';
40
60
  import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
41
61
  import CachedImage from '../components/CachedImage';
42
62
  import colors from 'tailwindcss/colors';
63
+ import ExpandableInputActionSheet from '../components/ExpandableInputActionSheet';
64
+ import GiftedChatInboxComponent from '../components/GiftedChatInboxComponent';
43
65
 
44
66
  const {
45
67
  MESSAGES_PER_PAGE,
@@ -60,6 +82,9 @@ const createdAtText = (value: string) => {
60
82
  interface IMessageProps extends IMessage {
61
83
  type: string;
62
84
  propsConfiguration?: any;
85
+ images?: string[];
86
+ isOptimistic?: boolean;
87
+ isUploading?: boolean;
63
88
  }
64
89
 
65
90
  export interface AlertMessageAttachmentsInterface {
@@ -77,21 +102,88 @@ interface IThreadSubscriptionHandlerProps {
77
102
  channelId: string;
78
103
  }
79
104
 
105
+ // Define an extended interface for ImagePickerAsset with url property
106
+ interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
107
+ url?: string;
108
+ fileName?: string;
109
+ mimeType?: string;
110
+ }
111
+
112
+ // Custom notification component
113
+ const ErrorNotification = ({ message, onClose }) => {
114
+ const opacity = useRef(new Animated.Value(0)).current;
115
+
116
+ useEffect(() => {
117
+ // Fade in
118
+ Animated.timing(opacity, {
119
+ toValue: 1,
120
+ duration: 300,
121
+ useNativeDriver: true,
122
+ }).start();
123
+
124
+ // Auto hide after 4 seconds
125
+ const timer = setTimeout(() => {
126
+ Animated.timing(opacity, {
127
+ toValue: 0,
128
+ duration: 300,
129
+ useNativeDriver: true,
130
+ }).start(() => onClose && onClose());
131
+ }, 4000);
132
+
133
+ return () => clearTimeout(timer);
134
+ }, []);
135
+
136
+ return (
137
+ <Animated.View
138
+ style={{
139
+ position: 'absolute',
140
+ top: 10,
141
+ left: 10,
142
+ right: 10,
143
+ backgroundColor: '#f44336',
144
+ padding: 15,
145
+ borderRadius: 8,
146
+ shadowColor: '#000',
147
+ shadowOffset: { width: 0, height: 2 },
148
+ shadowOpacity: 0.25,
149
+ shadowRadius: 3.84,
150
+ elevation: 5,
151
+ zIndex: 1000,
152
+ opacity,
153
+ }}
154
+ >
155
+ <HStack className="items-center justify-between">
156
+ <Text style={{ color: 'white', fontWeight: 'bold' }}>Upload Failed</Text>
157
+ <TouchableOpacity onPress={onClose}>
158
+ <Ionicons name="close" size={20} color="white" />
159
+ </TouchableOpacity>
160
+ </HStack>
161
+ <Text style={{ color: 'white', marginTop: 5 }}>{message}</Text>
162
+ </Animated.View>
163
+ );
164
+ };
165
+
166
+ // Add a utility function for file validation
167
+ const isValidFileUrl = (url: string | undefined): boolean => {
168
+ return !!url && typeof url === 'string' && url.length > 0;
169
+ };
170
+
80
171
  const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any) => {
81
172
  const { params } = useRoute<any>();
82
173
  const [channelToTop, setChannelToTop] = useState(0);
83
174
  const [channelMessages, setChannelMessages] = useState<any>([]);
84
175
  const auth: any = useSelector(userSelector);
85
176
  const [totalCount, setTotalCount] = useState<any>(0);
86
- const [selectedImage, setImage] = useState<string>('');
177
+ const [selectedImage, setSelectedImage] = useState<string>('');
87
178
  const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
88
179
  const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
89
180
  const navigation = useNavigation<any>();
90
181
  const [files, setFiles] = useState<File[]>([]);
91
- const [images, setImages] = useState<ImagePicker.ImagePickerAsset[]>([]);
182
+ const [images, setImages] = useState<ExtendedImagePickerAsset[]>([]);
92
183
  const [msg, setMsg] = useState<string>('');
93
184
  const [loading, setLoading] = useState(false);
94
185
  const [imageLoading, setImageLoading] = useState(false);
186
+ const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
95
187
  const [expoTokens, setExpoTokens] = useState<any[]>([]);
96
188
  const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
97
189
  const [imageObject, setImageObject] = useState<any>({});
@@ -101,15 +193,30 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
101
193
  const [threadPost, setThreadPost] = useState<any[]>([]);
102
194
  const [isScrollToBottom, setIsScrollToBottom] = useState(false);
103
195
  const threadMessageListRef = useRef<any>(null);
196
+ const toast = useToast();
197
+ const [errorMessage, setErrorMessage] = useState('');
198
+
199
+ // Add state for expandable input action sheet
200
+ const [isActionSheetVisible, setActionSheetVisible] = useState(false);
201
+ const [textUpdatedInActionSheet, setTextUpdatedInActionSheet] = useState(false);
202
+ const [lastShownMsg, setLastShownMsg] = useState('');
203
+ const textInputRef = useRef(null);
104
204
 
105
205
  // const [sendThreadMessage] = useSendThreadMessageMutation();
106
- const [sendThreadMessage] = useCreatePostThreadMutation();
107
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
206
+ const [sendThreadMessage] = useCreatePostThread();
207
+ const [sendExpoNotificationOnPostMutation] = useSendExpoNotification();
108
208
 
109
209
  const [
110
210
  getThreadMessages,
111
211
  { data, loading: threadLoading, fetchMore: fetchMoreMessages, refetch: refetchThreadMessages, subscribeToMore },
112
- ] = useGetPostThreadLazyQuery({ fetchPolicy: 'cache-and-network' });
212
+ ] = useGetPostThreadLazy({ fetchPolicy: 'cache-and-network' });
213
+
214
+ // Add new state for tracking uploads
215
+ const [pendingUploads, setPendingUploads] = useState<Record<string, any>>({});
216
+ const [uploadErrors, setUploadErrors] = useState<Record<string, string>>({});
217
+ const [isUploadingImage, setIsUploadingImage] = useState(false);
218
+
219
+ const [refreshing, setRefreshing] = useState(false);
113
220
 
114
221
  useFocusEffect(
115
222
  React.useCallback(() => {
@@ -199,6 +306,8 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
199
306
  const onFetchOld = useCallback(() => {
200
307
  if (totalCount > channelMessages.length && !loadingOldMessages) {
201
308
  setLoadEarlierMsg(true);
309
+ setLoadingOldMessages(true);
310
+
202
311
  fetchMoreMessages({
203
312
  variables: {
204
313
  channelId: channelId?.toString(),
@@ -220,14 +329,24 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
220
329
  .finally(() => {
221
330
  setLoadEarlierMsg(false);
222
331
  setLoadingOldMessages(false);
332
+ setRefreshing(false);
223
333
  })
224
334
  .catch((error: any) => {
225
335
  setLoadEarlierMsg(false);
226
336
  setLoadingOldMessages(false);
337
+ setRefreshing(false);
227
338
  });
339
+ } else {
340
+ setRefreshing(false);
228
341
  }
229
342
  }, [parentId, channelId, totalCount, channelMessages]);
230
343
 
344
+ // Pull to refresh handler
345
+ const onRefresh = useCallback(() => {
346
+ setRefreshing(true);
347
+ onFetchOld();
348
+ }, [onFetchOld]);
349
+
231
350
  // const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
232
351
  // return contentOffset.y <= 100; // 100px from top
233
352
  // };
@@ -249,118 +368,438 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
249
368
  return new File([u8arr], filename, { type: mime });
250
369
  };
251
370
 
252
- const onSelectImages = async () => {
253
- setImageLoading(true);
254
- let imageSource: any = await ImagePicker.launchImageLibraryAsync({
255
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
256
- allowsEditing: true,
257
- aspect: [4, 3],
258
- quality: 1,
259
- base64: true,
371
+ // Add a helper function for removing messages from the UI when uploads fail
372
+ const removeMessageFromUI = useCallback((messageId: string) => {
373
+ // Remove from pending uploads
374
+ setPendingUploads((prev) => {
375
+ const newPending = { ...prev };
376
+ delete newPending[messageId];
377
+ return newPending;
260
378
  });
261
- if (!imageSource.canceled) {
262
- const image = 'data:image/jpeg;base64,' + imageSource.assets[0]?.base64;
263
- setImage(image);
264
- const file = dataURLtoFile(image, 'inputImage.jpg');
265
- setFiles((files) => files.concat(file));
266
- setImages((images) => images.concat(imageSource.assets[0] as ImagePicker.ImagePickerAsset));
379
+
380
+ // Also remove any error state
381
+ setUploadErrors((prev) => {
382
+ const newErrors = { ...prev };
383
+ delete newErrors[messageId];
384
+ return newErrors;
385
+ });
386
+
387
+ // Reset upload state to ensure we don't get stuck with loading indicator
388
+ setIsUploadingImage(false);
389
+ }, []);
390
+
391
+ // const onSelectImages = async () => {
392
+ // setImageLoading(true);
393
+
394
+ // try {
395
+ // let imageSource = await ImagePicker.launchImageLibraryAsync({
396
+ // mediaTypes: ImagePicker.MediaTypeOptions.Images,
397
+ // allowsEditing: true,
398
+ // aspect: [4, 3],
399
+ // quality: 0.8,
400
+ // base64: true,
401
+ // exif: false,
402
+ // });
403
+
404
+ // if (!imageSource?.canceled) {
405
+ // // Get the asset
406
+ // const selectedAsset = imageSource?.assets?.[0];
407
+ // if (!selectedAsset) {
408
+ // setImageLoading(false);
409
+ // return;
410
+ // }
411
+
412
+ // // Create a base64 image string for preview
413
+ // const base64Data = selectedAsset.base64;
414
+ // const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
415
+
416
+ // // Format the asset for upload service requirements
417
+ // const asset: ExtendedImagePickerAsset = {
418
+ // ...selectedAsset,
419
+ // url: selectedAsset.uri,
420
+ // fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
421
+ // mimeType: 'image/jpeg',
422
+ // };
423
+
424
+ // // Update state with the new image
425
+ // setSelectedImage(previewImage);
426
+ // setImages([asset]);
427
+ // setImageLoading(false);
428
+ // } else {
429
+ // setImageLoading(false);
430
+ // }
431
+ // } catch (error) {
432
+ // console.error('Error selecting image:', error);
433
+ // setImageLoading(false);
434
+ // }
435
+ // };
436
+
437
+ // Image selection handler
438
+ const onSelectImages = async () => {
439
+ setLoading(true);
440
+
441
+ try {
442
+ let imageSource = await ImagePicker.launchImageLibraryAsync({
443
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
444
+ allowsEditing: false,
445
+ aspect: [4, 3],
446
+ quality: 0.8,
447
+ base64: true,
448
+ exif: false,
449
+ allowsMultipleSelection: true, // Enable multiple selection
450
+ });
451
+
452
+ if (!imageSource?.canceled) {
453
+ // Get all selected assets
454
+ const selectedAssets = imageSource?.assets || [];
455
+ if (selectedAssets.length === 0) {
456
+ setLoading(false);
457
+ return;
458
+ }
459
+
460
+ // Process all selected images
461
+ const newImages = selectedAssets.map((selectedAsset) => {
462
+ // Create a base64 image string for preview
463
+ const base64Data = selectedAsset.base64;
464
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
465
+
466
+ // Format the asset for upload service requirements
467
+ const asset: ExtendedImagePickerAsset = {
468
+ ...selectedAsset,
469
+ url: selectedAsset.uri,
470
+ fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
471
+ mimeType: 'image/jpeg',
472
+ };
473
+
474
+ return asset;
475
+ });
476
+
477
+ // Set preview for the first image (for backward compatibility)
478
+ if (newImages.length > 0) {
479
+ const base64Data = newImages[0].base64;
480
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : newImages[0].uri;
481
+ setSelectedImage(previewImage);
482
+ }
483
+
484
+ // Add new images to existing ones
485
+ setImages((currentImages) => [...currentImages, ...newImages]);
486
+ setLoading(false); // Set loading to false after successful selection
487
+ } else {
488
+ setLoading(false);
489
+ }
490
+ } catch (error) {
491
+ setLoading(false);
492
+ }
493
+ };
494
+
495
+ // Action sheet handlers
496
+ const showActionSheet = useCallback(() => {
497
+ setLastShownMsg(msg); // Store current message text when opening sheet
498
+ setActionSheetVisible(true);
499
+ }, [msg]);
500
+
501
+ const handleActionSheetClose = useCallback(() => {
502
+ // Close the sheet
503
+ setActionSheetVisible(false);
504
+
505
+ // If user didn't make any changes, restore the original message
506
+ if (!textUpdatedInActionSheet) {
507
+ setMsg(lastShownMsg);
267
508
  }
268
- if (imageSource.canceled) setLoading(false);
509
+
510
+ // Reset the flag
511
+ setTextUpdatedInActionSheet(false);
512
+ }, [textUpdatedInActionSheet, lastShownMsg]);
513
+
514
+ const handleActionSheetSend = () => {
515
+ // Send the message using the existing handleSend function
516
+ handleSend(msg);
517
+
518
+ // Close the action sheet
519
+ setActionSheetVisible(false);
269
520
  };
270
521
 
522
+ // Add focus/blur handlers for the input field
523
+ useEffect(() => {
524
+ // When showing the action sheet, we should hide the keyboard for the main input
525
+ if (isActionSheetVisible && Platform.OS === 'ios') {
526
+ Keyboard.dismiss();
527
+ }
528
+ }, [isActionSheetVisible]);
529
+
530
+ // Keep track of whether the input is in focus to help sync UI states
531
+ const [isGiftedInputFocused, setIsGiftedInputFocused] = useState(false);
532
+
271
533
  const handleSend = useCallback(
272
534
  async (message: string) => {
535
+ const newMessageText = message && message.length > 0 ? message || ' ' : ' ';
536
+ // Check if we can send a message (channel and thread exists)
537
+
273
538
  if (!channelId) return;
274
- if (!message && message != ' ' && images.length == 0) return;
539
+
540
+ // Allow sending if we have text OR images (image-only messages are valid)
541
+ const hasText = !!newMessageText && newMessageText !== ' ';
542
+ const hasImages = images && images.length > 0;
543
+
544
+ if (!hasText && !hasImages) return;
545
+
546
+ // Start loading state
547
+ setLoading(true);
275
548
 
276
549
  const postId = objectId();
550
+ const currentDate = new Date();
277
551
 
278
- if (images && images.length > 0) {
279
- setLoading(true);
280
- const uploadResponse = await startUpload({
281
- file: images,
282
- saveUploadedFile: {
283
- variables: {
284
- postId,
285
- },
286
- },
287
- createUploadLink: {
288
- variables: {
289
- postId,
290
- },
291
- },
292
- });
293
- if (uploadResponse?.error) setLoading(false);
294
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
295
- if (uploadResponse.data) {
296
- setImage('');
297
- setFiles([]);
298
- setImages([]);
299
- //setLoading(false);
300
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
301
- await sendThreadMessage({
302
- variables: {
303
- postId,
304
- channelId,
305
- postThreadId: postThread && postThread?.id,
306
- postParentId: !parentId || parentId == 0 ? null : parentId,
307
- threadMessageInput: {
308
- content: message,
309
- files,
310
- role,
552
+ // Store current values before clearing
553
+ const currentMessageText = message || '';
554
+ const currentImages = [...images];
555
+
556
+ // Clear UI immediately for next message
557
+ setMsg('');
558
+ setSelectedImage('');
559
+ setImages([]);
560
+ setFiles([]);
561
+
562
+ // Close the action sheet if it's open
563
+ if (isActionSheetVisible) {
564
+ setActionSheetVisible(false);
565
+ }
566
+
567
+ // Create optimistic message with all images for immediate display
568
+ const optimisticMessage = {
569
+ id: postId,
570
+ message: currentMessageText || (currentImages.length > 0 ? ' ' : ''),
571
+ createdAt: currentDate.toISOString(),
572
+ author: {
573
+ id: auth?.profile?.id,
574
+ givenName: auth?.profile?.given_name,
575
+ familyName: auth?.profile?.family_name,
576
+ picture: auth?.profile?.picture,
577
+ },
578
+ isDelivered: false, // Mark as not delivered yet during upload
579
+ isRead: false,
580
+ isOptimistic: true, // Flag to identify optimistic messages
581
+ files:
582
+ currentImages.length > 0
583
+ ? {
584
+ data: currentImages
585
+ .filter((img) => img && (img.uri || img.url)) // Filter out invalid images
586
+ .map((img) => ({
587
+ id: objectId(),
588
+ url: img.uri || img.url,
589
+ localUri: img.uri || img.url, // Keep local URI for preview
590
+ name: img.fileName || `image_${Date.now()}.jpg`,
591
+ extension: 'jpg',
592
+ mimeType: 'image/jpeg',
593
+ refType: FileRefType.Post,
594
+ height: img.height || 0,
595
+ width: img.width || 0,
596
+ isUploading: true, // Flag to indicate this file is uploading
597
+ })),
598
+ totalCount: currentImages.filter((img) => img && (img.uri || img.url)).length, // Only count valid images
599
+ }
600
+ : undefined,
601
+ };
602
+
603
+ // Only add optimistic message if there's content to send
604
+ if (
605
+ (optimisticMessage.message && optimisticMessage.message.trim() !== '') ||
606
+ (optimisticMessage.files && optimisticMessage.files.data && optimisticMessage.files.data.length > 0)
607
+ ) {
608
+ setChannelMessages((oldMessages: any) => uniqBy([optimisticMessage, ...oldMessages], ({ id }) => id));
609
+ setIsScrollToBottom(true);
610
+ }
611
+
612
+ try {
613
+ let fileIds: string[] | null = null;
614
+
615
+ // Handle file uploads if images exist
616
+ if (currentImages && currentImages.length > 0) {
617
+ // Filter out any invalid images before upload
618
+ const validImages = currentImages.filter((img) => img && (img.uri || img.url));
619
+
620
+ if (validImages.length === 0) {
621
+ // No valid images to upload, just send the text message
622
+ setIsUploadingImage(false);
623
+ } else {
624
+ setIsUploadingImage(true);
625
+
626
+ // Format images for upload
627
+ const imagesToUpload = validImages.map((img) => {
628
+ return {
629
+ ...img,
630
+ uri: img.uri || img.url, // Use either uri or url
631
+ type: img.mimeType || 'image/jpeg',
632
+ name: img.fileName || `image_${Date.now()}.jpg`,
633
+ };
634
+ });
635
+
636
+ // Track upload progress with messageId
637
+ setUploadingMessageId(postId);
638
+ setPendingUploads((prev) => ({
639
+ ...prev,
640
+ [postId]: {
641
+ images: validImages,
642
+ message: currentMessageText,
311
643
  },
312
- },
313
- update: (cache, { data, errors }: any) => {
314
- if (!data || errors) {
315
- setLoading(false);
316
- return;
317
- }
318
- setPostThread(data?.createPostThread?.data);
319
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
320
- if (!parentId || parentId == 0) {
321
- setParentId(data?.createPostThread?.lastMessage?.id);
322
- }
644
+ }));
323
645
 
324
- setChannelToTop(channelToTop + 1);
325
- setLoading(false);
326
- setMsg('');
327
- const msg = message == '' ? 'Send a file' : message;
328
- sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
329
- },
330
- });
646
+ const uploadResponse = await startUpload({
647
+ file: imagesToUpload as unknown as ImagePicker.ImagePickerAsset[],
648
+ saveUploadedFile: {
649
+ variables: {
650
+ postId,
651
+ },
652
+ },
653
+ createUploadLink: {
654
+ variables: {
655
+ postId,
656
+ },
657
+ },
658
+ });
659
+
660
+ if (uploadResponse?.error) {
661
+ throw new Error(uploadResponse.error.toString());
662
+ }
663
+
664
+ const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
665
+ if (uploadResponse.data) {
666
+ fileIds = uploadedFiles?.map((f: any) => f.id) ?? null;
667
+
668
+ // Update message with uploaded file information
669
+ setChannelMessages((oldMessages: any) => {
670
+ return oldMessages.map((msg) => {
671
+ if (msg.id === postId && msg.isOptimistic) {
672
+ // Update the uploaded files with real URLs from server
673
+ return {
674
+ ...msg,
675
+ isOptimistic: false,
676
+ files: {
677
+ ...msg.files,
678
+ data: uploadedFiles
679
+ .filter((file) => isValidFileUrl(file.url))
680
+ .map((file, index) => ({
681
+ ...msg.files.data[index],
682
+ url: file.url,
683
+ isUploading: false,
684
+ })),
685
+ },
686
+ };
687
+ }
688
+ return msg;
689
+ });
690
+ });
691
+ }
692
+
693
+ // Clear pending upload now that it's complete
694
+ setPendingUploads((prev) => {
695
+ const newPending = { ...prev };
696
+ delete newPending[postId];
697
+ return newPending;
698
+ });
699
+ }
331
700
  }
332
- } else {
333
- setLoading(true);
701
+
702
+ // Send message with Apollo mutation
334
703
  await sendThreadMessage({
335
704
  variables: {
705
+ postId,
336
706
  channelId,
337
707
  postThreadId: postThread && postThread?.id,
338
- postParentId: !parentId || parentId == 0 ? null : parentId,
708
+ postParentId: !parentId || parentId === 0 ? null : parentId,
339
709
  threadMessageInput: {
340
- content: message,
710
+ content: currentMessageText,
711
+ files: fileIds,
341
712
  role,
342
713
  },
343
714
  },
715
+ optimisticResponse: {
716
+ createPostThread: {
717
+ __typename: 'ThreadMessageSent',
718
+ lastMessage: {
719
+ __typename: 'Post',
720
+ id: postId,
721
+ message: currentMessageText || ' ',
722
+ createdAt: new Date().toISOString(),
723
+ author: {
724
+ __typename: 'UserAccount',
725
+ id: auth?.profile?.id,
726
+ givenName: auth?.profile?.given_name,
727
+ familyName: auth?.profile?.family_name,
728
+ picture: auth?.profile?.picture,
729
+ },
730
+ channel: { __typename: 'Channel', id: channelId.toString() },
731
+ } as any, // Type assertion to avoid TypeScript errors
732
+ data: {
733
+ __typename: 'PostThread',
734
+ id: postThread?.id || objectId(),
735
+ },
736
+ },
737
+ },
344
738
  update: (cache, { data, errors }: any) => {
345
- if (!data || errors) {
346
- setLoading(false);
347
- return;
348
- }
349
- setPostThread(data?.createPostThread.data);
739
+ if (!data || errors) return;
740
+
741
+ setPostThread(data?.createPostThread?.data);
350
742
  const lastMessageId = data?.createPostThread?.lastMessage?.id;
351
- if (!parentId || parentId == 0) {
743
+ if (!parentId || parentId === 0) {
352
744
  setParentId(data?.createPostThread?.lastMessage?.id);
353
745
  }
354
746
  setChannelToTop(channelToTop + 1);
355
- setLoading(false);
356
- setMsg('');
357
747
 
748
+ // Send push notification with message info
358
749
  sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
359
750
  },
360
751
  });
752
+ } catch (error) {
753
+ console.error('Error sending message:', error);
754
+
755
+ // Format error message based on environment
756
+ let formattedErrorMessage = 'Failed to upload image. Please try again.';
757
+
758
+ if (__DEV__) {
759
+ // Show detailed error in development
760
+ if (error.name === 'ApolloError') {
761
+ formattedErrorMessage = error.message
762
+ .replace('[ApolloError: ', '')
763
+ .replace(']', '')
764
+ .split(';')[0]; // Take first part of error for clarity
765
+ } else {
766
+ formattedErrorMessage = error.message || 'Unknown error occurred';
767
+ }
768
+ } else {
769
+ // Show friendly message in production
770
+ if (error.message?.includes('network') || error.message?.includes('timeout')) {
771
+ formattedErrorMessage = 'Network error. Please check your connection.';
772
+ } else if (error.message?.includes('permission')) {
773
+ formattedErrorMessage = 'Permission denied for this operation.';
774
+ }
775
+ }
776
+
777
+ // Show custom error notification
778
+ setErrorMessage(formattedErrorMessage);
779
+
780
+ // Remove the optimistic message if there was an error
781
+ setChannelMessages((oldMessages: any) => oldMessages.filter((msg) => msg.id !== postId));
782
+
783
+ // Clean up any pending upload states
784
+ removeMessageFromUI(postId);
785
+ } finally {
786
+ setLoading(false);
787
+ setIsUploadingImage(false);
788
+ setUploadingMessageId(null);
361
789
  }
362
790
  },
363
- [setChannelMessages, channelId, images, parentId, expoTokens],
791
+ [
792
+ auth,
793
+ channelId,
794
+ channelToTop,
795
+ images,
796
+ parentId,
797
+ postThread,
798
+ selectedImage,
799
+ setChannelMessages,
800
+ removeMessageFromUI,
801
+ role,
802
+ ],
364
803
  );
365
804
 
366
805
  const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
@@ -381,6 +820,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
381
820
  }
382
821
  };
383
822
 
823
+ // Improved messageList function to better handle images
384
824
  const messageList = useMemo(() => {
385
825
  let currentDate = '';
386
826
  let res: any = [];
@@ -403,17 +843,56 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
403
843
  message._id = msg.id;
404
844
  message.text = msg.message;
405
845
  message.createdAt = date;
406
- (message.user = {
846
+ message.user = {
407
847
  _id: msg?.author?.id ?? auth?.profile?.id,
408
848
  name:
409
849
  msg?.author?.givenName ??
410
850
  auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
411
851
  auth?.profile?.family_name,
412
852
  avatar: msg?.author?.picture ?? auth?.profile?.picture,
413
- }),
414
- (message.image = msg.files?.data[0]?.url),
415
- (message.sent = msg?.isDelivered),
416
- (message.received = msg?.isRead);
853
+ };
854
+
855
+ // Handle image uploads with proper UI feedback
856
+ message.isOptimistic = msg?.isOptimistic || false;
857
+ message.isUploading = msg.files?.data?.some((file) => file.isUploading) || false;
858
+
859
+ // Improved image handling - ensure we have valid image URLs
860
+ if (msg.files?.data?.length > 0) {
861
+ // Filter out any invalid files first
862
+ const validFiles = msg.files.data.filter(
863
+ (file) =>
864
+ file && (file.url || file.localUri) && typeof (file.url || file.localUri) === 'string',
865
+ );
866
+
867
+ if (validFiles.length > 0) {
868
+ // For the first image (used in single image display)
869
+ const firstFile = validFiles[0];
870
+ const imageUrl = firstFile.isUploading
871
+ ? firstFile.localUri || firstFile.url
872
+ : firstFile.url || firstFile.localUri;
873
+
874
+ if (imageUrl) {
875
+ message.image = imageUrl;
876
+ }
877
+
878
+ // For multiple images
879
+ if (validFiles.length > 1) {
880
+ message.images = validFiles
881
+ .map((file) =>
882
+ file.isUploading ? file.localUri || file.url : file.url || file.localUri,
883
+ )
884
+ .filter((url) => !!url); // Filter out any undefined or empty URLs
885
+
886
+ // If no valid URLs after filtering, don't set the images property
887
+ if (message.images.length === 0) {
888
+ delete message.images;
889
+ }
890
+ }
891
+ }
892
+ }
893
+
894
+ message.sent = msg?.isDelivered;
895
+ message.received = msg?.isRead;
417
896
  message.type = msg?.type;
418
897
  message.propsConfiguration = msg?.propsConfiguration;
419
898
  res.push(message);
@@ -423,148 +902,494 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
423
902
  //return res;
424
903
  }, [channelMessages]);
425
904
 
426
- const renderSend = (props) => {
427
- return (
428
- <Send {...props}>
429
- <Box>
430
- <MaterialCommunityIcons
431
- name="send-circle"
432
- style={{ marginBottom: 5, marginRight: 5 }}
433
- size={32}
434
- color="#2e64e5"
435
- />
436
- </Box>
437
- </Send>
438
- );
439
- };
905
+ const renderSend = useCallback(
906
+ (props) => {
907
+ // If action sheet is visible, don't show the default send button
908
+ if (isActionSheetVisible) {
909
+ return null;
910
+ }
440
911
 
441
- const renderMessageText = (props: any) => {
442
- const { currentMessage } = props;
443
- if (currentMessage.type === 'ALERT') {
444
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
445
- let action: string = '';
446
- let actionId: any = '';
447
- let params: any = {};
448
- if (attachment?.callToAction?.extraParams) {
449
- const extraParams: any = attachment?.callToAction?.extraParams;
450
- const route: any = extraParams?.route ?? null;
451
- let path: any = null;
452
- let param: any = null;
453
- if (role && role == PreDefinedRole.Guest) {
454
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
455
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
456
- } else if (role && role == PreDefinedRole.Owner) {
457
- path = route?.host?.name ? route?.host?.name ?? null : null;
458
- param = route?.host?.params ? route?.host?.params ?? null : null;
459
- } else {
460
- path = route?.host?.name ? route?.host?.name ?? null : null;
461
- param = route?.host?.params ? route?.host?.params ?? null : null;
912
+ // Enable the send button if there's text OR we have images
913
+ const hasContent = !!props.text || images?.length > 0;
914
+ const canSend = channelId && hasContent;
915
+ const isDisabled = !canSend || isUploadingImage || loading;
916
+
917
+ return (
918
+ <Send
919
+ {...props}
920
+ disabled={isDisabled}
921
+ containerStyle={{
922
+ justifyContent: 'center',
923
+ alignItems: 'center',
924
+ height: 40,
925
+ width: 44,
926
+ marginRight: 4,
927
+ marginBottom: 0,
928
+ marginLeft: 4,
929
+ }}
930
+ // onSend={(messages, shouldReset) => {
931
+ // // Always use our custom handler to ensure images are handled properly
932
+ // console.log('messages', messages);
933
+ // // handleSend(props.text || ' ');
934
+ // }}
935
+ >
936
+ <View style={{ padding: 4 }}>
937
+ {isUploadingImage || loading ? (
938
+ <Spinner size="small" color={colors.blue[500]} />
939
+ ) : (
940
+ <MaterialCommunityIcons
941
+ name="send-circle"
942
+ size={32}
943
+ color={isDisabled ? colors.gray[400] : colors.blue[500]}
944
+ />
945
+ )}
946
+ </View>
947
+ </Send>
948
+ );
949
+ },
950
+ [channelId, images, isUploadingImage, loading, isActionSheetVisible],
951
+ );
952
+
953
+ const renderMessageText1 = useCallback(
954
+ (props: any) => {
955
+ const { currentMessage } = props;
956
+ if (currentMessage.type === 'ALERT') {
957
+ const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
958
+ let action: string = '';
959
+ let actionId: any = '';
960
+ let params: any = {};
961
+ if (attachment?.callToAction?.extraParams) {
962
+ const extraParams: any = attachment?.callToAction?.extraParams;
963
+ const route: any = extraParams?.route ?? null;
964
+ let path: any = null;
965
+ let param: any = null;
966
+ if (role && role == PreDefinedRole.Guest) {
967
+ path = route?.guest?.name ? route?.guest?.name ?? null : null;
968
+ param = route?.guest?.params ? route?.guest?.params ?? null : null;
969
+ } else if (role && role == PreDefinedRole.Owner) {
970
+ path = route?.host?.name ? route?.host?.name ?? null : null;
971
+ param = route?.host?.params ? route?.host?.params ?? null : null;
972
+ } else {
973
+ path = route?.host?.name ? route?.host?.name ?? null : null;
974
+ param = route?.host?.params ? route?.host?.params ?? null : null;
975
+ }
976
+
977
+ action = path;
978
+ params = { ...param };
979
+ } else if (attachment?.callToAction?.link) {
980
+ action = CALL_TO_ACTION_PATH;
981
+ actionId = attachment?.callToAction?.link.split('/').pop();
982
+ params = { reservationId: actionId };
462
983
  }
463
984
 
464
- action = path;
465
- params = { ...param };
466
- } else if (attachment?.callToAction?.link) {
467
- action = CALL_TO_ACTION_PATH;
468
- actionId = attachment?.callToAction?.link.split('/').pop();
469
- params = { reservationId: actionId };
985
+ return (
986
+ <>
987
+ {attachment?.callToAction && action ? (
988
+ <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
989
+ <Button
990
+ variant={'outline'}
991
+ size={'sm'}
992
+ className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
993
+ onPress={() => action && params && navigation.navigate(action, params)}
994
+ >
995
+ <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
996
+ {attachment.callToAction.title}
997
+ </ButtonText>
998
+ </Button>
999
+ <MessageText
1000
+ {...props}
1001
+ textStyle={{
1002
+ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1003
+ }}
1004
+ />
1005
+ </Box>
1006
+ ) : (
1007
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1008
+ )}
1009
+ </>
1010
+ );
1011
+ } else {
1012
+ return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
470
1013
  }
1014
+ },
1015
+ [navigation, role],
1016
+ );
471
1017
 
472
- // if (attachment?.callToAction?.link?.includes('my-reservation-details')) {
473
- // action = CALL_TO_ACTION_PATH;
474
- // actionId = attachment?.callToAction?.link.split('/').pop();
475
- // }
1018
+ const renderMessageText = useCallback(
1019
+ (props: any) => {
1020
+ const { currentMessage } = props;
1021
+ const lastReply: any =
1022
+ currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
476
1023
 
477
- return (
478
- <>
479
- {attachment?.callToAction && action ? (
480
- <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
481
- <Button
482
- variant={'outline'}
483
- size={'sm'}
484
- className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
485
- onPress={() => action && params && navigation.navigate(action, params)}
486
- //onPress={() => navigation.navigate(action, { reservationId: actionId })}
487
- >
488
- <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
489
- {attachment.callToAction.title}
490
- </ButtonText>
491
- </Button>
492
- <MessageText
493
- {...props}
494
- textStyle={{
495
- left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1024
+ // Do not render anything if the message text is empty or only whitespace
1025
+ if (!currentMessage?.text || currentMessage.text.trim() === '') {
1026
+ return null;
1027
+ }
1028
+
1029
+ if (currentMessage.type === 'ALERT') {
1030
+ const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1031
+ let action: string = '';
1032
+ let actionId: any = '';
1033
+ let params: any = {};
1034
+
1035
+ if (attachment?.callToAction?.extraParams) {
1036
+ const extraParams: any = attachment?.callToAction?.extraParams;
1037
+ const route: any = extraParams?.route ?? null;
1038
+ let path: any = null;
1039
+ let param: any = null;
1040
+ if (role && role == PreDefinedRole.Guest) {
1041
+ path = route?.guest?.name ? route?.guest?.name ?? null : null;
1042
+ param = route?.guest?.params ? route?.guest?.params ?? null : null;
1043
+ } else if (role && role == PreDefinedRole.Owner) {
1044
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1045
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1046
+ } else {
1047
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1048
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1049
+ }
1050
+
1051
+ action = path;
1052
+ params = { ...param };
1053
+ } else if (attachment?.callToAction?.link) {
1054
+ action = CALL_TO_ACTION_PATH;
1055
+ actionId = attachment?.callToAction?.link.split('/').pop();
1056
+ params = { reservationId: actionId };
1057
+ }
1058
+
1059
+ return (
1060
+ <>
1061
+ {attachment?.callToAction && action ? (
1062
+ <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
1063
+ <Button
1064
+ variant={'outline'}
1065
+ size={'sm'}
1066
+ className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
1067
+ onPress={() => action && params && navigation.navigate(action, params)}
1068
+ >
1069
+ <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
1070
+ {attachment.callToAction.title}
1071
+ </ButtonText>
1072
+ </Button>
1073
+ <MessageText
1074
+ {...props}
1075
+ textStyle={{
1076
+ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1077
+ }}
1078
+ />
1079
+ </Box>
1080
+ ) : (
1081
+ <TouchableHighlight
1082
+ underlayColor={'#c0c0c0'}
1083
+ style={{ width: '100%' }}
1084
+ onPress={() => {
1085
+ if (currentMessage?.isShowThreadMessage)
1086
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
1087
+ channelId: channelId,
1088
+ title: 'Message',
1089
+ postParentId: currentMessage?._id,
1090
+ isPostParentIdThread: true,
1091
+ });
496
1092
  }}
497
- />
1093
+ >
1094
+ <>
1095
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1096
+ {currentMessage?.replies?.data?.length > 0 && (
1097
+ <HStack space={'sm'} className="px-1 items-center">
1098
+ <HStack>
1099
+ {currentMessage?.replies?.data
1100
+ ?.filter(
1101
+ (v: any, i: any, a: any) =>
1102
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) ===
1103
+ i,
1104
+ )
1105
+ ?.slice(0, 2)
1106
+ ?.reverse()
1107
+ ?.map((p: any, i: Number) => (
1108
+ <Avatar
1109
+ key={'conversations-view-key-' + i}
1110
+ size={'sm'}
1111
+ className="bg-transparent"
1112
+ >
1113
+ <AvatarFallbackText>
1114
+ {startCase(p?.author?.username?.charAt(0))}
1115
+ </AvatarFallbackText>
1116
+ <AvatarImage
1117
+ alt="user image"
1118
+ style={{
1119
+ borderRadius: 6,
1120
+ borderWidth: 2,
1121
+ borderColor: '#fff',
1122
+ }}
1123
+ source={{
1124
+ uri: p?.author?.picture,
1125
+ }}
1126
+ />
1127
+ </Avatar>
1128
+ ))}
1129
+ </HStack>
1130
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
1131
+ {currentMessage?.replies?.totalCount}{' '}
1132
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
1133
+ </Text>
1134
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
1135
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
1136
+ </Text>
1137
+ </HStack>
1138
+ )}
1139
+ </>
1140
+ </TouchableHighlight>
1141
+ )}
1142
+ </>
1143
+ );
1144
+ } else {
1145
+ return (
1146
+ <TouchableHighlight
1147
+ underlayColor={'#c0c0c0'}
1148
+ style={{ width: '100%' }}
1149
+ onPress={() => {
1150
+ if (currentMessage?.isShowThreadMessage)
1151
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
1152
+ channelId: channelId,
1153
+ title: 'Message',
1154
+ postParentId: currentMessage?._id,
1155
+ isPostParentIdThread: true,
1156
+ });
1157
+ }}
1158
+ >
1159
+ <>
1160
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1161
+ {currentMessage?.replies?.data?.length > 0 && (
1162
+ <HStack space={'sm'} className="px-1 items-center">
1163
+ <HStack>
1164
+ {currentMessage?.replies?.data
1165
+ ?.filter(
1166
+ (v: any, i: any, a: any) =>
1167
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
1168
+ )
1169
+ ?.slice(0, 2)
1170
+ ?.reverse()
1171
+ ?.map((p: any, i: Number) => (
1172
+ <Avatar
1173
+ key={'conversation-replies-key-' + i}
1174
+ className="bg-transparent"
1175
+ size={'sm'}
1176
+ >
1177
+ <AvatarFallbackText>
1178
+ {startCase(p?.author?.username?.charAt(0))}
1179
+ </AvatarFallbackText>
1180
+ <AvatarImage
1181
+ alt="user image"
1182
+ style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
1183
+ source={{
1184
+ uri: p?.author?.picture,
1185
+ }}
1186
+ />
1187
+ </Avatar>
1188
+ ))}
1189
+ </HStack>
1190
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
1191
+ {currentMessage?.replies?.totalCount}{' '}
1192
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
1193
+ </Text>
1194
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
1195
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
1196
+ </Text>
1197
+ </HStack>
1198
+ )}
1199
+ </>
1200
+ </TouchableHighlight>
1201
+ );
1202
+ }
1203
+ },
1204
+ [navigation, channelId, role],
1205
+ );
1206
+
1207
+ // const renderActions = useCallback((props) => {
1208
+ // return (
1209
+ // <Actions
1210
+ // {...props}
1211
+ // icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
1212
+ // />
1213
+ // );
1214
+ // }, [onSelectImages]);
1215
+
1216
+ // Render action buttons (including image upload)
1217
+ const renderActions = useCallback(
1218
+ (props) => {
1219
+ return (
1220
+ <Actions
1221
+ {...props}
1222
+ onPressActionButton={onSelectImages} // Direct action when pressing the button
1223
+ options={{
1224
+ ['Choose from Library']: () => {
1225
+ // Call the same image selection function used in the ActionSheet
1226
+ onSelectImages();
1227
+ },
1228
+ ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1229
+ }}
1230
+ optionTintColor="#000000"
1231
+ cancelButtonIndex={1} // Set the Cancel option as the cancel button
1232
+ icon={() => (
1233
+ <Box className="w-8 h-8 items-center justify-center">
1234
+ <Ionicons name="image" size={24} color={colors.blue[500]} />
498
1235
  </Box>
499
- ) : (
500
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
501
1236
  )}
502
- {/* <MessageText
503
- {...props}
504
- textStyle={{ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 } }}
505
- /> */}
506
- </>
1237
+ containerStyle={{
1238
+ alignItems: 'center',
1239
+ justifyContent: 'center',
1240
+ marginLeft: 8,
1241
+ marginBottom: 0,
1242
+ }}
1243
+ />
507
1244
  );
508
- } else {
509
- return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
510
- }
511
- };
512
-
513
- const renderActions = (props) => {
514
- return (
515
- <Actions
516
- {...props}
517
- icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
518
- />
519
- );
520
- };
1245
+ },
1246
+ [onSelectImages],
1247
+ );
521
1248
 
522
- const renderAccessory = (props) => {
1249
+ // Create a more visible and reliable image preview with cancel button
1250
+ const renderAccessory = useCallback(() => {
1251
+ if (!images.length) return null;
523
1252
  return (
524
- <Box>
525
- {selectedImage !== '' ? (
526
- <HStack className="items-center">
527
- <Image
528
- className="ml-3"
529
- key={selectedImage}
530
- alt={'image'}
531
- source={{ uri: selectedImage }}
532
- size={'xs'}
533
- />
534
- <Button
535
- variant={'solid'}
536
- className="bg-transparent"
537
- onPress={() => {
538
- setFiles([]);
539
- setImage('');
540
- setImages([]);
1253
+ <Box style={{ position: 'relative', height: 70, backgroundColor: 'transparent', justifyContent: 'center' }}>
1254
+ <ScrollView
1255
+ horizontal
1256
+ showsHorizontalScrollIndicator={false}
1257
+ style={{
1258
+ flexDirection: 'row',
1259
+ paddingLeft: 15,
1260
+ paddingRight: 5,
1261
+ }}
1262
+ contentContainerStyle={{
1263
+ alignItems: 'center',
1264
+ height: '100%',
1265
+ }}
1266
+ >
1267
+ {images.map((img, index) => (
1268
+ <View
1269
+ key={`image-preview-${index}`}
1270
+ style={{
1271
+ width: 40,
1272
+ height: 40,
1273
+ marginRight: 15,
1274
+ borderRadius: 4,
1275
+ backgroundColor: colors.gray[200],
1276
+ overflow: 'hidden',
1277
+ borderWidth: 1,
1278
+ borderColor: '#e0e0e0',
1279
+ position: 'relative',
1280
+ zIndex: 10,
541
1281
  }}
542
1282
  >
543
- <ButtonText className="color-black">Cancel</ButtonText>
544
- </Button>
545
- </HStack>
546
- ) : null}
1283
+ <Image
1284
+ alt={`selected image ${index + 1}`}
1285
+ source={{ uri: img.uri || img.url }}
1286
+ style={{ width: '100%', height: '100%' }}
1287
+ size={'md'}
1288
+ />
1289
+ {/* Cross button at top right */}
1290
+ <TouchableOpacity
1291
+ onPress={() => {
1292
+ const newImages = [...images];
1293
+ newImages.splice(index, 1);
1294
+ setImages(newImages);
1295
+ if (newImages.length === 0) {
1296
+ setSelectedImage('');
1297
+ }
1298
+ }}
1299
+ style={{
1300
+ position: 'absolute',
1301
+ top: -1,
1302
+ right: -1,
1303
+ backgroundColor: 'rgba(0,0,0,0.6)',
1304
+ borderRadius: 12,
1305
+ width: 20,
1306
+ height: 20,
1307
+ alignItems: 'center',
1308
+ justifyContent: 'center',
1309
+ zIndex: 9999,
1310
+ }}
1311
+ >
1312
+ <Ionicons name="close" size={16} color="white" />
1313
+ </TouchableOpacity>
1314
+ </View>
1315
+ ))}
1316
+ </ScrollView>
547
1317
  </Box>
548
1318
  );
549
- };
1319
+ }, [images]);
550
1320
 
551
- const setImageViewerObject = (obj: any, v: boolean) => {
1321
+ // Add a custom Composer for Slack-like input
1322
+ const renderComposer = useCallback(
1323
+ (props) => (
1324
+ <TouchableOpacity
1325
+ style={{
1326
+ flexDirection: 'row',
1327
+ alignItems: 'center',
1328
+ backgroundColor: '#fff',
1329
+ minHeight: 48,
1330
+ maxHeight: 48,
1331
+ paddingHorizontal: 12,
1332
+ paddingVertical: 0,
1333
+ marginHorizontal: 8,
1334
+ flex: 1,
1335
+ }}
1336
+ activeOpacity={0.7}
1337
+ onPress={() => {
1338
+ // Use the callback we defined to show the action sheet
1339
+ setActionSheetVisible(true);
1340
+ }}
1341
+ >
1342
+ {/* Left + button */}
1343
+ <TouchableOpacity
1344
+ onPress={(e) => {
1345
+ e.stopPropagation(); // Prevent parent TouchableOpacity from triggering
1346
+ onSelectImages();
1347
+ }}
1348
+ style={{
1349
+ width: 25,
1350
+ height: 25,
1351
+ borderRadius: 20,
1352
+ backgroundColor: '#f5f5f5',
1353
+ alignItems: 'center',
1354
+ justifyContent: 'center',
1355
+ marginRight: 8,
1356
+ }}
1357
+ >
1358
+ <MaterialIcons name="add" size={20} color="#888" />
1359
+ </TouchableOpacity>
1360
+
1361
+ {/* Text placeholder - no need for actual input component */}
1362
+ <Text
1363
+ style={{
1364
+ fontSize: 16,
1365
+ color: msg ? colors.gray[800] : colors.gray[400],
1366
+ flex: 1,
1367
+ }}
1368
+ numberOfLines={1}
1369
+ ellipsizeMode="tail"
1370
+ >
1371
+ {msg ? msg : 'Jot something down'}
1372
+ </Text>
1373
+ </TouchableOpacity>
1374
+ ),
1375
+ [msg, onSelectImages],
1376
+ );
1377
+
1378
+ const setImageViewerObject = useCallback((obj: any, v: boolean) => {
552
1379
  setImageObject(obj);
553
1380
  setImageViewer(v);
554
- };
1381
+ }, []);
555
1382
 
556
- const modalContent = React.useMemo(() => {
1383
+ const modalContent = useMemo(() => {
557
1384
  if (!imageObject) return <></>;
558
1385
  const { image, _id } = imageObject;
559
1386
  return (
560
1387
  <CachedImage
561
1388
  style={{ width: '100%', height: '100%' }}
562
1389
  resizeMode={'cover'}
563
- // cacheKey={`${_id}-conversation-modal-image-key`}
564
1390
  cacheKey={`${_id}-slack-bubble-imageKey`}
565
1391
  source={{
566
1392
  uri: image,
567
- //headers: `Authorization: Bearer ${token}`,
568
1393
  expiresIn: 86400,
569
1394
  }}
570
1395
  alt={'image'}
@@ -572,32 +1397,163 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
572
1397
  );
573
1398
  }, [imageObject]);
574
1399
 
575
- const renderMessage = (props: any) => {
576
- return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
577
- };
1400
+ const renderMessage1 = useCallback(
1401
+ (props: any) => {
1402
+ const { currentMessage } = props;
1403
+
1404
+ // Add special props for handling image upload states
1405
+ const customProps = {
1406
+ ...props,
1407
+ isShowImageViewer: isShowImageViewer,
1408
+ setImageViewer: setImageViewerObject,
1409
+ // Pass custom flags to indicate uploading/optimistic states
1410
+ isUploading: currentMessage.isUploading || false,
1411
+ isOptimistic: currentMessage.isOptimistic || false,
1412
+ };
1413
+
1414
+ // Use SlackMessage component with our enhanced props
1415
+ return <SlackMessage {...customProps} />;
1416
+ },
1417
+ [isShowImageViewer, setImageViewerObject],
1418
+ );
1419
+ const renderMessage = useCallback(
1420
+ (props: any) => {
1421
+ // For all messages, use the SlackMessage component directly
1422
+ return (
1423
+ <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1424
+ );
1425
+ },
1426
+ [isShowImageViewer],
1427
+ );
1428
+
1429
+ const renderInputToolbar = useCallback(
1430
+ (props) => {
1431
+ if (isActionSheetVisible) return null;
1432
+
1433
+ return (
1434
+ <InputToolbar
1435
+ {...props}
1436
+ containerStyle={{
1437
+ backgroundColor: 'transparent',
1438
+ padding: 0,
1439
+ margin: 0,
1440
+ borderTopWidth: 1,
1441
+ borderTopColor: colors.gray[200],
1442
+ }}
1443
+ primaryStyle={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}
1444
+ // Add action slot for direct image selection button
1445
+ // renderActions={() => (
1446
+ // <TouchableOpacity
1447
+ // onPress={onSelectImages}
1448
+ // style={{
1449
+ // padding: 8,
1450
+ // marginLeft: 4,
1451
+ // marginBottom: Platform.OS === 'ios' ? 0 : 3,
1452
+ // }}
1453
+ // >
1454
+ // <Ionicons name="image" size={24} color={colors.blue[500]} />
1455
+ // </TouchableOpacity>
1456
+ // )}
1457
+ />
1458
+ );
1459
+ },
1460
+ [isActionSheetVisible, onSelectImages],
1461
+ );
1462
+
1463
+ const renderLoadEarlier = useCallback(() => {
1464
+ return loadingOldMessages && !refreshing ? (
1465
+ <Box
1466
+ style={{
1467
+ padding: 10,
1468
+ backgroundColor: 'rgba(255,255,255,0.8)',
1469
+ borderRadius: 10,
1470
+ marginTop: 10,
1471
+ alignItems: 'center',
1472
+ }}
1473
+ >
1474
+ <Spinner size="small" color={colors.blue[500]} />
1475
+ <Text style={{ fontSize: 12, color: colors.gray[600], marginTop: 4 }}>Loading earlier messages...</Text>
1476
+ </Box>
1477
+ ) : null;
1478
+ }, [loadingOldMessages, refreshing]);
578
1479
 
579
1480
  let onScroll = false;
580
1481
 
581
1482
  const onMomentumScrollBegin = ({ nativeEvent }: any) => {
582
1483
  onScroll = true;
583
- console.log('scroll top');
584
1484
  if (!loadingOldMessages && isCloseToTop(nativeEvent) && totalCount > channelMessages?.length) {
585
1485
  onFetchOld();
586
1486
  }
587
1487
  };
588
1488
 
589
1489
  const onEndReached = () => {
590
- console.log('on end reached');
591
1490
  if (!onScroll) return;
592
- // load messages, show ActivityIndicator
593
1491
  onScroll = false;
594
- // setLoadingOldMessages(true);
595
1492
  };
596
1493
 
1494
+ const renderChatFooter = useCallback(() => {
1495
+ return (
1496
+ <>
1497
+ <ImageViewerModal
1498
+ isVisible={isShowImageViewer}
1499
+ setVisible={setImageViewer}
1500
+ modalContent={modalContent}
1501
+ />
1502
+ <SubscriptionHandler
1503
+ channelId={channelId}
1504
+ subscribeToNewMessages={() =>
1505
+ subscribeToMore({
1506
+ document: CHAT_MESSAGE_ADDED,
1507
+ variables: {
1508
+ channelId: channelId?.toString(),
1509
+ postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
1510
+ },
1511
+ updateQuery: (prev, { subscriptionData }: any) => {
1512
+ if (!subscriptionData.data) return prev;
1513
+ const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
1514
+ const prevReplyCount: any = prev?.getPostThread?.replyCount;
1515
+ const newReplyCount = prevReplyCount || 0 + 1;
1516
+ const replies = prev?.getPostThread?.replies || [];
1517
+ setChannelMessages((oldMessages: any) =>
1518
+ uniqBy([...oldMessages, newMessage], ({ id }) => id),
1519
+ );
1520
+ setTotalCount(newReplyCount);
1521
+ return Object.assign({}, prev, {
1522
+ getPostThread: {
1523
+ ...prev?.getPostThread,
1524
+ lastReplyAt: newMessage.createdAt,
1525
+ replies: [newMessage, ...replies],
1526
+ replyCount: newReplyCount,
1527
+ updatedAt: newMessage.createdAt,
1528
+ },
1529
+ });
1530
+ },
1531
+ })
1532
+ }
1533
+ />
1534
+ </>
1535
+ );
1536
+ }, [channelId, isShowImageViewer, modalContent, parentId, setImageViewer, subscribeToMore]);
1537
+
1538
+ const handleRemoveImage = useCallback(
1539
+ (index: number) => {
1540
+ const newImages = [...images];
1541
+ newImages.splice(index, 1);
1542
+ setImages(newImages);
1543
+ if (newImages.length === 0) {
1544
+ setSelectedImage('');
1545
+ if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
1546
+ textInputRef.current.focus();
1547
+ }
1548
+ }
1549
+ },
1550
+ [images],
1551
+ );
1552
+
597
1553
  return (
598
- <>
599
- {(loadingOldMessages || loadEarlierMsg) && <Spinner color={colors.blue[500]} />}
600
- {/* {loadEarlierMsg && <Spinner />} */}
1554
+ <View style={{ flex: 1, marginTop: -40 }}>
1555
+ {errorMessage ? <ErrorNotification message={errorMessage} onClose={() => setErrorMessage('')} /> : null}
1556
+ {loading && <Spinner color={'#3b82f6'} />}
601
1557
  {isPostParentIdThread && (
602
1558
  <>
603
1559
  {threadPost?.length > 0 && (
@@ -648,99 +1604,151 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
648
1604
  )}
649
1605
  </>
650
1606
  )}
651
- <GiftedChat
1607
+ <GiftedChatInboxComponent
652
1608
  ref={threadMessageListRef}
653
- wrapInSafeArea={false}
654
- renderLoading={() => <Spinner color={colors.blue[500]} />}
1609
+ onRemoveImage={handleRemoveImage}
1610
+ images={images}
1611
+ onSelectImages={onSelectImages}
1612
+ selectedImage={selectedImage}
1613
+ setSelectedImage={setSelectedImage}
1614
+ isUploadingImage={isUploadingImage}
1615
+ loading={loading}
1616
+ wrapInSafeArea={true}
1617
+ inverted={true}
1618
+ renderLoading={() => <Skeleton variant="rounded" style={{ flex: 1 }} />}
655
1619
  messages={messageList}
656
1620
  listViewProps={{
657
1621
  onEndReached: onEndReached,
658
1622
  onEndReachedThreshold: 0.5,
659
1623
  onMomentumScrollBegin: onMomentumScrollBegin,
1624
+ style: { marginTop: 0 },
1625
+ contentContainerStyle: {
1626
+ paddingTop: 0,
1627
+ paddingBottom: selectedImage ? 90 : 0,
1628
+ },
1629
+ refreshControl: (
1630
+ <RefreshControl
1631
+ refreshing={refreshing}
1632
+ onRefresh={onRefresh}
1633
+ colors={[colors.blue[500]]}
1634
+ tintColor={colors.blue[500]}
1635
+ title="Loading earlier messages..."
1636
+ titleColor={colors.gray[600]}
1637
+ />
1638
+ ),
660
1639
  }}
661
1640
  onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
662
- text={msg ? msg : ' '}
1641
+ text={msg || ' '}
663
1642
  onInputTextChanged={(text) => setMsg(text)}
664
- renderFooter={() =>
665
- loading ? (
666
- <Spinner color={colors.blue[500]} />
667
- ) : imageLoading ? (
668
- <Spinner color={colors.blue[500]} />
669
- ) : (
670
- ''
671
- )
672
- }
1643
+ renderFooter={() => null}
673
1644
  scrollToBottom
674
1645
  user={{
675
1646
  _id: auth?.id || '',
676
1647
  }}
677
- isTyping={true}
678
- alwaysShowSend={loading ? false : true}
679
- // onLoadEarlier={onFetchOld}
1648
+ placeholder="Jot something down"
680
1649
  infiniteScroll={true}
681
1650
  renderSend={renderSend}
682
- // loadEarlier={data?.messages?.totalCount > channelMessages.length}
683
- // isLoadingEarlier={loadEarlierMsg}
684
- // renderLoadEarlier={() =>
685
- // !loadEarlierMsg && (
686
- // <Center py={2}>
687
- // <Button
688
- // onPress={() => onFetchOld(channelMessages?.length)}
689
- // variant={'outline'}
690
- // _text={{ color: 'black', fontSize: 15, fontWeight: 'bold' }}
691
- // >
692
- // Load earlier messages
693
- // </Button>
694
- // </Center>
695
- // )
696
- // }
697
1651
  renderMessageText={renderMessageText}
698
- minInputToolbarHeight={50}
699
- renderActions={renderActions}
700
- renderAccessory={renderAccessory}
701
1652
  renderMessage={renderMessage}
702
- renderChatFooter={() => (
703
- <>
704
- <ImageViewerModal
705
- isVisible={isShowImageViewer}
706
- setVisible={setImageViewer}
707
- modalContent={modalContent}
708
- />
709
- <SubscriptionHandler
710
- channelId={channelId}
711
- subscribeToNewMessages={() =>
712
- subscribeToMore({
713
- document: CHAT_MESSAGE_ADDED,
714
- variables: {
715
- channelId: channelId?.toString(),
716
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
717
- },
718
- updateQuery: (prev, { subscriptionData }: any) => {
719
- if (!subscriptionData.data) return prev;
720
- const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
721
- const prevReplyCount: any = prev?.getPostThread?.replyCount;
722
- const newReplyCount = prevReplyCount || 0 + 1;
723
- const replies = prev?.getPostThread?.replies || [];
724
- setChannelMessages((oldMessages: any) =>
725
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
726
- );
727
- setTotalCount(newReplyCount);
728
- return Object.assign({}, prev, {
729
- getPostThread: {
730
- ...prev?.getPostThread,
731
- lastReplyAt: newMessage.createdAt,
732
- replies: [newMessage, ...replies],
733
- replyCount: newReplyCount,
734
- updatedAt: newMessage.createdAt,
735
- },
736
- });
737
- },
738
- })
739
- }
1653
+ renderLoadEarlier={renderLoadEarlier}
1654
+ loadEarlier={totalCount > channelMessages.length}
1655
+ isLoadingEarlier={loadingOldMessages}
1656
+ textInputProps={{
1657
+ multiline: true,
1658
+ returnKeyType: 'default',
1659
+ enablesReturnKeyAutomatically: true,
1660
+ placeholderTextColor: colors.gray[400],
1661
+ // Allow direct editing in the main input too for better UX
1662
+ editable: true,
1663
+ onFocus: () => setIsGiftedInputFocused(true),
1664
+ onBlur: () => setIsGiftedInputFocused(false),
1665
+ }}
1666
+ renderChatFooter={renderChatFooter}
1667
+ messagesContainerStyle={{
1668
+ ...(messageList?.length == 0 ? { transform: [{ scaleY: -1 }] } : {}),
1669
+ // marginTop: -40, // Negative margin to remove the top padding
1670
+ paddingTop: 0,
1671
+ }}
1672
+ // renderChatEmpty={() => (
1673
+ // <>
1674
+ // {!threadLoading && messageList && messageList?.length == 0 && (
1675
+ // <Box className="p-5">
1676
+ // <Center className="mt-6">
1677
+ // <Ionicons name="chatbubbles" size={30} />
1678
+ // <Text>You don't have any message yet!</Text>
1679
+ // </Center>
1680
+ // </Box>
1681
+ // )}
1682
+ // </>
1683
+ // )}
1684
+ />
1685
+ {/* <GiftedChat
1686
+ ref={threadMessageListRef}
1687
+ wrapInSafeArea={true}
1688
+ inverted={true}
1689
+ renderLoading={() => <Skeleton variant="rounded" style={{ flex: 1 }} />}
1690
+ messages={messageList}
1691
+ listViewProps={{
1692
+ onEndReached: onEndReached,
1693
+ onEndReachedThreshold: 0.5,
1694
+ onMomentumScrollBegin: onMomentumScrollBegin,
1695
+ style: { marginTop: 0 },
1696
+ contentContainerStyle: {
1697
+ paddingTop: 0,
1698
+ paddingBottom: selectedImage ? 90 : 0,
1699
+ },
1700
+ refreshControl: (
1701
+ <RefreshControl
1702
+ refreshing={refreshing}
1703
+ onRefresh={onRefresh}
1704
+ colors={[colors.blue[500]]}
1705
+ tintColor={colors.blue[500]}
1706
+ title="Loading earlier messages..."
1707
+ titleColor={colors.gray[600]}
740
1708
  />
741
- </>
742
- )}
743
- messagesContainerStyle={messageList?.length == 0 && { transform: [{ scaleY: -1 }] }}
1709
+ ),
1710
+ }}
1711
+ onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
1712
+ text={msg || ' '}
1713
+ onInputTextChanged={(text) => setMsg(text)}
1714
+ renderFooter={() => null}
1715
+ scrollToBottom
1716
+ user={{
1717
+ _id: auth?.id || '',
1718
+ }}
1719
+ isTyping={false}
1720
+ alwaysShowSend={true}
1721
+ infiniteScroll={true}
1722
+ renderSend={renderSend}
1723
+ renderMessageText={renderMessageText}
1724
+ renderInputToolbar={renderInputToolbar}
1725
+ renderComposer={renderComposer}
1726
+ minInputToolbarHeight={isActionSheetVisible ? 0 : 56}
1727
+ renderAccessory={images.length > 0 ? renderAccessory : null}
1728
+ renderMessage={renderMessage}
1729
+ renderLoadEarlier={renderLoadEarlier}
1730
+ loadEarlier={totalCount > channelMessages.length}
1731
+ isLoadingEarlier={loadingOldMessages}
1732
+ bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0}
1733
+ textInputProps={{
1734
+ multiline: true,
1735
+ returnKeyType: 'default',
1736
+ enablesReturnKeyAutomatically: true,
1737
+ placeholderTextColor: colors.gray[400],
1738
+ // Allow direct editing in the main input too for better UX
1739
+ editable: true,
1740
+ onFocus: () => setIsGiftedInputFocused(true),
1741
+ onBlur: () => setIsGiftedInputFocused(false),
1742
+ }}
1743
+ minComposerHeight={36}
1744
+ maxComposerHeight={100}
1745
+ isKeyboardInternallyHandled={false}
1746
+ renderChatFooter={renderChatFooter}
1747
+ messagesContainerStyle={{
1748
+ ...(messageList?.length == 0 ? { transform: [{ scaleY: -1 }] } : {}),
1749
+ // marginTop: -40, // Negative margin to remove the top padding
1750
+ paddingTop: 0,
1751
+ }}
744
1752
  renderChatEmpty={() => (
745
1753
  <>
746
1754
  {!threadLoading && messageList && messageList?.length == 0 && (
@@ -758,8 +1766,36 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
758
1766
  springConfig: { tension: 90000, friction: 90000 },
759
1767
  disabled: true,
760
1768
  }}
761
- />
762
- </>
1769
+ /> */}
1770
+
1771
+ {/* Add the expandable action sheet */}
1772
+ {/* {isActionSheetVisible && <Divider />}
1773
+ {isActionSheetVisible && (
1774
+ <ExpandableInputActionSheet
1775
+ isVisible={isActionSheetVisible}
1776
+ onClose={handleActionSheetClose}
1777
+ onSend={handleActionSheetSend}
1778
+ onSelectImages={onSelectImages}
1779
+ text={msg}
1780
+ onChangeText={(text) => {
1781
+ setMsg(text);
1782
+ setTextUpdatedInActionSheet(true);
1783
+ }}
1784
+ textInputRef={textInputRef}
1785
+ isSendDisabled={(!msg.trim() && images.length === 0) || isUploadingImage}
1786
+ selectedImages={images}
1787
+ loading={loading || isUploadingImage}
1788
+ onRemoveImage={(index) => {
1789
+ const newImages = [...images];
1790
+ newImages.splice(index, 1);
1791
+ setImages(newImages);
1792
+ if (newImages.length === 0) {
1793
+ setSelectedImage('');
1794
+ }
1795
+ }}
1796
+ />
1797
+ )} */}
1798
+ </View>
763
1799
  );
764
1800
  };
765
1801