@messenger-box/platform-mobile 10.0.3-alpha.40 → 10.0.3-alpha.46

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 (54) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/compute.js +2 -3
  3. package/lib/index.js.map +1 -1
  4. package/lib/queries/inboxQueries.js +77 -0
  5. package/lib/queries/inboxQueries.js.map +1 -0
  6. package/lib/routes.json +2 -3
  7. package/lib/screens/inbox/DialogThreadMessages.js +3 -7
  8. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  9. package/lib/screens/inbox/DialogThreads.js +3 -7
  10. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  11. package/lib/screens/inbox/components/DialogsListItem.js +47 -46
  12. package/lib/screens/inbox/components/DialogsListItem.js.map +1 -1
  13. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
  14. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  15. package/lib/screens/inbox/components/ServiceDialogsListItem.js +72 -57
  16. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +1 -1
  17. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +115 -14
  18. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  19. package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
  20. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  21. package/lib/screens/inbox/containers/ConversationView.js +640 -493
  22. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  23. package/lib/screens/inbox/containers/Dialogs.js +100 -181
  24. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  25. package/lib/screens/inbox/containers/ThreadConversationView.js +659 -245
  26. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  27. package/lib/screens/inbox/containers/ThreadsView.js +3 -3
  28. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  29. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  30. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  31. package/package.json +4 -4
  32. package/src/index.ts +2 -0
  33. package/src/queries/inboxQueries.ts +298 -0
  34. package/src/queries/index.d.ts +2 -0
  35. package/src/queries/index.ts +1 -0
  36. package/src/screens/inbox/DialogThreadMessages.tsx +3 -11
  37. package/src/screens/inbox/DialogThreads.tsx +3 -7
  38. package/src/screens/inbox/components/Actionsheet.tsx +30 -0
  39. package/src/screens/inbox/components/DialogsListItem.tsx +89 -148
  40. package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
  41. package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
  42. package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
  43. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +202 -221
  44. package/src/screens/inbox/components/SlackInput.tsx +23 -0
  45. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +216 -30
  46. package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
  47. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +6 -7
  48. package/src/screens/inbox/containers/ConversationView.tsx +1109 -669
  49. package/src/screens/inbox/containers/Dialogs.tsx +198 -342
  50. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +2 -2
  51. package/src/screens/inbox/containers/ThreadConversationView.tsx +1141 -402
  52. package/src/screens/inbox/containers/ThreadsView.tsx +5 -5
  53. package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
  54. package/src/screens/inbox/machines/threadsMachine.ts +2 -2
@@ -14,8 +14,22 @@ import {
14
14
  Spinner,
15
15
  Text,
16
16
  Skeleton,
17
+ Toast,
18
+ ToastTitle,
19
+ ToastDescription,
20
+ useToast,
21
+ Divider,
17
22
  } from '@admin-layout/gluestack-ui-mobile';
18
- import { Platform, TouchableHighlight, View } 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';
19
33
  import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
20
34
  import { useSelector } from 'react-redux';
21
35
  import { orderBy, startCase, uniqBy } from 'lodash-es';
@@ -33,14 +47,11 @@ import {
33
47
  PostTypeEnum,
34
48
  } from 'common';
35
49
  import {
36
- OnThreadChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
37
- useCreatePostThreadMutation,
38
- useOnThreadChatMessageAddedSubscription,
39
- useSendExpoNotificationOnPostMutation,
40
- useSendThreadMessageMutation,
41
- useThreadMessagesLazyQuery,
42
- useGetPostThreadLazyQuery,
43
- } from 'common/graphql';
50
+ CHAT_MESSAGE_ADDED,
51
+ useCreatePostThread,
52
+ useSendExpoNotification,
53
+ useGetPostThreadLazy,
54
+ } from '../../../queries/inboxQueries';
44
55
  import { useUploadFilesNative } from '@messenger-box/platform-client';
45
56
  import { objectId } from '@messenger-box/core';
46
57
  import { format, isToday, isYesterday } from 'date-fns';
@@ -49,6 +60,8 @@ import { config } from '../config';
49
60
  import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
50
61
  import CachedImage from '../components/CachedImage';
51
62
  import colors from 'tailwindcss/colors';
63
+ import ExpandableInputActionSheet from '../components/ExpandableInputActionSheet';
64
+ import GiftedChatInboxComponent from '../components/GiftedChatInboxComponent';
52
65
 
53
66
  const {
54
67
  MESSAGES_PER_PAGE,
@@ -69,6 +82,9 @@ const createdAtText = (value: string) => {
69
82
  interface IMessageProps extends IMessage {
70
83
  type: string;
71
84
  propsConfiguration?: any;
85
+ images?: string[];
86
+ isOptimistic?: boolean;
87
+ isUploading?: boolean;
72
88
  }
73
89
 
74
90
  export interface AlertMessageAttachmentsInterface {
@@ -93,13 +109,72 @@ interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
93
109
  mimeType?: string;
94
110
  }
95
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
+
96
171
  const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any) => {
97
172
  const { params } = useRoute<any>();
98
173
  const [channelToTop, setChannelToTop] = useState(0);
99
174
  const [channelMessages, setChannelMessages] = useState<any>([]);
100
175
  const auth: any = useSelector(userSelector);
101
176
  const [totalCount, setTotalCount] = useState<any>(0);
102
- const [selectedImage, setImage] = useState<string>('');
177
+ const [selectedImage, setSelectedImage] = useState<string>('');
103
178
  const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
104
179
  const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
105
180
  const navigation = useNavigation<any>();
@@ -118,15 +193,30 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
118
193
  const [threadPost, setThreadPost] = useState<any[]>([]);
119
194
  const [isScrollToBottom, setIsScrollToBottom] = useState(false);
120
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);
121
204
 
122
205
  // const [sendThreadMessage] = useSendThreadMessageMutation();
123
- const [sendThreadMessage] = useCreatePostThreadMutation();
124
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
206
+ const [sendThreadMessage] = useCreatePostThread();
207
+ const [sendExpoNotificationOnPostMutation] = useSendExpoNotification();
125
208
 
126
209
  const [
127
210
  getThreadMessages,
128
211
  { data, loading: threadLoading, fetchMore: fetchMoreMessages, refetch: refetchThreadMessages, subscribeToMore },
129
- ] = 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);
130
220
 
131
221
  useFocusEffect(
132
222
  React.useCallback(() => {
@@ -216,6 +306,8 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
216
306
  const onFetchOld = useCallback(() => {
217
307
  if (totalCount > channelMessages.length && !loadingOldMessages) {
218
308
  setLoadEarlierMsg(true);
309
+ setLoadingOldMessages(true);
310
+
219
311
  fetchMoreMessages({
220
312
  variables: {
221
313
  channelId: channelId?.toString(),
@@ -237,14 +329,24 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
237
329
  .finally(() => {
238
330
  setLoadEarlierMsg(false);
239
331
  setLoadingOldMessages(false);
332
+ setRefreshing(false);
240
333
  })
241
334
  .catch((error: any) => {
242
335
  setLoadEarlierMsg(false);
243
336
  setLoadingOldMessages(false);
337
+ setRefreshing(false);
244
338
  });
339
+ } else {
340
+ setRefreshing(false);
245
341
  }
246
342
  }, [parentId, channelId, totalCount, channelMessages]);
247
343
 
344
+ // Pull to refresh handler
345
+ const onRefresh = useCallback(() => {
346
+ setRefreshing(true);
347
+ onFetchOld();
348
+ }, [onFetchOld]);
349
+
248
350
  // const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
249
351
  // return contentOffset.y <= 100; // 100px from top
250
352
  // };
@@ -266,81 +368,206 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
266
368
  return new File([u8arr], filename, { type: mime });
267
369
  };
268
370
 
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;
378
+ });
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
269
438
  const onSelectImages = async () => {
270
- setImageLoading(true);
439
+ setLoading(true);
271
440
 
272
441
  try {
273
442
  let imageSource = await ImagePicker.launchImageLibraryAsync({
274
443
  mediaTypes: ImagePicker.MediaTypeOptions.Images,
275
- allowsEditing: true,
444
+ allowsEditing: false,
276
445
  aspect: [4, 3],
277
446
  quality: 0.8,
278
447
  base64: true,
279
448
  exif: false,
449
+ allowsMultipleSelection: true, // Enable multiple selection
280
450
  });
281
451
 
282
452
  if (!imageSource?.canceled) {
283
- // Get the asset
284
- const selectedAsset = imageSource?.assets?.[0];
285
- if (!selectedAsset) {
286
- setImageLoading(false);
453
+ // Get all selected assets
454
+ const selectedAssets = imageSource?.assets || [];
455
+ if (selectedAssets.length === 0) {
456
+ setLoading(false);
287
457
  return;
288
458
  }
289
459
 
290
- // Create a base64 image string for preview
291
- const base64Data = selectedAsset.base64;
292
- const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
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
+ });
293
476
 
294
- // Format the asset for upload service requirements
295
- const asset: ExtendedImagePickerAsset = {
296
- ...selectedAsset,
297
- url: selectedAsset.uri,
298
- fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
299
- mimeType: 'image/jpeg',
300
- };
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
+ }
301
483
 
302
- // Update state with the new image
303
- setImage(previewImage);
304
- setImages([asset]);
305
- setImageLoading(false);
484
+ // Add new images to existing ones
485
+ setImages((currentImages) => [...currentImages, ...newImages]);
486
+ setLoading(false); // Set loading to false after successful selection
306
487
  } else {
307
- setImageLoading(false);
488
+ setLoading(false);
308
489
  }
309
490
  } catch (error) {
310
- console.error('Error selecting image:', error);
311
- setImageLoading(false);
491
+ setLoading(false);
312
492
  }
313
493
  };
314
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);
508
+ }
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);
520
+ };
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
+
315
533
  const handleSend = useCallback(
316
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
+
317
538
  if (!channelId) return;
318
- 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);
319
548
 
320
549
  const postId = objectId();
321
550
  const currentDate = new Date();
322
551
 
323
- setUploadingMessageId(postId);
324
-
325
- // Create a temporary preview of the image if uploading
326
- const previewImageData =
327
- images.length > 0
328
- ? {
329
- id: objectId(),
330
- url: selectedImage,
331
- name: images[0]?.fileName || 'image.jpg',
332
- extension: 'jpg',
333
- mimeType: 'image/jpeg',
334
- refType: FileRefType.Post,
335
- height: images[0]?.height || 0,
336
- width: images[0]?.width || 0,
337
- }
338
- : null;
339
-
340
- // Create optimistic message with common properties
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
341
568
  const optimisticMessage = {
342
569
  id: postId,
343
- message: message || (images.length > 0 ? ' ' : ''),
570
+ message: currentMessageText || (currentImages.length > 0 ? ' ' : ''),
344
571
  createdAt: currentDate.toISOString(),
345
572
  author: {
346
573
  id: auth?.profile?.id,
@@ -348,60 +575,127 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
348
575
  familyName: auth?.profile?.family_name,
349
576
  picture: auth?.profile?.picture,
350
577
  },
351
- isDelivered: false,
578
+ isDelivered: false, // Mark as not delivered yet during upload
352
579
  isRead: false,
353
- files: previewImageData
354
- ? {
355
- data: [previewImageData],
356
- totalCount: 1,
357
- }
358
- : undefined,
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,
359
601
  };
360
602
 
361
- // Add optimistic message to UI immediately
362
- // setChannelMessages((oldMessages: any) => uniqBy([optimisticMessage, ...oldMessages], ({ id }) => id));
363
- setMsg('');
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
+ }
364
611
 
365
612
  try {
366
613
  let fileIds: string[] | null = null;
367
614
 
368
615
  // Handle file uploads if images exist
369
- if (images && images.length > 0) {
370
- setLoading(true);
371
- // Format images for upload
372
- const imagesToUpload = images.map((img) => {
373
- return {
374
- ...img,
375
- uri: img.uri || img.url, // Use either uri or url
376
- type: img.mimeType || 'image/jpeg',
377
- name: img.fileName || `image_${Date.now()}.jpg`,
378
- };
379
- });
380
-
381
- const uploadResponse = await startUpload({
382
- file: imagesToUpload as unknown as ImagePicker.ImagePickerAsset[],
383
- saveUploadedFile: {
384
- variables: {
385
- postId,
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,
386
643
  },
387
- },
388
- createUploadLink: {
389
- variables: {
390
- postId,
644
+ }));
645
+
646
+ const uploadResponse = await startUpload({
647
+ file: imagesToUpload as unknown as ImagePicker.ImagePickerAsset[],
648
+ saveUploadedFile: {
649
+ variables: {
650
+ postId,
651
+ },
391
652
  },
392
- },
393
- });
653
+ createUploadLink: {
654
+ variables: {
655
+ postId,
656
+ },
657
+ },
658
+ });
394
659
 
395
- if (uploadResponse?.error) throw new Error(uploadResponse.error.toString());
660
+ if (uploadResponse?.error) {
661
+ throw new Error(uploadResponse.error.toString());
662
+ }
396
663
 
397
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
398
- if (uploadResponse.data) {
399
- // Clear image related states after successful upload
400
- setImage('');
401
- setFiles([]);
402
- setImages([]);
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
+ }
403
692
 
404
- fileIds = uploadedFiles?.map((f: any) => f.id) ?? null;
693
+ // Clear pending upload now that it's complete
694
+ setPendingUploads((prev) => {
695
+ const newPending = { ...prev };
696
+ delete newPending[postId];
697
+ return newPending;
698
+ });
405
699
  }
406
700
  }
407
701
 
@@ -413,11 +707,34 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
413
707
  postThreadId: postThread && postThread?.id,
414
708
  postParentId: !parentId || parentId === 0 ? null : parentId,
415
709
  threadMessageInput: {
416
- content: message,
710
+ content: currentMessageText,
417
711
  files: fileIds,
418
712
  role,
419
713
  },
420
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
+ },
421
738
  update: (cache, { data, errors }: any) => {
422
739
  if (!data || errors) return;
423
740
 
@@ -432,21 +749,57 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
432
749
  sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
433
750
  },
434
751
  });
435
-
436
- // Update the optimistic message to show it's delivered
437
- setChannelMessages((oldMessages: any) =>
438
- oldMessages.map((msg: any) => (msg.id === postId ? { ...msg, isDelivered: true } : msg)),
439
- );
440
752
  } catch (error) {
441
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
+
442
780
  // Remove the optimistic message if there was an error
443
781
  setChannelMessages((oldMessages: any) => oldMessages.filter((msg) => msg.id !== postId));
782
+
783
+ // Clean up any pending upload states
784
+ removeMessageFromUI(postId);
444
785
  } finally {
445
786
  setLoading(false);
787
+ setIsUploadingImage(false);
446
788
  setUploadingMessageId(null);
447
789
  }
448
790
  },
449
- [auth, channelId, channelToTop, images, parentId, postThread, selectedImage, setChannelMessages],
791
+ [
792
+ auth,
793
+ channelId,
794
+ channelToTop,
795
+ images,
796
+ parentId,
797
+ postThread,
798
+ selectedImage,
799
+ setChannelMessages,
800
+ removeMessageFromUI,
801
+ role,
802
+ ],
450
803
  );
451
804
 
452
805
  const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
@@ -467,6 +820,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
467
820
  }
468
821
  };
469
822
 
823
+ // Improved messageList function to better handle images
470
824
  const messageList = useMemo(() => {
471
825
  let currentDate = '';
472
826
  let res: any = [];
@@ -489,17 +843,56 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
489
843
  message._id = msg.id;
490
844
  message.text = msg.message;
491
845
  message.createdAt = date;
492
- (message.user = {
846
+ message.user = {
493
847
  _id: msg?.author?.id ?? auth?.profile?.id,
494
848
  name:
495
849
  msg?.author?.givenName ??
496
850
  auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
497
851
  auth?.profile?.family_name,
498
852
  avatar: msg?.author?.picture ?? auth?.profile?.picture,
499
- }),
500
- (message.image = msg.files?.data[0]?.url),
501
- (message.sent = msg?.isDelivered),
502
- (message.received = msg?.isRead);
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;
503
896
  message.type = msg?.type;
504
897
  message.propsConfiguration = msg?.propsConfiguration;
505
898
  res.push(message);
@@ -509,22 +902,55 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
509
902
  //return res;
510
903
  }, [channelMessages]);
511
904
 
512
- const renderSend = useCallback((props) => {
513
- return (
514
- <Send {...props}>
515
- <Box>
516
- <MaterialCommunityIcons
517
- name="send-circle"
518
- style={{ marginBottom: 5, marginRight: 5 }}
519
- size={32}
520
- color="#2e64e5"
521
- />
522
- </Box>
523
- </Send>
524
- );
525
- }, []);
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
+ }
526
911
 
527
- const renderMessageText = useCallback(
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(
528
954
  (props: any) => {
529
955
  const { currentMessage } = props;
530
956
  if (currentMessage.type === 'ALERT') {
@@ -589,6 +1015,195 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
589
1015
  [navigation, role],
590
1016
  );
591
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;
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
+ });
1092
+ }}
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
+
592
1207
  // const renderActions = useCallback((props) => {
593
1208
  // return (
594
1209
  // <Actions
@@ -599,189 +1214,167 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
599
1214
  // }, [onSelectImages]);
600
1215
 
601
1216
  // Render action buttons (including image upload)
602
- const renderActions = (props) => {
603
- return (
604
- <Actions
605
- {...props}
606
- options={{
607
- ['Choose from Library']: onSelectImages,
608
- ['Cancel']: () => {}, // Add this option to make the sheet dismissible
609
- }}
610
- optionTintColor="#000000"
611
- cancelButtonIndex={1} // Set the Cancel option as the cancel button
612
- icon={() => (
613
- <Box className="w-8 h-8 items-center justify-center">
614
- <Ionicons name="image" size={24} color={colors.blue[500]} />
615
- </Box>
616
- )}
617
- containerStyle={{
618
- alignItems: 'center',
619
- justifyContent: 'center',
620
- marginLeft: 8,
621
- marginBottom: 0,
622
- }}
623
- />
624
- );
625
- };
626
-
627
- // const renderAccessory = useCallback(() => {
628
- // if (!selectedImage) return null;
629
-
630
- // return (
631
- // <Box className="bg-white border-t border-gray-200 px-2 py-2">
632
- // <HStack className="items-center justify-between">
633
- // <HStack space="sm" className="items-center">
634
- // {imageLoading ? (
635
- // <Box className="w-12 h-12 rounded-md bg-gray-100 overflow-hidden flex items-center justify-center">
636
- // <Spinner size="small" color={colors.blue[500]} />
637
- // </Box>
638
- // ) : (
639
- // <Box className="relative">
640
- // <Image
641
- // className="rounded-md"
642
- // key={selectedImage}
643
- // alt={'Selected image'}
644
- // source={{ uri: selectedImage }}
645
- // style={{
646
- // width: 48,
647
- // height: 48,
648
- // borderRadius: 4,
649
- // }}
650
- // />
651
- // <Box className="absolute -top-1 -right-1 bg-white rounded-full shadow-sm">
652
- // <Button
653
- // variant="link"
654
- // size="xs"
655
- // onPress={() => {
656
- // setFiles([]);
657
- // setImage('');
658
- // setImages([]);
659
- // }}
660
- // >
661
- // <MaterialIcons name="cancel" size={16} color={colors.red[500]} />
662
- // </Button>
663
- // </Box>
664
- // </Box>
665
- // )}
666
- // <Text className="text-sm text-gray-600 ml-2">
667
- // {imageLoading ? 'Processing image...' : 'Ready to send'}
668
- // </Text>
669
- // </HStack>
670
- // </HStack>
671
- // </Box>
672
- // );
673
- // }, [selectedImage, imageLoading]);
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]} />
1235
+ </Box>
1236
+ )}
1237
+ containerStyle={{
1238
+ alignItems: 'center',
1239
+ justifyContent: 'center',
1240
+ marginLeft: 8,
1241
+ marginBottom: 0,
1242
+ }}
1243
+ />
1244
+ );
1245
+ },
1246
+ [onSelectImages],
1247
+ );
674
1248
 
1249
+ // Create a more visible and reliable image preview with cancel button
675
1250
  const renderAccessory = useCallback(() => {
676
- if (!selectedImage) {
677
- return null;
678
- }
679
-
1251
+ if (!images.length) return null;
680
1252
  return (
681
- <View
682
- style={{
683
- height: 70,
684
- backgroundColor: 'white',
685
- borderTopWidth: 1,
686
- borderTopColor: '#e0e0e0',
687
- flexDirection: 'row',
688
- alignItems: 'center',
689
- margin: 0,
690
- padding: 0,
691
- paddingVertical: 0,
692
- position: 'absolute',
693
- bottom: Platform.OS === 'ios' ? 105 : 95, // Position well above the input area
694
- left: 0,
695
- right: 0,
696
- zIndex: 1,
697
- elevation: 3,
698
- shadowColor: '#000',
699
- shadowOffset: { width: 0, height: -1 },
700
- shadowOpacity: 0.05,
701
- shadowRadius: 2,
702
- }}
703
- >
704
- <View
1253
+ <Box style={{ position: 'relative', height: 70, backgroundColor: 'transparent', justifyContent: 'center' }}>
1254
+ <ScrollView
1255
+ horizontal
1256
+ showsHorizontalScrollIndicator={false}
705
1257
  style={{
706
- flex: 1,
707
1258
  flexDirection: 'row',
708
- alignItems: 'center',
709
1259
  paddingLeft: 15,
710
1260
  paddingRight: 5,
711
1261
  }}
1262
+ contentContainerStyle={{
1263
+ alignItems: 'center',
1264
+ height: '100%',
1265
+ }}
712
1266
  >
713
- <View
714
- style={{
715
- width: 56,
716
- height: 56,
717
- marginRight: 15,
718
- borderRadius: 4,
719
- backgroundColor: colors.gray[200],
720
- overflow: 'hidden',
721
- borderWidth: 1,
722
- borderColor: '#e0e0e0',
723
- }}
724
- >
725
- <Image
726
- key={selectedImage}
727
- alt={'selected image'}
728
- source={{ uri: selectedImage }}
1267
+ {images.map((img, index) => (
1268
+ <View
1269
+ key={`image-preview-${index}`}
729
1270
  style={{
730
- width: '100%',
731
- height: '100%',
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,
732
1281
  }}
733
- size={'md'}
734
- />
735
- {loading && (
736
- <View
1282
+ >
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
+ }}
737
1299
  style={{
738
1300
  position: 'absolute',
739
- top: 0,
740
- left: 0,
741
- right: 0,
742
- bottom: 0,
743
- backgroundColor: 'rgba(255, 255, 255, 0.7)',
744
- justifyContent: 'center',
1301
+ top: -1,
1302
+ right: -1,
1303
+ backgroundColor: 'rgba(0,0,0,0.6)',
1304
+ borderRadius: 12,
1305
+ width: 20,
1306
+ height: 20,
745
1307
  alignItems: 'center',
1308
+ justifyContent: 'center',
1309
+ zIndex: 9999,
746
1310
  }}
747
1311
  >
748
- <Spinner size="small" color={colors.blue[500]} />
749
- </View>
750
- )}
751
- </View>
1312
+ <Ionicons name="close" size={16} color="white" />
1313
+ </TouchableOpacity>
1314
+ </View>
1315
+ ))}
1316
+ </ScrollView>
1317
+ </Box>
1318
+ );
1319
+ }, [images]);
752
1320
 
753
- <View style={{ flex: 1 }}>
754
- <Text style={{ fontSize: 14, fontWeight: '400', color: colors.gray[800] }}>
755
- {images[0]?.fileName || 'image_' + new Date().getTime() + '.jpg'}
756
- </Text>
757
- <Text style={{ fontSize: 12, color: colors.gray[500], marginTop: 2 }}>
758
- {loading ? 'Preparing...' : 'Ready to send'}
759
- </Text>
760
- </View>
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
+ );
761
1377
 
762
- <TouchableHighlight
763
- underlayColor={'rgba(0,0,0,0.1)'}
764
- onPress={() => {
765
- setFiles([]);
766
- setImage('');
767
- setImages([]);
768
- }}
769
- style={{
770
- backgroundColor: colors.red[500],
771
- borderRadius: 24,
772
- width: 36,
773
- height: 36,
774
- alignItems: 'center',
775
- justifyContent: 'center',
776
- marginRight: 10,
777
- }}
778
- >
779
- <Ionicons name="close" size={20} color="white" />
780
- </TouchableHighlight>
781
- </View>
782
- </View>
783
- );
784
- }, [selectedImage, loading, images]);
785
1378
  const setImageViewerObject = useCallback((obj: any, v: boolean) => {
786
1379
  setImageObject(obj);
787
1380
  setImageViewer(v);
@@ -804,75 +1397,85 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
804
1397
  );
805
1398
  }, [imageObject]);
806
1399
 
807
- const renderMessage = useCallback(
1400
+ const renderMessage1 = useCallback(
808
1401
  (props: any) => {
809
1402
  const { currentMessage } = props;
810
1403
 
811
- // Check if this message is currently being uploaded
812
- const isUploading = currentMessage._id === uploadingMessageId && loading;
813
-
814
- if (isUploading) {
815
- return (
816
- <Box className="p-3 mb-2 mr-3 self-end rounded-2xl bg-gray-100 max-w-[80%]">
817
- {currentMessage.text && currentMessage.text.trim() !== '' && (
818
- <Box className="h-4 mb-2 rounded bg-gray-200 overflow-hidden">
819
- <Skeleton variant="rounded" className="flex-1" />
820
- </Box>
821
- )}
822
- {currentMessage.image && (
823
- <Box className="h-[150px] w-[150px] rounded-lg bg-gray-200 overflow-hidden mt-1">
824
- <Box className="flex-1 items-center justify-center">
825
- <Spinner size="small" color={colors.blue[500]} />
826
- </Box>
827
- <Skeleton variant="rounded" className="flex-1" />
828
- </Box>
829
- )}
830
- </Box>
831
- );
832
- }
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
+ };
833
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
834
1422
  return (
835
1423
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
836
1424
  );
837
1425
  },
838
- [uploadingMessageId, loading, isShowImageViewer, setImageViewerObject],
1426
+ [isShowImageViewer],
839
1427
  );
840
1428
 
841
- const renderInputToolbar = useCallback((props) => {
842
- return (
843
- <InputToolbar
844
- {...props}
845
- containerStyle={{
846
- backgroundColor: 'white',
847
- borderTopWidth: 1,
848
- borderTopColor: colors.gray[200],
849
- paddingHorizontal: 4,
850
- paddingVertical: 0,
851
- paddingTop: 2,
852
- marginBottom: 0,
853
- marginTop: 0,
854
- }}
855
- primaryStyle={{
856
- alignItems: 'center',
857
- }}
858
- />
859
- );
860
- }, []);
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
+ );
861
1462
 
862
1463
  const renderLoadEarlier = useCallback(() => {
863
- return loadingOldMessages ? (
1464
+ return loadingOldMessages && !refreshing ? (
864
1465
  <Box
865
1466
  style={{
866
1467
  padding: 10,
867
1468
  backgroundColor: 'rgba(255,255,255,0.8)',
868
1469
  borderRadius: 10,
869
1470
  marginTop: 10,
1471
+ alignItems: 'center',
870
1472
  }}
871
1473
  >
872
1474
  <Spinner size="small" color={colors.blue[500]} />
1475
+ <Text style={{ fontSize: 12, color: colors.gray[600], marginTop: 4 }}>Loading earlier messages...</Text>
873
1476
  </Box>
874
1477
  ) : null;
875
- }, [loadingOldMessages]);
1478
+ }, [loadingOldMessages, refreshing]);
876
1479
 
877
1480
  let onScroll = false;
878
1481
 
@@ -888,8 +1491,69 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
888
1491
  onScroll = false;
889
1492
  };
890
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
+
891
1553
  return (
892
- <>
1554
+ <View style={{ flex: 1, marginTop: -40 }}>
1555
+ {errorMessage ? <ErrorNotification message={errorMessage} onClose={() => setErrorMessage('')} /> : null}
1556
+ {loading && <Spinner color={'#3b82f6'} />}
893
1557
  {isPostParentIdThread && (
894
1558
  <>
895
1559
  {threadPost?.length > 0 && (
@@ -940,18 +1604,112 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
940
1604
  )}
941
1605
  </>
942
1606
  )}
943
- <GiftedChat
1607
+ <GiftedChatInboxComponent
944
1608
  ref={threadMessageListRef}
1609
+ onRemoveImage={handleRemoveImage}
1610
+ images={images}
1611
+ onSelectImages={onSelectImages}
1612
+ selectedImage={selectedImage}
1613
+ setSelectedImage={setSelectedImage}
1614
+ isUploadingImage={isUploadingImage}
1615
+ loading={loading}
945
1616
  wrapInSafeArea={true}
1617
+ inverted={true}
946
1618
  renderLoading={() => <Skeleton variant="rounded" style={{ flex: 1 }} />}
947
1619
  messages={messageList}
948
1620
  listViewProps={{
949
1621
  onEndReached: onEndReached,
950
1622
  onEndReachedThreshold: 0.5,
951
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
+ ),
952
1639
  }}
953
1640
  onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
954
- text={msg ? msg : ' '}
1641
+ text={msg || ' '}
1642
+ onInputTextChanged={(text) => setMsg(text)}
1643
+ renderFooter={() => null}
1644
+ scrollToBottom
1645
+ user={{
1646
+ _id: auth?.id || '',
1647
+ }}
1648
+ placeholder="Jot something down"
1649
+ infiniteScroll={true}
1650
+ renderSend={renderSend}
1651
+ renderMessageText={renderMessageText}
1652
+ renderMessage={renderMessage}
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]}
1708
+ />
1709
+ ),
1710
+ }}
1711
+ onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
1712
+ text={msg || ' '}
955
1713
  onInputTextChanged={(text) => setMsg(text)}
956
1714
  renderFooter={() => null}
957
1715
  scrollToBottom
@@ -964,80 +1722,33 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
964
1722
  renderSend={renderSend}
965
1723
  renderMessageText={renderMessageText}
966
1724
  renderInputToolbar={renderInputToolbar}
967
- minInputToolbarHeight={50}
968
- renderActions={renderActions}
969
- renderAccessory={!!selectedImage ? renderAccessory : undefined}
1725
+ renderComposer={renderComposer}
1726
+ minInputToolbarHeight={isActionSheetVisible ? 0 : 56}
1727
+ renderAccessory={images.length > 0 ? renderAccessory : null}
970
1728
  renderMessage={renderMessage}
971
1729
  renderLoadEarlier={renderLoadEarlier}
972
1730
  loadEarlier={totalCount > channelMessages.length}
973
1731
  isLoadingEarlier={loadingOldMessages}
974
- bottomOffset={Platform.OS === 'ios' ? 10 : 0}
1732
+ bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0}
975
1733
  textInputProps={{
976
- style: {
977
- borderWidth: 1,
978
- borderColor: colors.gray[300],
979
- backgroundColor: '#f8f8f8',
980
- borderRadius: 20,
981
- minHeight: 36,
982
- maxHeight: 80,
983
- color: '#000',
984
- padding: 8,
985
- paddingHorizontal: 15,
986
- fontSize: 16,
987
- flex: 1,
988
- marginVertical: 2,
989
- marginBottom: 0,
990
- },
991
1734
  multiline: true,
992
1735
  returnKeyType: 'default',
993
1736
  enablesReturnKeyAutomatically: true,
994
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),
995
1742
  }}
996
1743
  minComposerHeight={36}
997
1744
  maxComposerHeight={100}
998
- placeholder="Type a message..."
999
- renderChatFooter={() => (
1000
- <>
1001
- <ImageViewerModal
1002
- isVisible={isShowImageViewer}
1003
- setVisible={setImageViewer}
1004
- modalContent={modalContent}
1005
- />
1006
- <SubscriptionHandler
1007
- channelId={channelId}
1008
- subscribeToNewMessages={() =>
1009
- subscribeToMore({
1010
- document: CHAT_MESSAGE_ADDED,
1011
- variables: {
1012
- channelId: channelId?.toString(),
1013
- postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
1014
- },
1015
- updateQuery: (prev, { subscriptionData }: any) => {
1016
- if (!subscriptionData.data) return prev;
1017
- const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
1018
- const prevReplyCount: any = prev?.getPostThread?.replyCount;
1019
- const newReplyCount = prevReplyCount || 0 + 1;
1020
- const replies = prev?.getPostThread?.replies || [];
1021
- setChannelMessages((oldMessages: any) =>
1022
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
1023
- );
1024
- setTotalCount(newReplyCount);
1025
- return Object.assign({}, prev, {
1026
- getPostThread: {
1027
- ...prev?.getPostThread,
1028
- lastReplyAt: newMessage.createdAt,
1029
- replies: [newMessage, ...replies],
1030
- replyCount: newReplyCount,
1031
- updatedAt: newMessage.createdAt,
1032
- },
1033
- });
1034
- },
1035
- })
1036
- }
1037
- />
1038
- </>
1039
- )}
1040
- messagesContainerStyle={messageList?.length == 0 && { transform: [{ scaleY: -1 }] }}
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
+ }}
1041
1752
  renderChatEmpty={() => (
1042
1753
  <>
1043
1754
  {!threadLoading && messageList && messageList?.length == 0 && (
@@ -1055,8 +1766,36 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
1055
1766
  springConfig: { tension: 90000, friction: 90000 },
1056
1767
  disabled: true,
1057
1768
  }}
1058
- />
1059
- </>
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>
1060
1799
  );
1061
1800
  };
1062
1801