@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
@@ -12,26 +12,45 @@ import {
12
12
  Spinner,
13
13
  Text,
14
14
  Skeleton,
15
+ ScrollView,
16
+ Toast,
17
+ ToastTitle,
18
+ ToastDescription,
19
+ useToast,
20
+ ToastAlert,
21
+ VStack,
22
+ Divider,
23
+ Center,
15
24
  } from '@admin-layout/gluestack-ui-mobile';
16
- import { Platform, TouchableHighlight, SafeAreaView, View } from 'react-native';
17
- import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native';
25
+ import {
26
+ Platform,
27
+ TouchableHighlight,
28
+ SafeAreaView,
29
+ View,
30
+ TouchableOpacity,
31
+ Animated,
32
+ Text as RNText,
33
+ TextInput,
34
+ KeyboardAvoidingView,
35
+ } from 'react-native';
36
+ import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
18
37
  import { navigationRef } from '@common-stack/client-react';
19
- import { useSelector } from 'react-redux';
38
+ import { useSelector, shallowEqual } from 'react-redux';
20
39
  import { orderBy, startCase, uniqBy } from 'lodash-es';
21
40
  import * as ImagePicker from 'expo-image-picker';
22
41
  import { encode as atob } from 'base-64';
23
- import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
42
+ import { Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
24
43
  import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
25
44
  import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo, FileRefType, PostTypeEnum } from 'common';
26
45
  import {
27
- OnChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
28
- useMessagesQuery,
29
- useSendExpoNotificationOnPostMutation,
30
- useSendMessagesMutation,
31
- useViewChannelDetailQuery,
32
- useAddDirectChannelMutation,
33
- MessagesDocument,
34
- } from 'common/graphql';
46
+ CHAT_MESSAGE_ADDED,
47
+ useChannelDetailQuery,
48
+ useChannelMessagesQuery,
49
+ useSendChannelMessage,
50
+ useAddDirectChannel,
51
+ MESSAGES_DOCUMENT,
52
+ useSendExpoNotification,
53
+ } from '../../../queries/inboxQueries';
35
54
  import { useUploadFilesNative } from '@messenger-box/platform-client';
36
55
  import { objectId } from '@messenger-box/core';
37
56
  import { userSelector } from '@adminide-stack/user-auth0-client';
@@ -40,7 +59,14 @@ import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContai
40
59
  import CachedImage from '../components/CachedImage';
41
60
  import { config } from '../config';
42
61
  import colors from 'tailwindcss/colors';
43
- import { v4 as uuidv4 } from 'uuid';
62
+ import ExpandableInputActionSheet from '../components/ExpandableInputActionSheet';
63
+ import ExpandableInput from '../components/ExpandableInput';
64
+ import { SubscriptionHandler } from '../components/SubscriptionHandler';
65
+ import { useInboxMessages } from '../hooks/useInboxMessages';
66
+ import Reanimated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
67
+ import Constants from 'expo-constants';
68
+ import { Keyboard } from 'react-native';
69
+ import GiftedChatInboxComponent from '../components/GiftedChatInboxComponent';
44
70
 
45
71
  // Define an extended interface for ImagePickerAsset with url property
46
72
  interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
@@ -75,6 +101,7 @@ interface IMessageProps extends IMessage {
75
101
  propsConfiguration?: any;
76
102
  replies?: any;
77
103
  isShowThreadMessage?: boolean;
104
+ images?: string[]; // Add support for multiple images
78
105
  }
79
106
 
80
107
  export interface AlertMessageAttachmentsInterface {
@@ -93,11 +120,96 @@ type OptimisticPropsConfig = {
93
120
  resource: string;
94
121
  };
95
122
 
123
+ // Custom notification component
124
+ const ErrorNotification = ({ message, onClose, type = 'error' }) => {
125
+ const opacity = useRef(new Animated.Value(0)).current;
126
+
127
+ // Choose colors based on type
128
+ const bgColor = type === 'error' ? '#f44336' : '#ff9800';
129
+ const title = type === 'error' ? 'Error' : 'Warning';
130
+
131
+ useEffect(() => {
132
+ // Fade in
133
+ Animated.timing(opacity, {
134
+ toValue: 1,
135
+ duration: 300,
136
+ useNativeDriver: true,
137
+ }).start();
138
+
139
+ // Auto hide after 4 seconds
140
+ const timer = setTimeout(() => {
141
+ Animated.timing(opacity, {
142
+ toValue: 0,
143
+ duration: 300,
144
+ useNativeDriver: true,
145
+ }).start(() => onClose && onClose());
146
+ }, 4000);
147
+
148
+ return () => clearTimeout(timer);
149
+ }, []);
150
+
151
+ return (
152
+ <Animated.View
153
+ style={{
154
+ position: 'absolute',
155
+ top: 10,
156
+ left: 10,
157
+ right: 10,
158
+ backgroundColor: bgColor,
159
+ padding: 15,
160
+ borderRadius: 8,
161
+ shadowColor: '#000',
162
+ shadowOffset: { width: 0, height: 2 },
163
+ shadowOpacity: 0.25,
164
+ shadowRadius: 3.84,
165
+ elevation: 5,
166
+ zIndex: 1000,
167
+ opacity,
168
+ }}
169
+ >
170
+ <HStack className="items-center justify-between">
171
+ <Text style={{ color: 'white', fontWeight: 'bold' }}>{title}</Text>
172
+ <TouchableOpacity onPress={onClose}>
173
+ <Ionicons name="close" size={20} color="white" />
174
+ </TouchableOpacity>
175
+ </HStack>
176
+ <Text style={{ color: 'white', marginTop: 5 }}>{message}</Text>
177
+ </Animated.View>
178
+ );
179
+ };
180
+
181
+ const PADDING_BOTTOM = Platform.OS === 'ios' ? 20 : 0;
182
+
183
+ function useGradualKeyboardAnimation() {
184
+ const height = useSharedValue(PADDING_BOTTOM);
185
+ const isExpoGo = Constants.executionEnvironment === 'storeClient';
186
+ let useKeyboardHandler;
187
+ if (!isExpoGo) {
188
+ useKeyboardHandler = require('react-native-keyboard-controller').useKeyboardHandler;
189
+ }
190
+ if (useKeyboardHandler) {
191
+ useKeyboardHandler(
192
+ {
193
+ onMove: (e) => {
194
+ 'worklet';
195
+ height.value = Math.max(e.height, PADDING_BOTTOM);
196
+ },
197
+ onEnd: (e) => {
198
+ 'worklet';
199
+ height.value = e.height;
200
+ },
201
+ },
202
+ [],
203
+ );
204
+ }
205
+ return { height };
206
+ }
207
+
96
208
  const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowThreadMessage, ...rest }: any) => {
97
209
  // Core state management using React hooks instead of XState
210
+ const { params } = useRoute<any>();
98
211
  const [channelId, setChannelId] = useState<string | null>(initialChannelId || null);
99
212
  const [messageText, setMessageText] = useState('');
100
- const [skip, setSkip] = useState(0);
101
213
  const [loading, setLoading] = useState(false);
102
214
  const [loadingOldMessages, setLoadingOldMessages] = useState(false);
103
215
  const [error, setError] = useState<string | null>(null);
@@ -105,51 +217,60 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
105
217
  const [images, setImages] = useState<any[]>([]);
106
218
  const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
107
219
  const [imageObject, setImageObject] = useState<any>({});
220
+ const [errorMessage, setErrorMessage] = useState('');
221
+ const [notificationType, setNotificationType] = useState('error');
222
+
223
+ // Add state for expandable action sheet
224
+ const [isActionSheetVisible, setActionSheetVisible] = useState(false);
225
+ // Add state for controlling bottom margin
226
+ const [bottomMargin, setBottomMargin] = useState(0);
108
227
 
109
228
  // Create refs for various operations
110
229
  const messageRootListRef = useRef<any>(null);
230
+ const textInputRef = useRef<any>(null); // Add new ref for the text input
111
231
  const isMounted = useRef(true);
112
232
  const fetchOldDebounceRef = useRef(false);
113
233
 
114
234
  // Navigation and auth
115
- const auth: any = useSelector(userSelector);
235
+ const auth: any = useSelector(userSelector, shallowEqual);
116
236
  const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
117
237
  const navigation = useNavigation<any>();
118
238
  const isFocused = useIsFocused();
119
239
 
120
240
  // Apollo mutations
121
- const [addDirectChannel] = useAddDirectChannelMutation();
241
+ const [addDirectChannel] = useAddDirectChannel();
122
242
  const { startUpload } = useUploadFilesNative();
123
- const [sendMsg] = useSendMessagesMutation();
124
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
243
+ const [sendMsg] = useSendChannelMessage();
244
+ const [sendExpoNotification] = useSendExpoNotification();
245
+
246
+ // Add skip state for pagination
247
+ const [skip, setSkip] = useState(0);
125
248
 
126
249
  // Apollo query for messages
127
250
  const {
128
251
  data,
129
252
  loading: messageLoading,
253
+ error: inboxError,
130
254
  refetch,
131
- fetchMore: fetchMoreMessages,
132
- subscribeToMore,
133
- } = useMessagesQuery({
134
- variables: {
255
+ subscribe,
256
+ } = useInboxMessages({
257
+ useQueryHook: useChannelMessagesQuery,
258
+ queryVariables: {
135
259
  channelId: channelId?.toString(),
136
260
  parentId: null,
137
261
  limit: MESSAGES_PER_PAGE,
138
- skip: skip,
139
- },
140
- skip: !channelId,
141
- fetchPolicy: 'cache-and-network',
142
- nextFetchPolicy: 'cache-first',
143
- refetchWritePolicy: 'merge',
144
- notifyOnNetworkStatusChange: true,
145
- onError: (error) => {
146
- setError(String(error));
262
+ skip: skip, // Use skip state for pagination
263
+ orgName: params?.orgName,
147
264
  },
265
+ subscriptionDocument: CHAT_MESSAGE_ADDED,
266
+ subscriptionVariables: { channelId: channelId?.toString() },
267
+ updateQuery: undefined, // Provide custom updateQuery if needed
268
+ onError: (err) => setError(String(err)),
148
269
  });
149
270
 
150
271
  // Extract messages from the query data
151
272
  const channelMessages = useMemo(() => {
152
- return data?.messages?.data || [];
273
+ return (data?.messages?.data as any[]) || [];
153
274
  }, [data?.messages?.data]);
154
275
 
155
276
  // Get total message count
@@ -176,47 +297,33 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
176
297
  useFocusEffect(
177
298
  React.useCallback(() => {
178
299
  if (channelId) {
179
- // Refresh messages when screen comes into focus
180
300
  refetch();
181
301
  }
182
- return () => {
183
- // Nothing needed on unfocus
184
- };
185
- }, [channelId, isFocused, refetch]),
302
+ }, [isFocused, refetch]),
186
303
  );
187
304
 
188
- // Loading state for image selection
189
- useEffect(() => {
190
- if (selectedImage) {
191
- setLoading(false);
192
- }
193
- }, [selectedImage]);
194
-
195
- // Fetch more messages function
305
+ // When fetching more messages, update skip
196
306
  const fetchMoreMessagesImpl = useCallback(async () => {
197
307
  try {
198
308
  setLoadingOldMessages(true);
199
- const response = await fetchMoreMessages({
200
- variables: {
201
- channelId: channelId?.toString(),
202
- parentId: null,
203
- skip: channelMessages.length,
204
- },
205
- // Let type policy handle the merge
309
+ const response = await refetch({
310
+ channelId: channelId?.toString(),
311
+ parentId: null,
312
+ limit: MESSAGES_PER_PAGE,
313
+ skip: channelMessages.length,
206
314
  });
207
-
315
+ setSkip(channelMessages.length); // Update skip after fetching
208
316
  setLoadingOldMessages(false);
209
317
  if (!response?.data?.messages?.data) {
210
318
  return { error: 'No messages returned' };
211
319
  }
212
-
213
320
  return { messages: response.data.messages.data };
214
321
  } catch (error) {
215
322
  setLoadingOldMessages(false);
216
323
  setError(String(error));
217
324
  return { error: String(error) };
218
325
  }
219
- }, [channelId, channelMessages.length, fetchMoreMessages]);
326
+ }, [channelId, channelMessages.length, refetch]);
220
327
 
221
328
  // Send message function
222
329
  const sendMessageImpl = useCallback(async () => {
@@ -243,12 +350,13 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
243
350
  author: {
244
351
  __typename: 'UserAccount' as const,
245
352
  id: auth?.id,
246
- picture: auth?.picture || '',
247
- givenName: auth?.givenName || '',
248
- familyName: auth?.familyName || '',
249
- email: auth?.email || '',
250
- username: auth?.username || '',
251
- alias: [] as string[],
353
+ givenName: auth?.profile?.given_name || '',
354
+ familyName: auth?.profile?.family_name || '',
355
+ email: auth?.profile?.email || '',
356
+ username: auth?.profile?.nickname || '',
357
+ fullName: auth?.profile?.name || '',
358
+ picture: auth?.profile?.picture || '',
359
+ alias: [auth?.authUserId ?? ''] as string[],
252
360
  tokens: [],
253
361
  },
254
362
  isDelivered: true,
@@ -288,57 +396,69 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
288
396
  __typename: 'Mutation',
289
397
  sendMessage: optimisticMessage,
290
398
  },
399
+ // Let the type policies handle the cache update automatically
291
400
  update: (cache, { data }) => {
292
- if (data?.sendMessage) {
293
- try {
294
- // Read the existing messages from the cache
295
- const existingData = cache.readQuery<{
401
+ // Only perform cache update if we have valid data
402
+ if (!data?.sendMessage) return;
403
+
404
+ try {
405
+ // Let Apollo type policies handle merging by using writeQuery
406
+ // This will trigger the merge functions in the type policies
407
+ cache.writeQuery({
408
+ query: MESSAGES_DOCUMENT,
409
+ variables: {
410
+ channelId: channelId?.toString(),
411
+ parentId: null,
412
+ limit: MESSAGES_PER_PAGE,
413
+ skip: 0,
414
+ },
415
+ data: {
296
416
  messages: {
297
- __typename: string;
298
- messagesRefId?: string;
299
- data: any[];
300
- totalCount: number;
301
- };
302
- }>({
303
- query: MessagesDocument,
304
- variables: {
305
- channelId: channelId?.toString(),
306
- parentId: null,
307
- limit: MESSAGES_PER_PAGE,
308
- skip: 0,
309
- },
310
- });
311
-
312
- // If we don't have data yet in the cache, don't try to update
313
- if (!existingData) return;
314
-
315
- // Let the type policy handle the merging
316
- cache.writeQuery({
317
- query: MessagesDocument,
318
- variables: {
319
- channelId: channelId?.toString(),
320
- parentId: null,
321
- limit: MESSAGES_PER_PAGE,
322
- skip: 0,
417
+ __typename: 'Messages',
418
+ messagesRefId: channelId,
419
+ data: [data.sendMessage],
420
+ totalCount: 1, // Just send the count for this single message
323
421
  },
324
- data: {
325
- messages: {
326
- ...existingData.messages,
327
- data: [data.sendMessage, ...existingData.messages.data],
328
- totalCount: (existingData.messages.totalCount || 0) + 1,
329
- },
330
- },
331
- });
332
- } catch (error) {
333
- console.error('Error updating cache:', error);
422
+ },
423
+ });
424
+ } catch (error) {
425
+ console.error('Error updating cache:', error);
426
+
427
+ // Format error for notification
428
+ let errorMsg = 'Failed to update message cache';
429
+ if (__DEV__ && error) {
430
+ // In development, show actual error
431
+ errorMsg = error.message
432
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
433
+ : 'Cache update failed';
334
434
  }
435
+
436
+ setNotificationType('error');
437
+ setErrorMessage(errorMsg);
335
438
  }
336
439
  },
337
440
  });
338
441
 
442
+ // Ensure loader is removed after sending
443
+ setIsUploadingImage(false);
444
+ setLoading(false);
445
+
339
446
  return { message: response.data?.sendMessage };
340
447
  } catch (error) {
341
448
  setLoading(false);
449
+ setIsUploadingImage(false);
450
+
451
+ // Format error for notification
452
+ let errorMsg = 'Failed to send message';
453
+ if (__DEV__ && error) {
454
+ // In development, show actual error
455
+ errorMsg = error.message
456
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
457
+ : 'Message sending failed';
458
+ }
459
+
460
+ setNotificationType('error');
461
+ setErrorMessage(errorMsg);
342
462
  setError(String(error));
343
463
  return { error: String(error) };
344
464
  }
@@ -351,36 +471,53 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
351
471
  try {
352
472
  let imageSource = await ImagePicker.launchImageLibraryAsync({
353
473
  mediaTypes: ImagePicker.MediaTypeOptions.Images,
354
- allowsEditing: true,
474
+ allowsEditing: false,
355
475
  aspect: [4, 3],
356
476
  quality: 0.8,
357
477
  base64: true,
358
478
  exif: false,
479
+ allowsMultipleSelection: true, // Enable multiple selection
359
480
  });
360
481
 
361
482
  if (!imageSource?.canceled) {
362
- // Get the asset
363
- const selectedAsset = imageSource?.assets?.[0];
364
- if (!selectedAsset) {
483
+ // Get all selected assets
484
+ const selectedAssets = imageSource?.assets || [];
485
+ if (selectedAssets.length === 0) {
365
486
  setLoading(false);
366
487
  return;
367
488
  }
368
489
 
369
- // Create a base64 image string for preview
370
- const base64Data = selectedAsset.base64;
371
- const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
490
+ // Process all selected images
491
+ const newImages = selectedAssets.map((selectedAsset) => {
492
+ // Create a base64 image string for preview
493
+ const base64Data = selectedAsset.base64;
494
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
495
+
496
+ // Format the asset for upload service requirements
497
+ const asset: ExtendedImagePickerAsset = {
498
+ ...selectedAsset,
499
+ url: selectedAsset.uri,
500
+ fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
501
+ mimeType: 'image/jpeg',
502
+ };
503
+
504
+ return asset;
505
+ });
506
+
507
+ // Set preview for the first image (for backward compatibility)
508
+ if (newImages.length > 0) {
509
+ const base64Data = newImages[0].base64;
510
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : newImages[0].uri;
511
+ setSelectedImage(previewImage);
512
+ }
372
513
 
373
- // Format the asset for upload service requirements
374
- const asset: ExtendedImagePickerAsset = {
375
- ...selectedAsset,
376
- url: selectedAsset.uri,
377
- fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
378
- mimeType: 'image/jpeg',
379
- };
514
+ // Add new images to existing ones
515
+ setImages((currentImages) => [...currentImages, ...newImages]);
380
516
 
381
- // Update state with the new image
382
- setSelectedImage(previewImage);
383
- setImages([asset]);
517
+ // Show action sheet if it's not visible
518
+ if (!isActionSheetVisible) {
519
+ setActionSheetVisible(true);
520
+ }
384
521
  } else {
385
522
  setLoading(false);
386
523
  }
@@ -391,53 +528,107 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
391
528
 
392
529
  // Add a state variable to track which message should show the skeleton
393
530
  const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
531
+ // Add new state for tracking pending uploads
532
+ const [pendingUploads, setPendingUploads] = useState<Record<string, IMessageProps>>({});
533
+ const [uploadErrors, setUploadErrors] = useState<Record<string, string>>({});
534
+
535
+ // Add new state variable to track image upload status
536
+ const [isUploadingImage, setIsUploadingImage] = useState(false);
537
+
538
+ // Ensure loader is hidden when all images are removed
539
+ useEffect(() => {
540
+ if (images.length === 0) {
541
+ setIsUploadingImage(false);
542
+ }
543
+ }, [images]);
544
+
545
+ // Add toast hook for notifications
546
+ const toast = useToast();
547
+
548
+ // Add a helper function for removing messages from the UI when uploads fail
549
+ const removeMessageFromUI = useCallback((messageId: string) => {
550
+ // Remove from pending uploads
551
+ setPendingUploads((prev) => {
552
+ const newPending = { ...prev };
553
+ delete newPending[messageId];
554
+ return newPending;
555
+ });
394
556
 
395
- // Send message with file function - update to set and clear uploadingMessageId
557
+ // Also remove any error state
558
+ setUploadErrors((prev) => {
559
+ const newErrors = { ...prev };
560
+ delete newErrors[messageId];
561
+ return newErrors;
562
+ });
563
+
564
+ // Reset upload state to ensure we don't get stuck with loading indicator
565
+ setIsUploadingImage(false);
566
+ }, []);
567
+
568
+ // Send message with file - fix to ensure images display without loading indicators
396
569
  const sendMessageWithFileImpl = useCallback(async () => {
397
570
  try {
398
- // For file uploads, we still need loading state since we need to wait for the file upload
399
- setLoading(true);
400
-
401
- // Generate a unique post ID for the message
571
+ // Generate a unique ID for the message
402
572
  const postId = objectId();
403
573
 
404
- // Set the message ID that should show the skeleton
405
- setUploadingMessageId(postId);
574
+ // Set uploading state to true
575
+ setIsUploadingImage(true);
406
576
 
407
- // Prepare notification data
408
- const notificationData: IExpoNotificationData = {
409
- url: config.INBOX_MESSEGE_PATH,
410
- params: { channelId, hideTabBar: true },
411
- screen: 'DialogMessages',
412
- other: { sound: Platform.OS === 'android' ? undefined : 'default' },
413
- };
577
+ // Clear all loading states immediately
578
+ setLoading(false);
579
+ setUploadingMessageId(null);
414
580
 
415
581
  // Safety check for images
416
582
  if (!images || images.length === 0) {
583
+ setIsUploadingImage(false);
417
584
  setLoading(false);
418
- setUploadingMessageId(null);
419
585
  return { error: 'No images available to upload' };
420
586
  }
421
587
 
422
- // Format the images for upload if needed
423
- const imagesToUpload = images.map((img) => {
424
- // Ensure the image has all required properties
425
- return {
426
- ...img,
427
- uri: img.uri || img.url, // Use either uri or url
428
- type: 'image/jpeg',
429
- name: img.fileName || `image_${Date.now()}.jpg`,
430
- };
431
- });
432
-
433
- // Store current message text and clear inputs immediately for better UX
588
+ // Store current values before clearing
434
589
  const currentMessageText = messageText;
590
+ const currentImages = [...images];
591
+
592
+ // Prepare image URIs for optimistic UI update
593
+ const imageUris = currentImages.map((img) => img.uri || img.url);
594
+
595
+ // Clear UI immediately for next message
435
596
  setMessageText('');
597
+ setSelectedImage('');
598
+ setImages([]);
599
+
600
+ // Create a client message with all local image URIs
601
+ const clientMessage: IMessageProps = {
602
+ _id: postId,
603
+ text: currentMessageText || ' ',
604
+ createdAt: new Date(),
605
+ user: {
606
+ _id: auth?.id || '',
607
+ name: `${auth?.givenName || ''} ${auth?.familyName || ''}`,
608
+ avatar: auth?.picture || '',
609
+ },
610
+ image: imageUris[0], // First image for compatibility with GiftedChat
611
+ images: imageUris, // All images for our custom renderer
612
+ sent: true,
613
+ received: true,
614
+ pending: false,
615
+ type: 'TEXT',
616
+ replies: { data: [], totalCount: 0 },
617
+ isShowThreadMessage: false,
618
+ };
436
619
 
437
- // Create a unique file ID for the optimistic response
438
- const fileId = objectId();
620
+ // Add to displayed messages immediately
621
+ setPendingUploads((prev) => ({ ...prev, [postId]: clientMessage }));
439
622
 
440
- // Create minimal optimistic message with file
623
+ // Prepare notification data
624
+ const notificationData: IExpoNotificationData = {
625
+ url: config.INBOX_MESSEGE_PATH,
626
+ params: { channelId, hideTabBar: true },
627
+ screen: 'DialogMessages',
628
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
629
+ };
630
+
631
+ // Create optimistic message with minimal structure required for UI rendering
441
632
  const optimisticMessage = {
442
633
  __typename: 'Post' as const,
443
634
  id: postId,
@@ -447,12 +638,13 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
447
638
  author: {
448
639
  __typename: 'UserAccount' as const,
449
640
  id: auth?.id,
450
- picture: auth?.picture || '',
451
- givenName: auth?.givenName || '',
452
- familyName: auth?.familyName || '',
453
- email: auth?.email || '',
454
- username: auth?.username || '',
455
- alias: [] as string[],
641
+ givenName: auth?.profile?.given_name || '',
642
+ familyName: auth?.profile?.family_name || '',
643
+ email: auth?.profile?.email || '',
644
+ username: auth?.profile?.nickname || '',
645
+ fullName: auth?.profile?.name || '',
646
+ picture: auth?.profile?.picture || '',
647
+ alias: [auth?.authUserId ?? ''] as string[],
456
648
  tokens: [],
457
649
  },
458
650
  isDelivered: true,
@@ -472,22 +664,21 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
472
664
  props: {},
473
665
  files: {
474
666
  __typename: 'FilesInfo' as const,
475
- data: [
476
- {
477
- __typename: 'FileInfo' as const,
478
- id: fileId,
479
- url: selectedImage,
480
- name: imagesToUpload[0]?.name || 'image.jpg',
481
- extension: 'jpg',
482
- mimeType: 'image/jpeg',
483
- height: imagesToUpload[0]?.height || 0,
484
- width: imagesToUpload[0]?.width || 0,
485
- size: imagesToUpload[0]?.fileSize || 0,
486
- refType: 'Post' as FileRefType,
487
- ref: postId,
488
- },
489
- ],
490
- totalCount: 1,
667
+ data: imageUris.map((uri, index) => ({
668
+ __typename: 'FileInfo' as const,
669
+ id: `temp-file-${index}-${postId}`,
670
+ url: uri,
671
+ name: `image-${index}.jpg`,
672
+ extension: 'jpg',
673
+ mimeType: 'image/jpeg',
674
+ size: 0,
675
+ height: 300,
676
+ width: 300,
677
+ channel: null,
678
+ post: null,
679
+ refType: FileRefType.Post,
680
+ })),
681
+ totalCount: imageUris.length,
491
682
  },
492
683
  replies: {
493
684
  __typename: 'Messages' as const,
@@ -496,146 +687,178 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
496
687
  },
497
688
  };
498
689
 
499
- // Upload the files
500
- const uploadResponse = await startUpload({
501
- file: imagesToUpload,
502
- saveUploadedFile: {
503
- variables: { postId },
504
- },
505
- createUploadLink: {
506
- variables: { postId },
507
- },
508
- });
509
-
510
- if (uploadResponse?.error) {
511
- setLoading(false);
512
- setUploadingMessageId(null);
513
- return { error: String(uploadResponse.error) };
514
- }
690
+ // Start background processing without affecting UI
691
+ setTimeout(async () => {
692
+ try {
693
+ // Format images for upload
694
+ const imagesToUpload = currentImages.map((img) => ({
695
+ ...img,
696
+ uri: img.uri || img.url,
697
+ type: img.mimeType || 'image/jpeg',
698
+ name: img.fileName || `image_${Date.now()}.jpg`,
699
+ }));
700
+
701
+ // Upload the files in background - pass the array of images
702
+ const uploadResponse = await startUpload({
703
+ file: imagesToUpload,
704
+ saveUploadedFile: {
705
+ variables: { postId },
706
+ },
707
+ createUploadLink: {
708
+ variables: { postId },
709
+ },
710
+ });
711
+
712
+ // If upload fails, show error notification
713
+ if (uploadResponse?.error) {
714
+ console.error('Upload error:', uploadResponse.error);
715
+
716
+ // Format error message
717
+ let errorMsg = 'Failed to upload image. Please try again.';
718
+ if (__DEV__ && uploadResponse.error) {
719
+ // In development, show actual error
720
+ errorMsg =
721
+ typeof uploadResponse.error === 'string'
722
+ ? uploadResponse.error
723
+ : uploadResponse.error.message || errorMsg;
724
+ }
515
725
 
516
- // Get uploaded file IDs
517
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
518
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
726
+ // Show error notification
727
+ setNotificationType('error');
728
+ setErrorMessage(errorMsg);
519
729
 
520
- // Create a real version of the message with the actual file data
521
- const realMessage = {
522
- ...optimisticMessage,
523
- files: {
524
- __typename: 'FilesInfo' as const,
525
- data: uploadedFiles.map((file) => ({
526
- __typename: 'FileInfo' as const,
527
- id: file.id,
528
- url: file.url,
529
- name: file.name,
530
- extension: file.extension,
531
- mimeType: file.mimeType,
532
- height: file.height,
533
- width: file.width,
534
- size: file.size,
535
- refType: file.refType,
536
- ref: postId,
537
- })),
538
- totalCount: uploadedFiles.length,
539
- },
540
- };
730
+ // Store error in state
731
+ setUploadErrors((prev) => ({ ...prev, [postId]: errorMsg }));
541
732
 
542
- // Send the message with the uploaded files
543
- const response = await sendMsg({
544
- variables: {
545
- postId,
546
- channelId,
547
- content: currentMessageText || ' ', // Use a space if no text
548
- files,
549
- notificationParams: notificationData,
550
- },
551
- optimisticResponse: {
552
- __typename: 'Mutation',
553
- sendMessage: realMessage, // Use the message with real file data
554
- },
555
- update: (cache, { data }) => {
556
- if (data?.sendMessage) {
557
- try {
558
- // Read the existing messages from the cache
559
- const existingData = cache.readQuery<{
560
- messages: {
561
- __typename: string;
562
- messagesRefId?: string;
563
- data: any[];
564
- totalCount: number;
565
- };
566
- }>({
567
- query: MessagesDocument,
568
- variables: {
569
- channelId: channelId?.toString(),
570
- parentId: null,
571
- limit: MESSAGES_PER_PAGE,
572
- skip: 0,
573
- },
574
- });
575
-
576
- // If we don't have data yet in the cache, don't try to update
577
- if (!existingData) return;
578
-
579
- // Ensure the message has files data
580
- const messageWithFiles = {
581
- ...data.sendMessage,
582
- files: data.sendMessage.files || {
583
- __typename: 'FilesInfo',
584
- data: uploadedFiles.map((file) => ({
585
- __typename: 'FileInfo',
586
- id: file.id,
587
- url: file.url,
588
- name: file.name,
589
- extension: file.extension,
590
- mimeType: file.mimeType,
591
- height: file.height,
592
- width: file.width,
593
- size: file.size,
594
- refType: file.refType,
595
- ref: postId,
596
- })),
597
- totalCount: uploadedFiles.length,
598
- },
599
- };
600
-
601
- // Let the type policy handle the merging
602
- cache.writeQuery({
603
- query: MessagesDocument,
604
- variables: {
605
- channelId: channelId?.toString(),
606
- parentId: null,
607
- limit: MESSAGES_PER_PAGE,
608
- skip: 0,
609
- },
610
- data: {
611
- messages: {
612
- ...existingData.messages,
613
- data: [messageWithFiles, ...existingData.messages.data],
614
- totalCount: (existingData.messages.totalCount || 0) + 1,
615
- },
616
- },
617
- });
733
+ // Remove the message from UI
734
+ removeMessageFromUI(postId);
735
+ setIsUploadingImage(false);
736
+ setLoading(false);
737
+ return;
738
+ }
618
739
 
619
- // Clear the images after successful send
620
- setSelectedImage('');
621
- setImages([]);
622
- } catch (error) {
623
- console.error('Error updating cache:', error);
624
- }
740
+ // Get uploaded file info
741
+ const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
742
+ const fileIds = uploadedFiles?.map((f: any) => f.id) ?? null;
743
+
744
+ // Send the message with uploaded files
745
+ if (fileIds?.length > 0) {
746
+ await sendMsg({
747
+ variables: {
748
+ postId,
749
+ channelId,
750
+ content: currentMessageText || ' ',
751
+ files: fileIds,
752
+ notificationParams: notificationData,
753
+ },
754
+ optimisticResponse: {
755
+ __typename: 'Mutation',
756
+ sendMessage: optimisticMessage,
757
+ },
758
+ update: (cache, { data }) => {
759
+ if (!data?.sendMessage) {
760
+ setIsUploadingImage(false);
761
+ setLoading(false);
762
+ return;
763
+ }
764
+ try {
765
+ // Let Apollo type policies handle the cache update
766
+ cache.writeQuery({
767
+ query: MESSAGES_DOCUMENT,
768
+ variables: {
769
+ channelId: channelId?.toString(),
770
+ parentId: null,
771
+ limit: MESSAGES_PER_PAGE,
772
+ skip: 0,
773
+ },
774
+ data: {
775
+ messages: {
776
+ __typename: 'Messages',
777
+ messagesRefId: channelId,
778
+ data: [data.sendMessage],
779
+ totalCount: 1, // Just one message
780
+ },
781
+ },
782
+ });
783
+
784
+ // Check if the server response has the actual image
785
+ const serverMessage = data.sendMessage;
786
+ const hasServerImage = serverMessage?.files?.data?.some((file) => file.url);
787
+
788
+ if (hasServerImage) {
789
+ // Now that server has the image, we can remove client version
790
+ removeMessageFromUI(postId);
791
+ }
792
+
793
+ setIsUploadingImage(false);
794
+ setLoading(false);
795
+ } catch (error) {
796
+ console.error('Cache update error:', error);
797
+
798
+ // Format error for notification
799
+ let errorMsg = 'Failed to update message.';
800
+ if (__DEV__ && error) {
801
+ // In development, show actual error
802
+ errorMsg = error.message
803
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
804
+ : 'Cache update failed';
805
+ }
806
+
807
+ setNotificationType('error');
808
+ setErrorMessage(errorMsg);
809
+ setIsUploadingImage(false);
810
+ setLoading(false);
811
+ }
812
+ },
813
+ });
814
+ } else {
815
+ setIsUploadingImage(false);
816
+ setLoading(false);
817
+ }
818
+ } catch (error) {
819
+ console.error('Background process error:', error);
820
+
821
+ // Format error for notification
822
+ let errorMsg = 'Failed to send image. Please try again.';
823
+ if (__DEV__ && error) {
824
+ // In development, show actual error
825
+ errorMsg = error.message
826
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
827
+ : 'Background process failed';
625
828
  }
626
- },
627
- });
628
829
 
629
- setLoading(false);
630
- setUploadingMessageId(null);
631
- return { message: response.data?.sendMessage };
830
+ // Show error notification
831
+ setNotificationType('error');
832
+ setErrorMessage(errorMsg);
833
+ removeMessageFromUI(postId);
834
+ setIsUploadingImage(false);
835
+ setLoading(false);
836
+ }
837
+ }, 0);
838
+
839
+ // Return success immediately - UI already updated
840
+ return { success: true };
632
841
  } catch (error) {
633
- setLoading(false);
634
- setUploadingMessageId(null);
842
+ console.error('Send message error:', error);
843
+
844
+ // Format error for notification
845
+ let errorMsg = 'Failed to process image. Please try again.';
846
+ if (__DEV__ && error) {
847
+ // In development, show actual error
848
+ errorMsg = error.message
849
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
850
+ : 'Image processing failed';
851
+ }
852
+
853
+ // Show error notification
854
+ setNotificationType('error');
855
+ setErrorMessage(errorMsg);
635
856
  setError(String(error));
857
+ setIsUploadingImage(false);
858
+ setLoading(false);
636
859
  return { error: String(error) };
637
860
  }
638
- }, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth]);
861
+ }, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth, removeMessageFromUI]);
639
862
 
640
863
  // Create direct channel implementation
641
864
  const createDirectChannelImpl = useCallback(async () => {
@@ -647,6 +870,8 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
647
870
  !rest?.newChannelData?.userIds?.length
648
871
  ) {
649
872
  setLoading(false);
873
+ setNotificationType('error');
874
+ setErrorMessage(__DEV__ ? 'Invalid channel data' : 'Unable to create conversation');
650
875
  return { error: 'Invalid channel data' };
651
876
  }
652
877
 
@@ -664,6 +889,8 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
664
889
 
665
890
  if (!response?.data?.createDirectChannel?.id) {
666
891
  setLoading(false);
892
+ setNotificationType('error');
893
+ setErrorMessage(__DEV__ ? 'Failed to create channel' : 'Unable to create conversation');
667
894
  return { error: 'Failed to create channel' };
668
895
  }
669
896
 
@@ -696,7 +923,7 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
696
923
  email: auth?.email || '',
697
924
  username: auth?.username || '',
698
925
  alias: [] as string[],
699
- tokens: [],
926
+ tokens: auth?.token ? [...auth?.token] : [],
700
927
  },
701
928
  isDelivered: true,
702
929
  isRead: false,
@@ -737,30 +964,42 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
737
964
  sendMessage: optimisticMessage,
738
965
  },
739
966
  update: (cache, { data }) => {
740
- if (data?.sendMessage) {
741
- try {
742
- // For a new channel, we don't need to read existing data
743
- // Just write the new message to the cache
744
- cache.writeQuery({
745
- query: MessagesDocument,
746
- variables: {
747
- channelId: newChannelId,
748
- parentId: null,
749
- limit: MESSAGES_PER_PAGE,
750
- skip: 0,
751
- },
752
- data: {
753
- messages: {
754
- __typename: 'Messages',
755
- messagesRefId: newChannelId,
756
- data: [data.sendMessage],
757
- totalCount: 1,
758
- },
967
+ if (!data?.sendMessage) return;
968
+
969
+ try {
970
+ // For a new channel, simply write the initial message to the cache
971
+ // The type policies will handle it properly
972
+ cache.writeQuery({
973
+ query: MESSAGES_DOCUMENT,
974
+ variables: {
975
+ channelId: newChannelId,
976
+ parentId: null,
977
+ limit: MESSAGES_PER_PAGE,
978
+ skip: 0,
979
+ },
980
+ data: {
981
+ messages: {
982
+ __typename: 'Messages',
983
+ messagesRefId: newChannelId,
984
+ data: [data.sendMessage],
985
+ totalCount: 1,
759
986
  },
760
- });
761
- } catch (error) {
762
- console.error('Error updating cache:', error);
987
+ },
988
+ });
989
+ } catch (error) {
990
+ console.error('Error updating cache:', error);
991
+
992
+ // Format error for notification
993
+ let errorMsg = 'Failed to update message cache';
994
+ if (__DEV__ && error) {
995
+ // In development, show actual error
996
+ errorMsg = error.message
997
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
998
+ : 'Cache update failed';
763
999
  }
1000
+
1001
+ setNotificationType('error');
1002
+ setErrorMessage(errorMsg);
764
1003
  }
765
1004
  },
766
1005
  });
@@ -769,6 +1008,18 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
769
1008
  return { channelId: newChannelId };
770
1009
  } catch (error) {
771
1010
  setLoading(false);
1011
+
1012
+ // Format error for notification
1013
+ let errorMsg = 'Failed to create conversation';
1014
+ if (__DEV__ && error) {
1015
+ // In development, show actual error
1016
+ errorMsg = error.message
1017
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
1018
+ : 'Channel creation failed';
1019
+ }
1020
+
1021
+ setNotificationType('error');
1022
+ setErrorMessage(errorMsg);
772
1023
  setError(String(error));
773
1024
  return { error: String(error) };
774
1025
  }
@@ -799,72 +1050,93 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
799
1050
  return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
800
1051
  };
801
1052
 
802
- // Transform the message data for GiftedChat
1053
+ // Modify the messageList function to ensure local images take precedence
803
1054
  const messageList = useMemo(() => {
804
- // Short-circuit if no messages to process
1055
+ // Get pending upload messages as array
1056
+ const pendingMessages = Object.values(pendingUploads);
1057
+
1058
+ // If we have no server messages, just return pending messages
805
1059
  if (!channelMessages || channelMessages.length === 0) {
806
- return [];
1060
+ return pendingMessages;
807
1061
  }
808
1062
 
809
- // Use a more efficient approach - pre-filter messages once
1063
+ // Filter unique messages
810
1064
  const filteredMessages = uniqBy(channelMessages, ({ id }) => id);
811
1065
 
812
- // Skip processing if no filtered messages
813
- if (filteredMessages.length === 0) {
814
- return [];
815
- }
1066
+ // Process server messages - skip any that have client versions
1067
+ const serverMessages = orderBy(filteredMessages, ['createdAt'], ['desc'])
1068
+ .map((msg) => {
1069
+ const date = new Date(msg.createdAt);
1070
+
1071
+ // Skip messages that are in pendingUploads - client version takes precedence
1072
+ if (pendingUploads[msg.id]) {
1073
+ return null;
1074
+ }
1075
+
1076
+ // Extract image URLs from files data
1077
+ let imageUrls: string[] = [];
1078
+ let primaryImageUrl = null;
816
1079
 
817
- // Transform messages only once and return
818
- return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
819
- const date = new Date(msg.createdAt);
820
-
821
- // Extract image URL from files data
822
- let imageUrl = null;
823
- if (msg.files && typeof msg.files === 'object') {
824
- // Handle both cases where files might be directly present or via files.data
825
- const filesData = msg.files.data || (Array.isArray(msg.files) ? msg.files : null);
826
-
827
- if (filesData && filesData.length > 0) {
828
- const fileData = filesData[0];
829
- // Make sure we have valid file data with a URL
830
- if (fileData && typeof fileData === 'object' && fileData.url) {
831
- imageUrl = fileData.url;
1080
+ if (msg.files && typeof msg.files === 'object') {
1081
+ const filesData = msg.files.data || (Array.isArray(msg.files) ? msg.files : null);
1082
+
1083
+ if (filesData && filesData.length > 0) {
1084
+ // Collect all image URLs
1085
+ imageUrls = filesData
1086
+ .filter((fileData) => fileData && typeof fileData === 'object' && fileData.url)
1087
+ .map((fileData) => fileData.url);
1088
+
1089
+ // Set primary image for GiftedChat compatibility
1090
+ if (imageUrls.length > 0) {
1091
+ primaryImageUrl = imageUrls[0];
1092
+ }
832
1093
  }
833
1094
  }
834
- }
835
1095
 
836
- // Create message in a more direct way
837
- return {
838
- _id: msg.id,
839
- text: msg.message,
840
- createdAt: date,
841
- user: {
842
- _id: msg.author?.id || '',
843
- name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
844
- avatar: msg.author?.picture || '',
845
- },
846
- image: imageUrl,
847
- sent: msg?.isDelivered,
848
- received: msg?.isRead,
849
- type: msg?.type,
850
- propsConfiguration: msg?.propsConfiguration,
851
- replies: msg?.replies ?? [],
852
- isShowThreadMessage,
853
- };
854
- });
855
- }, [channelMessages, isShowThreadMessage]);
1096
+ // Create formatted message
1097
+ return {
1098
+ _id: msg.id,
1099
+ text: msg.message,
1100
+ createdAt: date,
1101
+ user: {
1102
+ _id: msg.author?.id || '',
1103
+ name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
1104
+ avatar: msg.author?.picture || '',
1105
+ },
1106
+ image: primaryImageUrl,
1107
+ images: imageUrls, // Store all images for custom rendering
1108
+ sent: msg?.isDelivered,
1109
+ received: msg?.isRead,
1110
+ type: msg?.type,
1111
+ propsConfiguration: msg?.propsConfiguration,
1112
+ replies: msg?.replies ?? [],
1113
+ isShowThreadMessage,
1114
+ };
1115
+ })
1116
+ .filter(Boolean); // Remove null entries
1117
+
1118
+ // Pending messages take precedence (they have local images)
1119
+ return [...pendingMessages, ...serverMessages];
1120
+ }, [channelMessages, pendingUploads, isShowThreadMessage]);
856
1121
 
857
1122
  // Render the send button
858
1123
  const renderSend = useCallback(
859
1124
  (props) => {
1125
+ // If action sheet is visible, don't show the default send button
1126
+ // if (isActionSheetVisible) {
1127
+ // return null;
1128
+ // }
1129
+
860
1130
  // Enable the send button if there's text OR we have images
861
1131
  const hasContent = !!props.text || images?.length > 0;
862
1132
  const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
1133
+ // const isDisabled = !canSend || isUploadingImage || loading;
1134
+ const isDisabled = !canSend;
863
1135
 
864
1136
  return (
865
1137
  <Send
866
1138
  {...props}
867
- disabled={!canSend}
1139
+ //disabled={isDisabled}
868
1140
  containerStyle={{
869
1141
  justifyContent: 'center',
870
1142
  alignItems: 'center',
@@ -879,15 +1151,107 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
879
1151
  <MaterialCommunityIcons
880
1152
  name="send-circle"
881
1153
  size={32}
882
- color={canSend ? colors.blue[500] : colors.gray[400]}
1154
+ color={isDisabled ? colors.gray[400] : colors.blue[500]}
883
1155
  />
884
1156
  </View>
885
1157
  </Send>
886
1158
  );
887
1159
  },
888
- [channelId, images, rest?.isCreateNewChannel],
1160
+ [channelId, images, rest?.isCreateNewChannel, isUploadingImage, loading, isActionSheetVisible],
1161
+ );
1162
+
1163
+ // Add new handler to open the action sheet
1164
+ const openExpandableInput = useCallback(() => {
1165
+ console.log('Opening action sheet');
1166
+ setActionSheetVisible(true);
1167
+ }, []);
1168
+
1169
+ // Add a debug useEffect to log when visibility changes
1170
+ useEffect(() => {
1171
+ console.log('Action sheet visibility:', isActionSheetVisible);
1172
+ // Set appropriate bottom margin when action sheet visibility changes
1173
+ if (isActionSheetVisible) {
1174
+ setBottomMargin(0);
1175
+ }
1176
+ }, [isActionSheetVisible]);
1177
+
1178
+ // Handle removing image from action sheet
1179
+ const handleRemoveImage = useCallback(
1180
+ (index: number) => {
1181
+ const newImages = [...images];
1182
+ newImages.splice(index, 1);
1183
+ setImages(newImages);
1184
+ if (newImages.length === 0) {
1185
+ setSelectedImage('');
1186
+ if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
1187
+ textInputRef.current.focus();
1188
+ }
1189
+ }
1190
+ },
1191
+ [images],
889
1192
  );
890
1193
 
1194
+ // Add a new state to track when the action sheet updates text
1195
+ const [textUpdatedInActionSheet, setTextUpdatedInActionSheet] = useState(false);
1196
+
1197
+ // Handle when the action sheet is closed
1198
+ const handleActionSheetClose = useCallback(() => {
1199
+ // Mark that we closed the sheet with potential text update
1200
+ setTextUpdatedInActionSheet(true);
1201
+ setActionSheetVisible(false);
1202
+ // Reset bottom margin to 0 when closing the expandable input
1203
+ setBottomMargin(0);
1204
+ }, []);
1205
+
1206
+ // Handle sending from action sheet
1207
+ const handleActionSheetSend = () => {
1208
+ if (messageText.trim() || images.length > 0) {
1209
+ // Set uploading state to show spinner
1210
+ setIsUploadingImage(true);
1211
+
1212
+ // Create a message object in the format GiftedChat expects
1213
+ const messages = [
1214
+ {
1215
+ text: messageText,
1216
+ user: {
1217
+ _id: auth?.id || '',
1218
+ },
1219
+ createdAt: new Date(),
1220
+ },
1221
+ ];
1222
+
1223
+ // Use the existing handleSend function
1224
+ handleSend(messages);
1225
+
1226
+ // Close the action sheet
1227
+ setActionSheetVisible(false);
1228
+ }
1229
+ };
1230
+
1231
+ // Update this useEffect to more reliably handle text syncing when action sheet closes
1232
+ useEffect(() => {
1233
+ // If action sheet just closed, ensure main input gets updated text
1234
+ if (!isActionSheetVisible && textUpdatedInActionSheet) {
1235
+ console.log('Action sheet closed with text:', messageText);
1236
+ // Reset the flag
1237
+ setTextUpdatedInActionSheet(false);
1238
+
1239
+ // Force GiftedChat to recognize the text change by creating a new state update
1240
+ const currentText = messageText;
1241
+ setMessageText('');
1242
+ setTimeout(() => {
1243
+ setMessageText(currentText);
1244
+ }, 50);
1245
+ }
1246
+ }, [isActionSheetVisible, textUpdatedInActionSheet]);
1247
+
1248
+ // Take a screenshot of the action sheet for debugging
1249
+ useEffect(() => {
1250
+ if (isActionSheetVisible && Platform.OS === 'ios') {
1251
+ console.log('Action sheet is visible, should show the input and options');
1252
+ }
1253
+ }, [isActionSheetVisible]);
1254
+
891
1255
  // Handle send for messages
892
1256
  const handleSend = useCallback(
893
1257
  async (messages) => {
@@ -910,20 +1274,36 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
910
1274
  // Update the message text state - now handled in send functions for better UX
911
1275
  setMessageText(newMessageText);
912
1276
 
1277
+ // Set uploading state to show spinner
1278
+ // setIsUploadingImage(true);
1279
+ // setLoading(true);
1280
+
913
1281
  // Handle direct channel creation if needed
914
1282
  if (rest?.isCreateNewChannel && !channelId) {
915
1283
  if (rest?.newChannelData?.type === RoomType?.Direct) {
916
- createDirectChannelImpl();
1284
+ await createDirectChannelImpl();
917
1285
  }
1286
+ setIsUploadingImage(false);
1287
+ setLoading(false);
918
1288
  return;
919
1289
  }
920
1290
 
921
1291
  // Send message with or without image based on state
922
1292
  if (hasImages) {
923
- sendMessageWithFileImpl();
1293
+ await sendMessageWithFileImpl();
924
1294
  } else {
925
- sendMessageImpl();
1295
+ await sendMessageImpl();
926
1296
  }
1297
+
1298
+ setIsUploadingImage(false);
1299
+ setLoading(false);
1300
+
1301
+ // Focus the input field after sending
1302
+ setTimeout(() => {
1303
+ if (textInputRef.current) {
1304
+ textInputRef.current.focus();
1305
+ }
1306
+ }, 100);
927
1307
  },
928
1308
  [
929
1309
  channelId,
@@ -943,6 +1323,11 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
943
1323
  const lastReply: any =
944
1324
  currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
945
1325
 
1326
+ // Do not render anything if the message text is empty or only whitespace
1327
+ if (!currentMessage?.text || currentMessage.text.trim() === '') {
1328
+ return null;
1329
+ }
1330
+
946
1331
  if (currentMessage.type === 'ALERT') {
947
1332
  const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
948
1333
  let action: string = '';
@@ -980,7 +1365,7 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
980
1365
  <Button
981
1366
  variant={'outline'}
982
1367
  size={'sm'}
983
- className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
1368
+ className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}] mt-2 rounded-full `}
984
1369
  onPress={() => action && params && navigation.navigate(action, params)}
985
1370
  >
986
1371
  <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
@@ -1126,28 +1511,42 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
1126
1511
  return (
1127
1512
  <Actions
1128
1513
  {...props}
1129
- options={{
1130
- ['Choose from Library']: onSelectImages,
1131
- ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1132
- }}
1514
+ // options={{
1515
+ // ['Choose from Library']: onSelectImages,
1516
+ // ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1517
+ // }}
1133
1518
  optionTintColor="#000000"
1134
1519
  cancelButtonIndex={1} // Set the Cancel option as the cancel button
1135
1520
  icon={() => (
1136
- <Box
1521
+ <TouchableOpacity
1522
+ onPress={onSelectImages}
1137
1523
  style={{
1138
- width: 32,
1139
- height: 32,
1524
+ width: 25,
1525
+ height: 25,
1526
+ borderRadius: 20,
1527
+ backgroundColor: '#f5f5f5',
1140
1528
  alignItems: 'center',
1141
1529
  justifyContent: 'center',
1530
+ marginRight: 8,
1142
1531
  }}
1143
1532
  >
1144
- <Ionicons name="image" size={24} color={colors.blue[500]} />
1145
- </Box>
1533
+ <MaterialIcons name="add" size={20} color="#888" />
1534
+ </TouchableOpacity>
1535
+ // <Box
1536
+ // style={{
1537
+ // width: 32,
1538
+ // height: 32,
1539
+ // alignItems: 'center',
1540
+ // justifyContent: 'center',
1541
+ // }}
1542
+ // >
1543
+ // <Ionicons name="image" size={24} color={colors.blue[500]} />
1544
+ // </Box>
1146
1545
  )}
1147
1546
  containerStyle={{
1148
1547
  alignItems: 'center',
1149
1548
  justifyContent: 'center',
1150
- marginLeft: 8,
1549
+ marginLeft: 20,
1151
1550
  marginBottom: 0,
1152
1551
  }}
1153
1552
  />
@@ -1156,114 +1555,77 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
1156
1555
 
1157
1556
  // Create a more visible and reliable image preview with cancel button
1158
1557
  const renderAccessory = useCallback(() => {
1159
- if (!selectedImage) {
1160
- return null;
1161
- }
1162
-
1558
+ if (!images.length) return null;
1163
1559
  return (
1164
- <View
1165
- style={{
1166
- height: 70,
1167
- backgroundColor: 'white',
1168
- borderTopWidth: 1,
1169
- borderTopColor: '#e0e0e0',
1170
- flexDirection: 'row',
1171
- alignItems: 'center',
1172
- margin: 0,
1173
- padding: 0,
1174
- paddingVertical: 0,
1175
- position: 'absolute',
1176
- bottom: Platform.OS === 'ios' ? 105 : 95, // Position well above the input area
1177
- left: 0,
1178
- right: 0,
1179
- zIndex: 1,
1180
- elevation: 3,
1181
- shadowColor: '#000',
1182
- shadowOffset: { width: 0, height: -1 },
1183
- shadowOpacity: 0.05,
1184
- shadowRadius: 2,
1185
- }}
1186
- >
1187
- <View
1560
+ <Box style={{ position: 'relative', height: 70, backgroundColor: 'transparent', justifyContent: 'center' }}>
1561
+ <ScrollView
1562
+ horizontal
1563
+ showsHorizontalScrollIndicator={false}
1188
1564
  style={{
1189
- flex: 1,
1190
1565
  flexDirection: 'row',
1191
- alignItems: 'center',
1192
1566
  paddingLeft: 15,
1193
1567
  paddingRight: 5,
1194
1568
  }}
1569
+ contentContainerStyle={{
1570
+ alignItems: 'center',
1571
+ height: '100%',
1572
+ }}
1195
1573
  >
1196
- <View
1197
- style={{
1198
- width: 56,
1199
- height: 56,
1200
- marginRight: 15,
1201
- borderRadius: 4,
1202
- backgroundColor: colors.gray[200],
1203
- overflow: 'hidden',
1204
- borderWidth: 1,
1205
- borderColor: '#e0e0e0',
1206
- }}
1207
- >
1208
- <Image
1209
- key={selectedImage}
1210
- alt={'selected image'}
1211
- source={{ uri: selectedImage }}
1574
+ {images.map((img, index) => (
1575
+ <View
1576
+ key={`image-preview-${index}`}
1212
1577
  style={{
1213
- width: '100%',
1214
- height: '100%',
1578
+ width: 40,
1579
+ height: 40,
1580
+ marginRight: 15,
1581
+ borderRadius: 4,
1582
+ backgroundColor: colors.gray[200],
1583
+ overflow: 'hidden',
1584
+ borderWidth: 1,
1585
+ borderColor: '#e0e0e0',
1586
+ position: 'relative',
1587
+ zIndex: 10,
1215
1588
  }}
1216
- size={'md'}
1217
- />
1218
- {loading && (
1219
- <View
1589
+ >
1590
+ <Image
1591
+ source={{ uri: img.uri || img.url }}
1592
+ style={{ width: '100%', height: '100%' }}
1593
+ alt={`selected image ${index + 1}`}
1594
+ />
1595
+ {/* Cross button at top right */}
1596
+ <TouchableOpacity
1597
+ onPress={() => {
1598
+ const newImages = [...images];
1599
+ newImages.splice(index, 1);
1600
+ setImages(newImages);
1601
+ if (newImages.length === 0) {
1602
+ setSelectedImage('');
1603
+ if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
1604
+ textInputRef.current.focus();
1605
+ }
1606
+ }
1607
+ }}
1220
1608
  style={{
1221
1609
  position: 'absolute',
1222
- top: 0,
1223
- left: 0,
1224
- right: 0,
1225
- bottom: 0,
1226
- backgroundColor: 'rgba(255, 255, 255, 0.7)',
1227
- justifyContent: 'center',
1610
+ top: -1,
1611
+ right: -1,
1612
+ backgroundColor: 'rgba(0,0,0,0.6)',
1613
+ borderRadius: 12,
1614
+ width: 20,
1615
+ height: 20,
1228
1616
  alignItems: 'center',
1617
+ justifyContent: 'center',
1618
+ zIndex: 9999,
1229
1619
  }}
1230
1620
  >
1231
- <Spinner size="small" color={colors.blue[500]} />
1232
- </View>
1233
- )}
1234
- </View>
1235
-
1236
- <View style={{ flex: 1 }}>
1237
- <Text style={{ fontSize: 14, fontWeight: '400', color: colors.gray[800] }}>
1238
- {images[0]?.fileName || 'image_' + new Date().getTime() + '.jpg'}
1239
- </Text>
1240
- <Text style={{ fontSize: 12, color: colors.gray[500], marginTop: 2 }}>
1241
- {loading ? 'Preparing...' : 'Ready to send'}
1242
- </Text>
1243
- </View>
1244
-
1245
- <TouchableHighlight
1246
- underlayColor={'rgba(0,0,0,0.1)'}
1247
- onPress={() => {
1248
- setSelectedImage('');
1249
- setImages([]);
1250
- }}
1251
- style={{
1252
- backgroundColor: colors.red[500],
1253
- borderRadius: 24,
1254
- width: 36,
1255
- height: 36,
1256
- alignItems: 'center',
1257
- justifyContent: 'center',
1258
- marginRight: 10,
1259
- }}
1260
- >
1261
- <Ionicons name="close" size={20} color="white" />
1262
- </TouchableHighlight>
1263
- </View>
1264
- </View>
1621
+ <Ionicons name="close" size={16} color="white" />
1622
+ </TouchableOpacity>
1623
+ </View>
1624
+ ))}
1625
+ </ScrollView>
1626
+ </Box>
1265
1627
  );
1266
- }, [selectedImage, loading, images]);
1628
+ }, [images]);
1267
1629
 
1268
1630
  const setImageViewerObject = (obj: any, v: boolean) => {
1269
1631
  setImageObject(obj);
@@ -1288,60 +1650,15 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
1288
1650
  );
1289
1651
  }, [imageObject]);
1290
1652
 
1291
- // Create a skeleton component for message bubbles with images
1653
+ // Update the message rendering to show images instantly without loaders
1292
1654
  const renderMessage = useCallback(
1293
1655
  (props: any) => {
1294
- // Check if this message ID matches the uploading message ID
1295
- const isUploading = props.currentMessage._id === uploadingMessageId && loading;
1296
-
1297
- if (isUploading && props.currentMessage.image) {
1298
- // Return a custom message skeleton during upload
1299
- return (
1300
- <View
1301
- style={{
1302
- padding: 10,
1303
- marginBottom: 10,
1304
- marginRight: 10,
1305
- alignSelf: 'flex-end',
1306
- borderRadius: 15,
1307
- backgroundColor: colors.gray[100],
1308
- maxWidth: '80%',
1309
- }}
1310
- >
1311
- {props.currentMessage.text && props.currentMessage.text.trim() !== '' && (
1312
- <Box
1313
- style={{
1314
- height: 15,
1315
- borderRadius: 4,
1316
- backgroundColor: colors.gray[200],
1317
- overflow: 'hidden',
1318
- marginBottom: 8,
1319
- }}
1320
- >
1321
- <Skeleton variant="rounded" style={{ flex: 1 }} />
1322
- </Box>
1323
- )}
1324
- <Box
1325
- style={{
1326
- height: 150,
1327
- width: 150,
1328
- borderRadius: 10,
1329
- backgroundColor: colors.gray[200],
1330
- overflow: 'hidden',
1331
- }}
1332
- >
1333
- <Skeleton variant="rounded" style={{ flex: 1 }} />
1334
- </Box>
1335
- </View>
1336
- );
1337
- }
1338
-
1339
- // Use memo to prevent unnecessary re-renders of each message
1656
+ // For all messages, use the SlackMessage component directly
1340
1657
  return (
1341
1658
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
1342
1659
  );
1343
1660
  },
1344
- [isShowImageViewer, uploadingMessageId, loading],
1661
+ [isShowImageViewer],
1345
1662
  );
1346
1663
 
1347
1664
  let onScroll = false;
@@ -1378,27 +1695,134 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
1378
1695
  ) : null;
1379
1696
  }, [loadingOldMessages]);
1380
1697
 
1381
- // Add renderInputToolbar function
1382
- const renderInputToolbar = useCallback((props) => {
1383
- return (
1384
- <InputToolbar
1385
- {...props}
1386
- containerStyle={{
1387
- backgroundColor: 'white',
1388
- borderTopWidth: 1,
1389
- borderTopColor: colors.gray[200],
1390
- paddingHorizontal: 4,
1391
- paddingVertical: 0,
1392
- paddingTop: 2,
1393
- marginBottom: 0,
1394
- marginTop: 0,
1395
- }}
1396
- primaryStyle={{
1397
- alignItems: 'center',
1398
- }}
1399
- />
1400
- );
1401
- }, []);
1698
+ // Add state for tracking input toolbar height
1699
+ const [inputToolbarHeight, setInputToolbarHeight] = useState(30);
1700
+
1701
+ // Update renderInputToolbar to use a compact, single-row style for the input area, ensuring it does not expand to fill the screen. The plus button, text input, and send button should be in a rounded row, with selected images in a row below. Use minHeight/maxHeight and proper padding/margin to keep the toolbar compact.
1702
+ const renderInputToolbar = useCallback(
1703
+ (props) => (
1704
+ <View style={{ backgroundColor: '#fff', paddingBottom: 4, paddingTop: 4 }}>
1705
+ <View
1706
+ style={{
1707
+ flexDirection: 'row',
1708
+ alignItems: 'center',
1709
+ minHeight: 44,
1710
+ maxHeight: 56,
1711
+ backgroundColor: '#fff',
1712
+ borderRadius: 22,
1713
+ marginHorizontal: 8,
1714
+ paddingHorizontal: 8,
1715
+ borderTopWidth: 1,
1716
+ borderTopColor: '#e0e0e0',
1717
+ }}
1718
+ >
1719
+ <TouchableOpacity
1720
+ onPress={onSelectImages}
1721
+ style={{
1722
+ width: 32,
1723
+ height: 32,
1724
+ borderRadius: 16,
1725
+ backgroundColor: '#fff',
1726
+ alignItems: 'center',
1727
+ justifyContent: 'center',
1728
+ marginRight: 8,
1729
+ }}
1730
+ >
1731
+ <MaterialIcons name="add" size={24} color="#888" />
1732
+ </TouchableOpacity>
1733
+ <TextInput
1734
+ ref={textInputRef}
1735
+ style={{
1736
+ flex: 1,
1737
+ //minHeight: 36,
1738
+ maxHeight: 44,
1739
+ backgroundColor: 'transparent',
1740
+ color: '#444',
1741
+ paddingHorizontal: 8,
1742
+ paddingVertical: 0,
1743
+ alignSelf: 'center',
1744
+ textAlignVertical: 'center',
1745
+ }}
1746
+ placeholder="Jot something down"
1747
+ placeholderTextColor={colors.gray[400]}
1748
+ multiline
1749
+ value={messageText}
1750
+ onChangeText={setMessageText}
1751
+ />
1752
+ <TouchableOpacity
1753
+ onPress={() => handleSend([{ text: messageText }])}
1754
+ // disabled={(!messageText.trim() && images.length === 0) || isUploadingImage || loading}
1755
+ disabled={false}
1756
+ style={{
1757
+ marginLeft: 8,
1758
+ // opacity: (!messageText.trim() && images.length === 0) || isUploadingImage || loading ? 0.5 : 1,
1759
+ opacity: !messageText.trim() && images.length === 0 ? 0.5 : 1,
1760
+ }}
1761
+ >
1762
+ <MaterialCommunityIcons
1763
+ name="send-circle"
1764
+ size={32}
1765
+ color={!messageText.trim() && images.length === 0 ? colors.gray[400] : colors.blue[500]}
1766
+ // color={
1767
+ // (!messageText.trim() && images.length === 0) || isUploadingImage || loading
1768
+ // ? colors.gray[400]
1769
+ // : colors.blue[500]
1770
+ // }
1771
+ />
1772
+ </TouchableOpacity>
1773
+ </View>
1774
+ {/* Selected Images Row */}
1775
+ {images && images.length > 0 && (
1776
+ <ScrollView
1777
+ horizontal
1778
+ showsHorizontalScrollIndicator={false}
1779
+ style={{ marginTop: 4, marginLeft: 8 }}
1780
+ >
1781
+ {images.map((img, index) => (
1782
+ <View
1783
+ key={`image-preview-${index}`}
1784
+ style={{
1785
+ width: 48,
1786
+ height: 48,
1787
+ marginRight: 8,
1788
+ borderRadius: 6,
1789
+ overflow: 'hidden',
1790
+ position: 'relative',
1791
+ backgroundColor: colors.gray[200],
1792
+ }}
1793
+ >
1794
+ <Image
1795
+ source={{ uri: img.uri || img.url }}
1796
+ style={{ width: '100%', height: '100%' }}
1797
+ alt={`selected image ${index + 1}`}
1798
+ />
1799
+ <TouchableOpacity
1800
+ onPress={() => {
1801
+ handleRemoveImage(index);
1802
+ // handleRemoveImage already focuses input if needed
1803
+ }}
1804
+ style={{
1805
+ position: 'absolute',
1806
+ top: 2,
1807
+ right: 2,
1808
+ backgroundColor: 'rgba(0,0,0,0.6)',
1809
+ borderRadius: 10,
1810
+ width: 20,
1811
+ height: 20,
1812
+ alignItems: 'center',
1813
+ justifyContent: 'center',
1814
+ }}
1815
+ >
1816
+ <Ionicons name="close" size={14} color="white" />
1817
+ </TouchableOpacity>
1818
+ </View>
1819
+ ))}
1820
+ </ScrollView>
1821
+ )}
1822
+ </View>
1823
+ ),
1824
+ [onSelectImages, messageText, images, isUploadingImage, loading, handleSend, handleRemoveImage],
1825
+ );
1402
1826
 
1403
1827
  // Create a memoized ImageViewerModal component
1404
1828
  const imageViewerModal = useMemo(
@@ -1408,34 +1832,20 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
1408
1832
  [isShowImageViewer, modalContent],
1409
1833
  );
1410
1834
 
1411
- // Create a memoized subscription handler component
1412
- const subscriptionHandler = useMemo(
1413
- () => (
1414
- <SubscriptionHandler
1415
- channelId={channelId?.toString()}
1416
- subscribeToNewMessages={() =>
1417
- subscribeToMore({
1418
- document: CHAT_MESSAGE_ADDED,
1419
- variables: {
1420
- channelId: channelId?.toString(),
1421
- },
1422
- // Let type policy handle the merge
1423
- })
1424
- }
1425
- />
1426
- ),
1427
- [channelId, subscribeToMore, auth?.id],
1428
- );
1429
-
1430
1835
  // Create a memoized renderChatFooter function
1431
1836
  const renderChatFooter = useCallback(() => {
1432
1837
  return (
1433
1838
  <>
1434
1839
  {imageViewerModal}
1435
- {subscriptionHandler}
1840
+ <SubscriptionHandler
1841
+ subscribeToMore={subscribe}
1842
+ document={CHAT_MESSAGE_ADDED}
1843
+ variables={{ channelId: channelId?.toString() }}
1844
+ updateQuery={undefined}
1845
+ />
1436
1846
  </>
1437
1847
  );
1438
- }, [imageViewerModal, subscriptionHandler]);
1848
+ }, [imageViewerModal, subscribe]);
1439
1849
 
1440
1850
  // Add optimized listViewProps to reduce re-renders and improve list performance
1441
1851
  const listViewProps = useMemo(
@@ -1478,113 +1888,143 @@ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowTh
1478
1888
 
1479
1889
  // Return optimized component with performance improvements
1480
1890
  return (
1481
- <View
1482
- style={{
1483
- flex: 1,
1484
- backgroundColor: 'white',
1485
- position: 'relative',
1486
- }}
1891
+ <KeyboardAvoidingView
1892
+ style={{ flex: 1, justifyContent: 'flex-end' }}
1893
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
1894
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
1487
1895
  >
1488
- {messageLoading && <Spinner color={'#3b82f6'} />}
1489
-
1490
- {/* Render the image preview directly in the container so it's properly positioned */}
1491
- {selectedImage ? renderAccessory() : null}
1492
-
1493
- <GiftedChat
1494
- ref={messageRootListRef}
1495
- wrapInSafeArea={true}
1496
- renderLoading={() => <Spinner color={'#3b82f6'} />}
1497
- messages={messageList}
1498
- listViewProps={{
1499
- ...listViewProps,
1500
- contentContainerStyle: {
1501
- paddingBottom: selectedImage ? 90 : 0, // Add padding at the bottom when image is selected
1502
- },
1503
- }}
1504
- onSend={handleSend}
1505
- text={messageText || ' '}
1506
- onInputTextChanged={(text) => setMessageText(text)}
1507
- renderFooter={() => (loading && !images.length ? <Spinner color={'#3b82f6'} /> : null)}
1508
- scrollToBottom
1509
- user={{
1510
- _id: auth?.id || '',
1511
- }}
1512
- isTyping={false} // Setting to false to reduce animations
1513
- alwaysShowSend={true} // Always show send button regardless of text content
1514
- renderSend={renderSend}
1515
- renderMessageText={renderMessageText}
1516
- renderInputToolbar={renderInputToolbar}
1517
- minInputToolbarHeight={50}
1518
- renderActions={channelId && renderActions}
1519
- renderMessage={renderMessage}
1520
- renderChatFooter={renderChatFooter}
1521
- renderLoadEarlier={renderLoadEarlier}
1522
- loadEarlier={totalCount > channelMessages.length}
1523
- isLoadingEarlier={loadingOldMessages}
1524
- bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0} // Adjust bottom offset based on image preview
1525
- textInputProps={{
1526
- style: {
1527
- borderWidth: 1,
1528
- borderColor: colors.gray[300],
1529
- backgroundColor: '#f8f8f8',
1530
- borderRadius: 20,
1531
- minHeight: 36,
1532
- maxHeight: 80,
1533
- color: '#000',
1534
- padding: 8,
1535
- paddingHorizontal: 15,
1536
- fontSize: 16,
1537
- flex: 1,
1538
- marginVertical: 2,
1539
- marginBottom: 0,
1540
- },
1541
- multiline: true,
1542
- returnKeyType: 'default',
1543
- enablesReturnKeyAutomatically: true,
1544
- placeholderTextColor: colors.gray[400],
1545
- }}
1546
- minComposerHeight={36}
1547
- maxComposerHeight={100}
1548
- isKeyboardInternallyHandled={true}
1549
- placeholder="Type a message..."
1550
- lightboxProps={{
1551
- underlayColor: 'transparent',
1552
- springConfig: { tension: 90000, friction: 90000 },
1553
- disabled: true,
1896
+ <View
1897
+ style={{
1898
+ flex: 1,
1899
+ backgroundColor: 'white',
1900
+ position: 'relative',
1901
+ marginBottom: images.length > 0 ? 5 : bottomMargin,
1554
1902
  }}
1555
- infiniteScroll={false} // Disable automatic loading
1556
- />
1557
- </View>
1903
+ >
1904
+ {errorMessage ? (
1905
+ <ErrorNotification
1906
+ message={errorMessage}
1907
+ onClose={() => setErrorMessage('')}
1908
+ type={notificationType}
1909
+ />
1910
+ ) : null}
1911
+
1912
+ {messageLoading && <Spinner color={'#3b82f6'} />}
1913
+ <GiftedChatInboxComponent
1914
+ ref={messageRootListRef}
1915
+ errorMessage={errorMessage}
1916
+ images={images}
1917
+ onSelectImages={onSelectImages}
1918
+ onRemoveImage={handleRemoveImage}
1919
+ selectedImage={selectedImage}
1920
+ setSelectedImage={setSelectedImage}
1921
+ isUploadingImage={isUploadingImage}
1922
+ loading={loading}
1923
+ wrapInSafeArea={true}
1924
+ renderLoading={() => <Spinner color={'#3b82f6'} />}
1925
+ messages={messageList}
1926
+ renderAvatar={null}
1927
+ showUserAvatar={false}
1928
+ listViewProps={{
1929
+ ...listViewProps,
1930
+ contentContainerStyle: {
1931
+ paddingBottom: inputToolbarHeight,
1932
+ },
1933
+ }}
1934
+ onSend={handleSend}
1935
+ text={messageText || ' '}
1936
+ onInputTextChanged={(text) => {
1937
+ setMessageText(text);
1938
+ }}
1939
+ renderFooter={() => (isUploadingImage ? <Spinner color={'#3b82f6'} /> : null)}
1940
+ scrollToBottom
1941
+ user={{
1942
+ _id: auth?.id || '',
1943
+ }}
1944
+ renderSend={renderSend}
1945
+ renderMessageText={renderMessageText}
1946
+ renderMessage={renderMessage}
1947
+ renderChatFooter={renderChatFooter}
1948
+ renderLoadEarlier={renderLoadEarlier}
1949
+ loadEarlier={totalCount > channelMessages.length}
1950
+ isLoadingEarlier={loadingOldMessages}
1951
+ placeholder="Jot something down"
1952
+ infiniteScroll={true}
1953
+ // renderChatEmpty={() => (
1954
+ // <><Text>Empty</Text>
1955
+ // {!loading && messageList && messageList?.length == 0 && (
1956
+ // <Box className="p-5">
1957
+ // <Center className="mt-6">
1958
+ // <Ionicons name="chatbubbles" size={30} />
1959
+ // <Text>You don't have any message yet!</Text>
1960
+ // </Center>
1961
+ // </Box>
1962
+ // )}
1963
+ // </>
1964
+ // )}
1965
+ />
1966
+
1967
+ {/* <GiftedChat
1968
+ ref={messageRootListRef}
1969
+ wrapInSafeArea={true}
1970
+ renderLoading={() => <Spinner color={'#3b82f6'} />}
1971
+ messages={messageList}
1972
+ renderAvatar={null}
1973
+ showUserAvatar={false}
1974
+ listViewProps={{
1975
+ ...listViewProps,
1976
+ contentContainerStyle: {
1977
+ paddingBottom: inputToolbarHeight,
1978
+ },
1979
+ }}
1980
+ onSend={handleSend}
1981
+ text={messageText || ' '}
1982
+ onInputTextChanged={(text) => {
1983
+ setMessageText(text);
1984
+ }}
1985
+ renderFooter={() => (isUploadingImage ? <Spinner color={'#3b82f6'} /> : null)}
1986
+ scrollToBottom
1987
+ user={{
1988
+ _id: auth?.id || '',
1989
+ }}
1990
+ isTyping={false}
1991
+ alwaysShowSend={true}
1992
+ renderSend={renderSend}
1993
+ renderMessageText={renderMessageText}
1994
+ renderInputToolbar={renderInputToolbar}
1995
+ // renderComposer={renderComposer}
1996
+ // minInputToolbarHeight={isActionSheetVisible ? 0 : 56}
1997
+ minInputToolbarHeight={inputToolbarHeight}
1998
+ renderActions={null}
1999
+ renderMessage={renderMessage}
2000
+ renderChatFooter={renderChatFooter}
2001
+ renderLoadEarlier={renderLoadEarlier}
2002
+ loadEarlier={totalCount > channelMessages.length}
2003
+ isLoadingEarlier={loadingOldMessages}
2004
+ bottomOffset={0}
2005
+ isKeyboardInternallyHandled={false}
2006
+ textInputProps={{
2007
+ multiline: true,
2008
+ returnKeyType: 'default',
2009
+ enablesReturnKeyAutomatically: true,
2010
+ placeholderTextColor: colors.gray[400],
2011
+ }}
2012
+ minComposerHeight={36}
2013
+ maxComposerHeight={100}
2014
+ placeholder="Jot something down"
2015
+ lightboxProps={{
2016
+ underlayColor: 'transparent',
2017
+ springConfig: { tension: 90000, friction: 90000 },
2018
+ disabled: true,
2019
+ }}
2020
+ infiniteScroll={false}
2021
+ renderAccessory={selectedImage ? renderAccessory : null}
2022
+ /> */}
2023
+ </View>
2024
+ </KeyboardAvoidingView>
1558
2025
  );
1559
2026
  };
1560
2027
 
1561
- const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptionHandlerProps) => {
1562
- // Store the channelId in a ref to track changes
1563
- const channelIdRef = useRef(channelId);
1564
-
1565
- useEffect(() => {
1566
- // Don't set up subscription if there's no channel ID
1567
- if (!channelId) {
1568
- return;
1569
- }
1570
-
1571
- // Call the subscribe function and store the unsubscribe function
1572
- const unsubscribe = subscribeToNewMessages();
1573
-
1574
- // Update the ref with the current channelId
1575
- channelIdRef.current = channelId;
1576
-
1577
- // Return cleanup function
1578
- return () => {
1579
- if (unsubscribe && typeof unsubscribe === 'function') {
1580
- unsubscribe();
1581
- }
1582
- };
1583
- }, [channelId, subscribeToNewMessages]);
1584
-
1585
- return null;
1586
- };
1587
-
1588
2028
  // Export with React.memo to prevent unnecessary re-renders
1589
2029
  export const ConversationView = React.memo(ConversationViewComponent, (prevProps, nextProps) => {
1590
2030
  // Only re-render if these critical props change