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

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