@messenger-box/platform-mobile 10.0.3-alpha.5 → 10.0.3-alpha.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +96 -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/queries/inboxQueries.js +65 -0
  6. package/lib/queries/inboxQueries.js.map +1 -0
  7. package/lib/routes.json +2 -3
  8. package/lib/screens/inbox/DialogMessages.js +1 -1
  9. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  10. package/lib/screens/inbox/DialogThreadMessages.js +4 -8
  11. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  12. package/lib/screens/inbox/DialogThreads.js +57 -12
  13. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  14. package/lib/screens/inbox/Inbox.js +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/SlackBubble.js +147 -31
  25. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  26. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +6 -1
  27. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  28. package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
  29. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  30. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -55
  31. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  32. package/lib/screens/inbox/config/config.js +2 -2
  33. package/lib/screens/inbox/config/config.js.map +1 -1
  34. package/lib/screens/inbox/containers/ConversationView.js +1111 -434
  35. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  36. package/lib/screens/inbox/containers/Dialogs.js +193 -80
  37. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  38. package/lib/screens/inbox/containers/ThreadConversationView.js +725 -216
  39. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  40. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  41. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  42. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  43. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  44. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  45. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  46. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  47. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  48. package/package.json +4 -4
  49. package/src/compute.ts +5 -6
  50. package/src/index.ts +2 -0
  51. package/src/navigation/InboxNavigation.tsx +3 -3
  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 +1 -1
  56. package/src/screens/inbox/DialogThreadMessages.tsx +7 -14
  57. package/src/screens/inbox/DialogThreads.tsx +55 -61
  58. package/src/screens/inbox/Inbox.tsx +1 -1
  59. package/src/screens/inbox/components/Actionsheet.tsx +30 -0
  60. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  61. package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
  62. package/src/screens/inbox/components/DialogItem.tsx +306 -0
  63. package/src/screens/inbox/components/DialogsHeader.tsx +6 -13
  64. package/src/screens/inbox/components/DialogsListItem.tsx +262 -198
  65. package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
  66. package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
  67. package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
  68. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +337 -194
  69. package/src/screens/inbox/components/SlackInput.tsx +23 -0
  70. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
  71. package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +1 -1
  72. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  73. package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
  74. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +53 -55
  75. package/src/screens/inbox/components/ThreadsViewItem.tsx +178 -285
  76. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  77. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  78. package/src/screens/inbox/config/config.ts +2 -2
  79. package/src/screens/inbox/containers/ConversationView.tsx +1843 -702
  80. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  81. package/src/screens/inbox/containers/Dialogs.tsx +402 -204
  82. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +4 -4
  83. package/src/screens/inbox/containers/ThreadConversationView.tsx +1350 -319
  84. package/src/screens/inbox/containers/ThreadsView.tsx +105 -193
  85. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  86. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  87. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  88. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
  89. package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
  90. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  91. package/src/screens/inbox/index.ts +37 -0
  92. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  93. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
  94. package/tsconfig.json +11 -54
  95. package/lib/screens/inbox/components/DialogsListItem.js +0 -171
  96. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  97. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -171
  98. 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/lib/generated/generated.js';
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';
@@ -39,6 +59,9 @@ import { userSelector } from '@adminide-stack/user-auth0-client';
39
59
  import { config } from '../config';
40
60
  import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
41
61
  import CachedImage from '../components/CachedImage';
62
+ import colors from 'tailwindcss/colors';
63
+ import ExpandableInputActionSheet from '../components/ExpandableInputActionSheet';
64
+ import GiftedChatInboxComponent from '../components/GiftedChatInboxComponent';
42
65
 
43
66
  const {
44
67
  MESSAGES_PER_PAGE,
@@ -59,6 +82,9 @@ const createdAtText = (value: string) => {
59
82
  interface IMessageProps extends IMessage {
60
83
  type: string;
61
84
  propsConfiguration?: any;
85
+ images?: string[];
86
+ isOptimistic?: boolean;
87
+ isUploading?: boolean;
62
88
  }
63
89
 
64
90
  export interface AlertMessageAttachmentsInterface {
@@ -76,21 +102,88 @@ interface IThreadSubscriptionHandlerProps {
76
102
  channelId: string;
77
103
  }
78
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
+
79
171
  const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any) => {
80
172
  const { params } = useRoute<any>();
81
173
  const [channelToTop, setChannelToTop] = useState(0);
82
174
  const [channelMessages, setChannelMessages] = useState<any>([]);
83
175
  const auth: any = useSelector(userSelector);
84
176
  const [totalCount, setTotalCount] = useState<any>(0);
85
- const [selectedImage, setImage] = useState<string>('');
177
+ const [selectedImage, setSelectedImage] = useState<string>('');
86
178
  const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
87
179
  const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
88
180
  const navigation = useNavigation<any>();
89
181
  const [files, setFiles] = useState<File[]>([]);
90
- const [images, setImages] = useState<ImagePicker.ImagePickerAsset[]>([]);
182
+ const [images, setImages] = useState<ExtendedImagePickerAsset[]>([]);
91
183
  const [msg, setMsg] = useState<string>('');
92
184
  const [loading, setLoading] = useState(false);
93
185
  const [imageLoading, setImageLoading] = useState(false);
186
+ const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
94
187
  const [expoTokens, setExpoTokens] = useState<any[]>([]);
95
188
  const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
96
189
  const [imageObject, setImageObject] = useState<any>({});
@@ -100,15 +193,30 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
100
193
  const [threadPost, setThreadPost] = useState<any[]>([]);
101
194
  const [isScrollToBottom, setIsScrollToBottom] = useState(false);
102
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);
103
204
 
104
205
  // const [sendThreadMessage] = useSendThreadMessageMutation();
105
- const [sendThreadMessage] = useCreatePostThreadMutation();
106
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
206
+ const [sendThreadMessage] = useCreatePostThread();
207
+ const [sendExpoNotificationOnPostMutation] = useSendExpoNotification();
107
208
 
108
209
  const [
109
210
  getThreadMessages,
110
211
  { data, loading: threadLoading, fetchMore: fetchMoreMessages, refetch: refetchThreadMessages, subscribeToMore },
111
- ] = 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);
112
220
 
113
221
  useFocusEffect(
114
222
  React.useCallback(() => {
@@ -117,12 +225,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
117
225
  navigation.setOptions({
118
226
  title: params?.title ?? 'Thread',
119
227
  headerLeft: (props: any) => (
120
- <Button
121
- bg={'transparent'}
122
- $pressed-bg="$trueGray200"
123
- $active-bg={'$trueGray200'}
124
- onPress={() => navigation.goBack()}
125
- >
228
+ <Button className="bg-transparent active:bg-gray-200 " onPress={() => navigation.goBack()}>
126
229
  <MaterialIcons size={20} name="arrow-back-ios" color={'black'} />
127
230
  </Button>
128
231
  ),
@@ -203,6 +306,8 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
203
306
  const onFetchOld = useCallback(() => {
204
307
  if (totalCount > channelMessages.length && !loadingOldMessages) {
205
308
  setLoadEarlierMsg(true);
309
+ setLoadingOldMessages(true);
310
+
206
311
  fetchMoreMessages({
207
312
  variables: {
208
313
  channelId: channelId?.toString(),
@@ -224,14 +329,24 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
224
329
  .finally(() => {
225
330
  setLoadEarlierMsg(false);
226
331
  setLoadingOldMessages(false);
332
+ setRefreshing(false);
227
333
  })
228
334
  .catch((error: any) => {
229
335
  setLoadEarlierMsg(false);
230
336
  setLoadingOldMessages(false);
337
+ setRefreshing(false);
231
338
  });
339
+ } else {
340
+ setRefreshing(false);
232
341
  }
233
342
  }, [parentId, channelId, totalCount, channelMessages]);
234
343
 
344
+ // Pull to refresh handler
345
+ const onRefresh = useCallback(() => {
346
+ setRefreshing(true);
347
+ onFetchOld();
348
+ }, [onFetchOld]);
349
+
235
350
  // const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
236
351
  // return contentOffset.y <= 100; // 100px from top
237
352
  // };
@@ -253,118 +368,438 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
253
368
  return new File([u8arr], filename, { type: mime });
254
369
  };
255
370
 
256
- const onSelectImages = async () => {
257
- setImageLoading(true);
258
- let imageSource: any = await ImagePicker.launchImageLibraryAsync({
259
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
260
- allowsEditing: true,
261
- aspect: [4, 3],
262
- quality: 1,
263
- 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;
264
378
  });
265
- if (!imageSource.canceled) {
266
- const image = 'data:image/jpeg;base64,' + imageSource.assets[0]?.base64;
267
- setImage(image);
268
- const file = dataURLtoFile(image, 'inputImage.jpg');
269
- setFiles((files) => files.concat(file));
270
- 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);
271
508
  }
272
- 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);
273
520
  };
274
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
+
275
533
  const handleSend = useCallback(
276
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
+
277
538
  if (!channelId) return;
278
- 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);
279
548
 
280
549
  const postId = objectId();
550
+ const currentDate = new Date();
281
551
 
282
- if (images && images.length > 0) {
283
- setLoading(true);
284
- const uploadResponse = await startUpload({
285
- file: images,
286
- saveUploadedFile: {
287
- variables: {
288
- postId,
289
- },
290
- },
291
- createUploadLink: {
292
- variables: {
293
- postId,
294
- },
295
- },
296
- });
297
- if (uploadResponse?.error) setLoading(false);
298
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
299
- if (uploadResponse.data) {
300
- setImage('');
301
- setFiles([]);
302
- setImages([]);
303
- //setLoading(false);
304
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
305
- await sendThreadMessage({
306
- variables: {
307
- postId,
308
- channelId,
309
- postThreadId: postThread && postThread?.id,
310
- postParentId: !parentId || parentId == 0 ? null : parentId,
311
- threadMessageInput: {
312
- content: message,
313
- files,
314
- 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,
315
643
  },
316
- },
317
- update: (cache, { data, errors }: any) => {
318
- if (!data || errors) {
319
- setLoading(false);
320
- return;
321
- }
322
- setPostThread(data?.createPostThread?.data);
323
- const lastMessageId = data?.createPostThread?.lastMessage?.id;
324
- if (!parentId || parentId == 0) {
325
- setParentId(data?.createPostThread?.lastMessage?.id);
326
- }
644
+ }));
327
645
 
328
- setChannelToTop(channelToTop + 1);
329
- setLoading(false);
330
- setMsg('');
331
- const msg = message == '' ? 'Send a file' : message;
332
- sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
333
- },
334
- });
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
+ }
335
700
  }
336
- } else {
337
- setLoading(true);
701
+
702
+ // Send message with Apollo mutation
338
703
  await sendThreadMessage({
339
704
  variables: {
705
+ postId,
340
706
  channelId,
341
707
  postThreadId: postThread && postThread?.id,
342
- postParentId: !parentId || parentId == 0 ? null : parentId,
708
+ postParentId: !parentId || parentId === 0 ? null : parentId,
343
709
  threadMessageInput: {
344
- content: message,
710
+ content: currentMessageText,
711
+ files: fileIds,
345
712
  role,
346
713
  },
347
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
+ },
348
738
  update: (cache, { data, errors }: any) => {
349
- if (!data || errors) {
350
- setLoading(false);
351
- return;
352
- }
353
- setPostThread(data?.createPostThread.data);
739
+ if (!data || errors) return;
740
+
741
+ setPostThread(data?.createPostThread?.data);
354
742
  const lastMessageId = data?.createPostThread?.lastMessage?.id;
355
- if (!parentId || parentId == 0) {
743
+ if (!parentId || parentId === 0) {
356
744
  setParentId(data?.createPostThread?.lastMessage?.id);
357
745
  }
358
746
  setChannelToTop(channelToTop + 1);
359
- setLoading(false);
360
- setMsg('');
361
747
 
748
+ // Send push notification with message info
362
749
  sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
363
750
  },
364
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);
365
789
  }
366
790
  },
367
- [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
+ ],
368
803
  );
369
804
 
370
805
  const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
@@ -385,6 +820,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
385
820
  }
386
821
  };
387
822
 
823
+ // Improved messageList function to better handle images
388
824
  const messageList = useMemo(() => {
389
825
  let currentDate = '';
390
826
  let res: any = [];
@@ -407,17 +843,56 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
407
843
  message._id = msg.id;
408
844
  message.text = msg.message;
409
845
  message.createdAt = date;
410
- (message.user = {
846
+ message.user = {
411
847
  _id: msg?.author?.id ?? auth?.profile?.id,
412
848
  name:
413
849
  msg?.author?.givenName ??
414
850
  auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
415
851
  auth?.profile?.family_name,
416
852
  avatar: msg?.author?.picture ?? auth?.profile?.picture,
417
- }),
418
- (message.image = msg.files?.data[0]?.url),
419
- (message.sent = msg?.isDelivered),
420
- (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;
421
896
  message.type = msg?.type;
422
897
  message.propsConfiguration = msg?.propsConfiguration;
423
898
  res.push(message);
@@ -427,149 +902,494 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
427
902
  //return res;
428
903
  }, [channelMessages]);
429
904
 
430
- const renderSend = (props) => {
431
- return (
432
- <Send {...props}>
433
- <Box>
434
- <MaterialCommunityIcons
435
- name="send-circle"
436
- style={{ marginBottom: 5, marginRight: 5 }}
437
- size={32}
438
- color="#2e64e5"
439
- />
440
- </Box>
441
- </Send>
442
- );
443
- };
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
+ }
444
911
 
445
- const renderMessageText = (props: any) => {
446
- const { currentMessage } = props;
447
- if (currentMessage.type === 'ALERT') {
448
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
449
- let action: string = '';
450
- let actionId: any = '';
451
- let params: any = {};
452
- if (attachment?.callToAction?.extraParams) {
453
- const extraParams: any = attachment?.callToAction?.extraParams;
454
- const route: any = extraParams?.route ?? null;
455
- let path: any = null;
456
- let param: any = null;
457
- if (role && role == PreDefinedRole.Guest) {
458
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
459
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
460
- } else if (role && role == PreDefinedRole.Owner) {
461
- path = route?.host?.name ? route?.host?.name ?? null : null;
462
- param = route?.host?.params ? route?.host?.params ?? null : null;
463
- } else {
464
- path = route?.host?.name ? route?.host?.name ?? null : null;
465
- 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 };
466
983
  }
467
984
 
468
- action = path;
469
- params = { ...param };
470
- } else if (attachment?.callToAction?.link) {
471
- action = CALL_TO_ACTION_PATH;
472
- actionId = attachment?.callToAction?.link.split('/').pop();
473
- 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 } }} />;
1013
+ }
1014
+ },
1015
+ [navigation, role],
1016
+ );
1017
+
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;
1023
+
1024
+ // Do not render anything if the message text is empty or only whitespace
1025
+ if (!currentMessage?.text || currentMessage.text.trim() === '') {
1026
+ return null;
474
1027
  }
475
1028
 
476
- // if (attachment?.callToAction?.link?.includes('my-reservation-details')) {
477
- // action = CALL_TO_ACTION_PATH;
478
- // actionId = attachment?.callToAction?.link.split('/').pop();
479
- // }
1029
+ if (currentMessage.type === 'ALERT') {
1030
+ const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1031
+ let action: string = '';
1032
+ let actionId: any = '';
1033
+ let params: any = {};
480
1034
 
481
- return (
482
- <>
483
- {attachment?.callToAction && action ? (
484
- <Box bg={CALL_TO_ACTION_BOX_BGCOLOR} borderRadius={15} pb={2}>
485
- <Button
486
- variant={'outline'}
487
- size={'sm'}
488
- borderColor={CALL_TO_ACTION_BUTTON_BORDERCOLOR}
489
- onPress={() => action && params && navigation.navigate(action, params)}
490
- //onPress={() => navigation.navigate(action, { reservationId: actionId })}
491
- >
492
- <ButtonText color={CALL_TO_ACTION_TEXT_COLOR}>
493
- {attachment.callToAction.title}
494
- </ButtonText>
495
- </Button>
496
- <MessageText
497
- {...props}
498
- textStyle={{
499
- left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
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
+ });
500
1092
  }}
501
- />
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]} />
502
1235
  </Box>
503
- ) : (
504
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
505
1236
  )}
506
- {/* <MessageText
507
- {...props}
508
- textStyle={{ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 } }}
509
- /> */}
510
- </>
1237
+ containerStyle={{
1238
+ alignItems: 'center',
1239
+ justifyContent: 'center',
1240
+ marginLeft: 8,
1241
+ marginBottom: 0,
1242
+ }}
1243
+ />
511
1244
  );
512
- } else {
513
- return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
514
- }
515
- };
516
-
517
- const renderActions = (props) => {
518
- return (
519
- <Actions
520
- {...props}
521
- icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
522
- />
523
- );
524
- };
1245
+ },
1246
+ [onSelectImages],
1247
+ );
525
1248
 
526
- 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;
527
1252
  return (
528
- <Box>
529
- {selectedImage !== '' ? (
530
- <HStack alignItems={'center'}>
531
- <Image
532
- ml={'$3'}
533
- key={selectedImage}
534
- alt={'image'}
535
- source={{ uri: selectedImage }}
536
- size={'xs'}
537
- />
538
- <Button
539
- variant={'solid'}
540
- //bg={'$secondary400'}
541
- bg={'transparent'}
542
- onPress={() => {
543
- setFiles([]);
544
- setImage('');
545
- 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,
546
1281
  }}
547
1282
  >
548
- <ButtonText color={'$black'}>Cancel</ButtonText>
549
- </Button>
550
- </HStack>
551
- ) : 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>
552
1317
  </Box>
553
1318
  );
554
- };
1319
+ }, [images]);
555
1320
 
556
- 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) => {
557
1379
  setImageObject(obj);
558
1380
  setImageViewer(v);
559
- };
1381
+ }, []);
560
1382
 
561
- const modalContent = React.useMemo(() => {
1383
+ const modalContent = useMemo(() => {
562
1384
  if (!imageObject) return <></>;
563
1385
  const { image, _id } = imageObject;
564
1386
  return (
565
1387
  <CachedImage
566
1388
  style={{ width: '100%', height: '100%' }}
567
1389
  resizeMode={'cover'}
568
- // cacheKey={`${_id}-conversation-modal-image-key`}
569
1390
  cacheKey={`${_id}-slack-bubble-imageKey`}
570
1391
  source={{
571
1392
  uri: image,
572
- //headers: `Authorization: Bearer ${token}`,
573
1393
  expiresIn: 86400,
574
1394
  }}
575
1395
  alt={'image'}
@@ -577,39 +1397,170 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
577
1397
  );
578
1398
  }, [imageObject]);
579
1399
 
580
- const renderMessage = (props: any) => {
581
- return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
582
- };
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]);
583
1479
 
584
1480
  let onScroll = false;
585
1481
 
586
1482
  const onMomentumScrollBegin = ({ nativeEvent }: any) => {
587
1483
  onScroll = true;
588
- console.log('scroll top');
589
1484
  if (!loadingOldMessages && isCloseToTop(nativeEvent) && totalCount > channelMessages?.length) {
590
1485
  onFetchOld();
591
1486
  }
592
1487
  };
593
1488
 
594
1489
  const onEndReached = () => {
595
- console.log('on end reached');
596
1490
  if (!onScroll) return;
597
- // load messages, show ActivityIndicator
598
1491
  onScroll = false;
599
- // setLoadingOldMessages(true);
600
1492
  };
601
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
+
602
1553
  return (
603
- <>
604
- {(loadingOldMessages || loadEarlierMsg) && <Spinner color={'$blue500'} />}
605
- {/* {loadEarlierMsg && <Spinner />} */}
1554
+ <View style={{ flex: 1, marginTop: -40 }}>
1555
+ {errorMessage ? <ErrorNotification message={errorMessage} onClose={() => setErrorMessage('')} /> : null}
1556
+ {loading && <Spinner color={'#3b82f6'} />}
606
1557
  {isPostParentIdThread && (
607
1558
  <>
608
1559
  {threadPost?.length > 0 && (
609
1560
  <>
610
- <VStack px={'$2'} pt={'$2'} pb={'$0'} space={'sm'}>
611
- <HStack space={'sm'} alignItems={'center'}>
612
- <Avatar bg={'transparent'} size={'md'}>
1561
+ <VStack className="px-2 pt-2 pb-0" space={'sm'}>
1562
+ <HStack space={'sm'} className="items-center">
1563
+ <Avatar className="bg-transparent" size={'md'}>
613
1564
  <AvatarFallbackText>
614
1565
  {startCase(threadPost[0]?.author?.username?.charAt(0))}
615
1566
  </AvatarFallbackText>
@@ -626,30 +1577,24 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
626
1577
  />
627
1578
  </Avatar>
628
1579
  <Box>
629
- <Text color={'$black'} fontWeight={'$bold'}>
1580
+ <Text className="font-bold color-black">
630
1581
  {threadPost[0]?.author?.givenName ?? ''}{' '}
631
1582
  {threadPost[0]?.author?.familyName ?? ''}
632
1583
  </Text>
633
- <Text color={'$trueGray500'} pl={'$0'}>
1584
+ <Text className="pl-0 color-gray-500">
634
1585
  {createdAtText(threadPost[0]?.createdAt)} at{' '}
635
1586
  {format(new Date(threadPost[0]?.createdAt), 'hh:ss:a')}
636
1587
  </Text>
637
1588
  </Box>
638
1589
  </HStack>
639
- <HStack px={'$2'} space={'sm'} alignItems={'center'}>
1590
+ <HStack space={'sm'} className="px-2 items-center">
640
1591
  <Text>{threadPost[0]?.message ?? ''}</Text>
641
1592
  </HStack>
642
1593
  </VStack>
643
1594
 
644
- <Box py={'$4'}>
645
- <Box
646
- px={'$4'}
647
- borderTopWidth={'$1'}
648
- borderBottomWidth={'$1'}
649
- py={'$2'}
650
- borderColor={'$trueGray200'}
651
- >
652
- <Text color={'$trueGray600'} fontWeight={'$bold'}>
1595
+ <Box className="py-4">
1596
+ <Box className="px-4 py-2 border-t border-b border-gray-200">
1597
+ <Text className="font-bold color-gray-600">
653
1598
  {threadPost[0]?.replies?.totalCount}{' '}
654
1599
  {threadPost[0]?.replies?.totalCount > 0 ? 'replies' : 'reply'}
655
1600
  </Text>
@@ -659,98 +1604,156 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
659
1604
  )}
660
1605
  </>
661
1606
  )}
662
- <GiftedChat
1607
+ <GiftedChatInboxComponent
663
1608
  ref={threadMessageListRef}
664
- wrapInSafeArea={false}
665
- renderLoading={() => <Spinner color={'$blue500'} />}
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 }} />}
666
1619
  messages={messageList}
667
1620
  listViewProps={{
668
1621
  onEndReached: onEndReached,
669
1622
  onEndReachedThreshold: 0.5,
670
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
+ ),
671
1639
  }}
672
1640
  onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
673
- text={msg ? msg : ' '}
1641
+ text={msg || ' '}
674
1642
  onInputTextChanged={(text) => setMsg(text)}
675
- renderFooter={() =>
676
- loading ? <Spinner color={'$blue500'} /> : imageLoading ? <Spinner color={'$blue500'} /> : ''
677
- }
1643
+ renderFooter={() => null}
678
1644
  scrollToBottom
679
1645
  user={{
680
1646
  _id: auth?.id || '',
681
1647
  }}
682
- isTyping={true}
683
- alwaysShowSend={loading ? false : true}
684
- // onLoadEarlier={onFetchOld}
1648
+ placeholder="Jot something down"
685
1649
  infiniteScroll={true}
686
1650
  renderSend={renderSend}
687
- // loadEarlier={data?.messages?.totalCount > channelMessages.length}
688
- // isLoadingEarlier={loadEarlierMsg}
689
- // renderLoadEarlier={() =>
690
- // !loadEarlierMsg && (
691
- // <Center py={2}>
692
- // <Button
693
- // onPress={() => onFetchOld(channelMessages?.length)}
694
- // variant={'outline'}
695
- // _text={{ color: 'black', fontSize: 15, fontWeight: 'bold' }}
696
- // >
697
- // Load earlier messages
698
- // </Button>
699
- // </Center>
700
- // )
701
- // }
702
1651
  renderMessageText={renderMessageText}
703
- minInputToolbarHeight={50}
704
- renderActions={renderActions}
705
- renderAccessory={renderAccessory}
706
1652
  renderMessage={renderMessage}
707
- renderChatFooter={() => (
708
- <>
709
- <ImageViewerModal
710
- isVisible={isShowImageViewer}
711
- setVisible={setImageViewer}
712
- modalContent={modalContent}
713
- />
714
- <SubscriptionHandler
715
- channelId={channelId}
716
- subscribeToNewMessages={() =>
717
- subscribeToMore({
718
- document: CHAT_MESSAGE_ADDED,
719
- variables: {
720
- channelId: channelId?.toString(),
721
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
722
- },
723
- updateQuery: (prev, { subscriptionData }: any) => {
724
- if (!subscriptionData.data) return prev;
725
- const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
726
- const prevReplyCount: any = prev?.getPostThread?.replyCount;
727
- const newReplyCount = prevReplyCount || 0 + 1;
728
- const replies = prev?.getPostThread?.replies || [];
729
- setChannelMessages((oldMessages: any) =>
730
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
731
- );
732
- setTotalCount(newReplyCount);
733
- return Object.assign({}, prev, {
734
- getPostThread: {
735
- ...prev?.getPostThread,
736
- lastReplyAt: newMessage.createdAt,
737
- replies: [newMessage, ...replies],
738
- replyCount: newReplyCount,
739
- updatedAt: newMessage.createdAt,
740
- },
741
- });
742
- },
743
- })
744
- }
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]}
745
1708
  />
746
- </>
747
- )}
748
- 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
+ }}
749
1752
  renderChatEmpty={() => (
750
1753
  <>
751
1754
  {!threadLoading && messageList && messageList?.length == 0 && (
752
- <Box p={'$5'}>
753
- <Center mt={'$6'}>
1755
+ <Box className="p-5">
1756
+ <Center className="mt-6">
754
1757
  <Ionicons name="chatbubbles" size={30} />
755
1758
  <Text>You don't have any message yet!</Text>
756
1759
  </Center>
@@ -763,8 +1766,36 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
763
1766
  springConfig: { tension: 90000, friction: 90000 },
764
1767
  disabled: true,
765
1768
  }}
766
- />
767
- </>
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>
768
1799
  );
769
1800
  };
770
1801