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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/lib/compute.js +2 -3
  3. package/lib/compute.js.map +1 -1
  4. package/lib/index.js.map +1 -1
  5. package/lib/queries/inboxQueries.js +65 -0
  6. package/lib/queries/inboxQueries.js.map +1 -0
  7. package/lib/routes.json +2 -3
  8. package/lib/screens/inbox/DialogMessages.js +1 -1
  9. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  10. package/lib/screens/inbox/DialogThreadMessages.js +4 -8
  11. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  12. package/lib/screens/inbox/DialogThreads.js +57 -12
  13. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  14. package/lib/screens/inbox/Inbox.js +1 -1
  15. package/lib/screens/inbox/Inbox.js.map +1 -1
  16. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  17. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  18. package/lib/screens/inbox/components/CachedImage/index.js +168 -46
  19. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  20. package/lib/screens/inbox/components/DialogItem.js +169 -0
  21. package/lib/screens/inbox/components/DialogItem.js.map +1 -0
  22. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
  23. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  24. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +147 -31
  25. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  26. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +6 -1
  27. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  28. package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
  29. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  30. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -55
  31. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  32. package/lib/screens/inbox/config/config.js +2 -2
  33. package/lib/screens/inbox/config/config.js.map +1 -1
  34. package/lib/screens/inbox/containers/ConversationView.js +1111 -434
  35. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  36. package/lib/screens/inbox/containers/Dialogs.js +193 -80
  37. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  38. package/lib/screens/inbox/containers/ThreadConversationView.js +725 -216
  39. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  40. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  41. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  42. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  43. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  44. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  45. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  46. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  47. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  48. package/package.json +4 -4
  49. package/src/compute.ts +5 -6
  50. package/src/index.ts +2 -0
  51. package/src/navigation/InboxNavigation.tsx +3 -3
  52. package/src/queries/inboxQueries.ts +299 -0
  53. package/src/queries/index.d.ts +2 -0
  54. package/src/queries/index.ts +1 -0
  55. package/src/screens/inbox/DialogMessages.tsx +1 -1
  56. package/src/screens/inbox/DialogThreadMessages.tsx +7 -14
  57. package/src/screens/inbox/DialogThreads.tsx +55 -61
  58. package/src/screens/inbox/Inbox.tsx +1 -1
  59. package/src/screens/inbox/components/Actionsheet.tsx +30 -0
  60. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  61. package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
  62. package/src/screens/inbox/components/DialogItem.tsx +306 -0
  63. package/src/screens/inbox/components/DialogsHeader.tsx +6 -13
  64. package/src/screens/inbox/components/DialogsListItem.tsx +262 -198
  65. package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
  66. package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
  67. package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
  68. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +337 -194
  69. package/src/screens/inbox/components/SlackInput.tsx +23 -0
  70. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
  71. package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +1 -1
  72. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  73. package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
  74. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +53 -55
  75. package/src/screens/inbox/components/ThreadsViewItem.tsx +178 -285
  76. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  77. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  78. package/src/screens/inbox/config/config.ts +2 -2
  79. package/src/screens/inbox/containers/ConversationView.tsx +1843 -702
  80. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  81. package/src/screens/inbox/containers/Dialogs.tsx +402 -204
  82. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +4 -4
  83. package/src/screens/inbox/containers/ThreadConversationView.tsx +1350 -319
  84. package/src/screens/inbox/containers/ThreadsView.tsx +105 -193
  85. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  86. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  87. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  88. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
  89. package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
  90. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  91. package/src/screens/inbox/index.ts +37 -0
  92. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  93. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
  94. package/tsconfig.json +11 -54
  95. package/lib/screens/inbox/components/DialogsListItem.js +0 -171
  96. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  97. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -171
  98. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
@@ -11,25 +11,46 @@ import {
11
11
  Image,
12
12
  Spinner,
13
13
  Text,
14
+ Skeleton,
15
+ ScrollView,
16
+ Toast,
17
+ ToastTitle,
18
+ ToastDescription,
19
+ useToast,
20
+ ToastAlert,
21
+ VStack,
22
+ Divider,
23
+ Center,
14
24
  } from '@admin-layout/gluestack-ui-mobile';
15
- import { Platform, TouchableHighlight } from 'react-native';
16
- 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';
17
37
  import { navigationRef } from '@common-stack/client-react';
18
- import { useSelector } from 'react-redux';
38
+ import { useSelector, shallowEqual } from 'react-redux';
19
39
  import { orderBy, startCase, uniqBy } from 'lodash-es';
20
40
  import * as ImagePicker from 'expo-image-picker';
21
41
  import { encode as atob } from 'base-64';
22
- import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
23
- import { Actions, GiftedChat, IMessage, MessageText, Send } from 'react-native-gifted-chat';
24
- import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo } from 'common';
42
+ import { Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
43
+ import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
44
+ import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo, FileRefType, PostTypeEnum } from 'common';
25
45
  import {
26
- OnChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
27
- useMessagesQuery,
28
- useSendExpoNotificationOnPostMutation,
29
- useSendMessagesMutation,
30
- useViewChannelDetailQuery,
31
- useAddDirectChannelMutation,
32
- } from 'common/lib/generated/generated.js';
46
+ CHAT_MESSAGE_ADDED,
47
+ useChannelDetailQuery,
48
+ useChannelMessagesQuery,
49
+ useSendChannelMessage,
50
+ useAddDirectChannel,
51
+ MESSAGES_DOCUMENT,
52
+ useSendExpoNotification,
53
+ } from '../../../queries/inboxQueries';
33
54
  import { useUploadFilesNative } from '@messenger-box/platform-client';
34
55
  import { objectId } from '@messenger-box/core';
35
56
  import { userSelector } from '@adminide-stack/user-auth0-client';
@@ -37,6 +58,22 @@ import { format, isToday, isYesterday } from 'date-fns';
37
58
  import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
38
59
  import CachedImage from '../components/CachedImage';
39
60
  import { config } from '../config';
61
+ import colors from 'tailwindcss/colors';
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';
70
+
71
+ // Define an extended interface for ImagePickerAsset with url property
72
+ interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
73
+ url?: string;
74
+ fileName?: string;
75
+ mimeType?: string;
76
+ }
40
77
 
41
78
  const {
42
79
  MESSAGES_PER_PAGE,
@@ -64,6 +101,7 @@ interface IMessageProps extends IMessage {
64
101
  propsConfiguration?: any;
65
102
  replies?: any;
66
103
  isShowThreadMessage?: boolean;
104
+ images?: string[]; // Add support for multiple images
67
105
  }
68
106
 
69
107
  export interface AlertMessageAttachmentsInterface {
@@ -76,257 +114,518 @@ export interface AlertMessageAttachmentsInterface {
76
114
  };
77
115
  }
78
116
 
79
- const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMessage, ...rest }: any) => {
80
- const [channelToTop, setChannelToTop] = useState(0);
81
- const [channelMessages, setChannelMessages] = useState<any>([]);
82
- const auth: any = useSelector(userSelector);
83
- const [totalCount, setTotalCount] = useState<any>(0);
84
- const [selectedImage, setImage] = useState<string>('');
85
- const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
86
- const [channelId, setChannelId] = useState<any>(rest?.isCreateNewChannel ? null : ChannelId);
87
- const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
88
- const navigation = useNavigation<any>();
89
- const [files, setFiles] = useState<File[]>([]);
90
- const [images, setImages] = useState<ImagePicker.ImagePickerAsset[]>([]);
91
- const [msg, setMsg] = useState<string>('');
117
+ // Fix for the optimistic response types
118
+ type OptimisticPropsConfig = {
119
+ __typename: 'MachineConfiguration';
120
+ resource: string;
121
+ };
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
+
208
+ const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowThreadMessage, ...rest }: any) => {
209
+ // Core state management using React hooks instead of XState
210
+ const { params } = useRoute<any>();
211
+ const [channelId, setChannelId] = useState<string | null>(initialChannelId || null);
212
+ const [messageText, setMessageText] = useState('');
92
213
  const [loading, setLoading] = useState(false);
93
- const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
94
- const [imageLoading, setImageLoading] = useState(false);
95
- const [expoTokens, setExpoTokens] = useState<any[]>([]);
214
+ const [loadingOldMessages, setLoadingOldMessages] = useState(false);
215
+ const [error, setError] = useState<string | null>(null);
216
+ const [selectedImage, setSelectedImage] = useState<string>('');
217
+ const [images, setImages] = useState<any[]>([]);
96
218
  const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
97
219
  const [imageObject, setImageObject] = useState<any>({});
98
- const [skip, setSkip] = useState(0);
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);
227
+
228
+ // Create refs for various operations
99
229
  const messageRootListRef = useRef<any>(null);
100
- const isFocused = useIsFocused();
230
+ const textInputRef = useRef<any>(null); // Add new ref for the text input
231
+ const isMounted = useRef(true);
232
+ const fetchOldDebounceRef = useRef(false);
101
233
 
102
- const [addDirectChannel, { loading: addDirectChannaleLoading }] = useAddDirectChannelMutation();
234
+ // Navigation and auth
235
+ const auth: any = useSelector(userSelector, shallowEqual);
236
+ const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
237
+ const navigation = useNavigation<any>();
238
+ const isFocused = useIsFocused();
103
239
 
240
+ // Apollo mutations
241
+ const [addDirectChannel] = useAddDirectChannel();
104
242
  const { startUpload } = useUploadFilesNative();
243
+ const [sendMsg] = useSendChannelMessage();
244
+ const [sendExpoNotification] = useSendExpoNotification();
105
245
 
106
- const [sendMsg] = useSendMessagesMutation();
107
-
108
- const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
246
+ // Add skip state for pagination
247
+ const [skip, setSkip] = useState(0);
109
248
 
249
+ // Apollo query for messages
110
250
  const {
111
251
  data,
112
252
  loading: messageLoading,
253
+ error: inboxError,
113
254
  refetch,
114
- fetchMore: fetchMoreMessages,
115
- subscribeToMore,
116
- }: any = useMessagesQuery({
117
- variables: {
255
+ subscribe,
256
+ } = useInboxMessages({
257
+ useQueryHook: useChannelMessagesQuery,
258
+ queryVariables: {
118
259
  channelId: channelId?.toString(),
119
260
  parentId: null,
120
261
  limit: MESSAGES_PER_PAGE,
121
- skip: skip,
262
+ skip: skip, // Use skip state for pagination
263
+ orgName: params?.orgName,
122
264
  },
123
- skip: !channelId,
124
- fetchPolicy: 'cache-and-network',
125
- nextFetchPolicy: 'cache-first',
126
- refetchWritePolicy: 'merge',
265
+ subscriptionDocument: CHAT_MESSAGE_ADDED,
266
+ subscriptionVariables: { channelId: channelId?.toString() },
267
+ updateQuery: undefined, // Provide custom updateQuery if needed
268
+ onError: (err) => setError(String(err)),
127
269
  });
128
270
 
129
- // const {
130
- // data: channelsDetail,
131
- // loading: channelLoading,
132
- // refetch: refetchChannelDetail,
133
- // } = useViewChannelDetailQuery({
134
- // variables: {
135
- // id: channelId?.toString(),
136
- // },
137
- // });
138
- // const { data: users } = useGetAllUsersQuery();
139
-
140
- React.useEffect(() => {
271
+ // Extract messages from the query data
272
+ const channelMessages = useMemo(() => {
273
+ return (data?.messages?.data as any[]) || [];
274
+ }, [data?.messages?.data]);
275
+
276
+ // Get total message count
277
+ const totalCount = useMemo(() => {
278
+ return data?.messages?.totalCount || 0;
279
+ }, [data?.messages?.totalCount]);
280
+
281
+ // Clear messages when component unmounts
282
+ useEffect(() => {
141
283
  return () => {
142
- setChannelMessages([]);
284
+ isMounted.current = false;
143
285
  };
144
286
  }, []);
145
287
 
288
+ // Update channelId from props or navigation params
289
+ useEffect(() => {
290
+ const currentChannelId = initialChannelId || currentRoute?.params?.channelId;
291
+ if (currentChannelId) {
292
+ setChannelId(currentChannelId);
293
+ }
294
+ }, [initialChannelId, currentRoute]);
295
+
296
+ // Focus/unfocus behavior
146
297
  useFocusEffect(
147
298
  React.useCallback(() => {
148
- // Do something when the screen is focused
149
- setSkip(0);
150
- // refetchChannelDetail({ id: channelId?.toString() });
151
299
  if (channelId) {
152
- refetch({
153
- channelId: channelId?.toString(),
154
- parentId: null,
155
- limit: MESSAGES_PER_PAGE,
156
- skip: 0,
157
- }).then(({ data }) => {
158
- if (!data?.messages) {
159
- return;
160
- }
161
- const { data: messages, totalCount }: any = data.messages;
162
- setTotalCount(totalCount);
163
- setChannelMessages(messages);
164
- });
300
+ refetch();
165
301
  }
166
- return () => {
167
- // Do something when the screen is unfocused
168
- // Useful for cleanup functions
169
- setChannelId(null);
170
- setTotalCount(0);
171
- setSkip(0);
172
- };
173
- }, [channelId, isFocused]),
302
+ }, [isFocused, refetch]),
174
303
  );
175
304
 
176
- React.useEffect(() => {
177
- const currentChannelId = ChannelId || currentRoute?.params?.channelId;
178
- setChannelId(currentChannelId);
179
- }, [ChannelId, currentRoute]);
305
+ // When fetching more messages, update skip
306
+ const fetchMoreMessagesImpl = useCallback(async () => {
307
+ try {
308
+ setLoadingOldMessages(true);
309
+ const response = await refetch({
310
+ channelId: channelId?.toString(),
311
+ parentId: null,
312
+ limit: MESSAGES_PER_PAGE,
313
+ skip: channelMessages.length,
314
+ });
315
+ setSkip(channelMessages.length); // Update skip after fetching
316
+ setLoadingOldMessages(false);
317
+ if (!response?.data?.messages?.data) {
318
+ return { error: 'No messages returned' };
319
+ }
320
+ return { messages: response.data.messages.data };
321
+ } catch (error) {
322
+ setLoadingOldMessages(false);
323
+ setError(String(error));
324
+ return { error: String(error) };
325
+ }
326
+ }, [channelId, channelMessages.length, refetch]);
180
327
 
181
- React.useEffect(() => {
182
- if (selectedImage) setImageLoading(false);
183
- }, [selectedImage]);
328
+ // Send message function
329
+ const sendMessageImpl = useCallback(async () => {
330
+ try {
331
+ // Store the current message text and clear input immediately for better UX
332
+ const currentMessageText = messageText;
333
+ setMessageText('');
184
334
 
185
- useEffect(() => {
186
- if (data?.messages?.data) {
187
- const { data: messages, totalCount: messeageTotalCount } = data.messages;
188
- console.log('messeageTotalCount', messeageTotalCount, ' totalCount=', totalCount);
335
+ const notificationData: IExpoNotificationData = {
336
+ url: config.INBOX_MESSEGE_PATH,
337
+ params: { channelId, hideTabBar: true },
338
+ screen: 'DialogMessages',
339
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
340
+ };
189
341
 
190
- if (
191
- (messages && messages.length > 0 && messeageTotalCount > totalCount) ||
192
- (messages && messages.length > 0 && (loadingOldMessages || channelMessages.length === 0))
193
- ) {
194
- setChannelMessages((oldMessages: any) => uniqBy([...messages, ...oldMessages], ({ id }) => id));
195
- setTotalCount(messeageTotalCount);
342
+ // Create optimistic message with minimal structure required for UI rendering
343
+ const messageId = objectId();
344
+ const optimisticMessage = {
345
+ __typename: 'Post' as const,
346
+ id: messageId,
347
+ message: currentMessageText,
348
+ createdAt: new Date().toISOString(),
349
+ updatedAt: new Date().toISOString(),
350
+ author: {
351
+ __typename: 'UserAccount' as const,
352
+ id: auth?.id,
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[],
360
+ tokens: [],
361
+ },
362
+ isDelivered: true,
363
+ isRead: false,
364
+ type: 'TEXT' as PostTypeEnum,
365
+ parentId: null,
366
+ fromServer: false,
367
+ channel: {
368
+ __typename: 'Channel' as const,
369
+ id: channelId,
370
+ },
371
+ // Required fields that Apollo expects in the cache
372
+ propsConfiguration: {
373
+ __typename: 'MachineConfiguration' as const,
374
+ id: null,
375
+ resource: '' as any,
376
+ contents: null,
377
+ keys: null,
378
+ target: null,
379
+ overrides: null,
380
+ },
381
+ props: {},
382
+ files: {
383
+ __typename: 'FilesInfo' as const,
384
+ data: [],
385
+ totalCount: 0,
386
+ },
387
+ replies: {
388
+ __typename: 'Messages' as const,
389
+ data: [],
390
+ totalCount: 0,
391
+ },
392
+ };
393
+
394
+ const response = await sendMsg({
395
+ variables: {
396
+ channelId,
397
+ content: currentMessageText,
398
+ notificationParams: notificationData,
399
+ },
400
+ optimisticResponse: {
401
+ __typename: 'Mutation',
402
+ sendMessage: optimisticMessage,
403
+ },
404
+ // Let the type policies handle the cache update automatically
405
+ update: (cache, { data }) => {
406
+ // Only perform cache update if we have valid data
407
+ if (!data?.sendMessage) return;
408
+
409
+ try {
410
+ // Let Apollo type policies handle merging by using writeQuery
411
+ // This will trigger the merge functions in the type policies
412
+ cache.writeQuery({
413
+ query: MESSAGES_DOCUMENT,
414
+ variables: {
415
+ channelId: channelId?.toString(),
416
+ parentId: null,
417
+ limit: MESSAGES_PER_PAGE,
418
+ skip: 0,
419
+ },
420
+ data: {
421
+ messages: {
422
+ __typename: 'Messages',
423
+ messagesRefId: channelId,
424
+ data: [data.sendMessage],
425
+ totalCount: 1, // Just send the count for this single message
426
+ },
427
+ },
428
+ });
429
+ } catch (error) {
430
+ console.error('Error updating cache:', error);
431
+
432
+ // Format error for notification
433
+ let errorMsg = 'Failed to update message cache';
434
+ if (__DEV__ && error) {
435
+ // In development, show actual error
436
+ errorMsg = error.message
437
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
438
+ : 'Cache update failed';
439
+ }
440
+
441
+ setNotificationType('error');
442
+ setErrorMessage(errorMsg);
443
+ }
444
+ },
445
+ });
446
+
447
+ // Ensure loader is removed after sending
448
+ setIsUploadingImage(false);
449
+ setLoading(false);
450
+
451
+ return { message: response.data?.sendMessage };
452
+ } catch (error) {
453
+ setLoading(false);
454
+ setIsUploadingImage(false);
455
+
456
+ // Format error for notification
457
+ let errorMsg = 'Failed to send message';
458
+ if (__DEV__ && error) {
459
+ // In development, show actual error
460
+ errorMsg = error.message
461
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
462
+ : 'Message sending failed';
196
463
  }
197
464
 
198
- if (loadingOldMessages && channelMessages) setLoadingOldMessages(false);
465
+ setNotificationType('error');
466
+ setErrorMessage(errorMsg);
467
+ setError(String(error));
468
+ return { error: String(error) };
199
469
  }
200
- }, [data, loadingOldMessages, channelMessages, totalCount]);
470
+ }, [channelId, messageText, sendMsg, auth]);
201
471
 
202
- const onFetchOld = useCallback(async () => {
203
- if (totalCount > channelMessages.length && !loadingOldMessages) {
204
- setLoadEarlierMsg(true);
205
- try {
206
- const response = await fetchMoreMessages({
207
- variables: {
208
- channelId: channelId?.toString(),
209
- parentId: null,
210
- skip: channelMessages.length,
211
- },
472
+ // Image selection handler
473
+ const onSelectImages = async () => {
474
+ setLoading(true);
475
+
476
+ try {
477
+ let imageSource = await ImagePicker.launchImageLibraryAsync({
478
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
479
+ allowsEditing: false,
480
+ aspect: [4, 3],
481
+ quality: 0.8,
482
+ base64: true,
483
+ exif: false,
484
+ allowsMultipleSelection: true, // Enable multiple selection
485
+ });
486
+
487
+ if (!imageSource?.canceled) {
488
+ // Get all selected assets
489
+ const selectedAssets = imageSource?.assets || [];
490
+ if (selectedAssets.length === 0) {
491
+ setLoading(false);
492
+ return;
493
+ }
494
+
495
+ // Process all selected images
496
+ const newImages = selectedAssets.map((selectedAsset) => {
497
+ // Create a base64 image string for preview
498
+ const base64Data = selectedAsset.base64;
499
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
500
+
501
+ // Format the asset for upload service requirements
502
+ const asset: ExtendedImagePickerAsset = {
503
+ ...selectedAsset,
504
+ url: selectedAsset.uri,
505
+ fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
506
+ mimeType: 'image/jpeg',
507
+ };
508
+
509
+ return asset;
212
510
  });
213
- if (response?.data) {
214
- setSkip(channelMessages.length);
215
- setLoadEarlierMsg(false);
216
- setLoadingOldMessages(true);
511
+
512
+ // Set preview for the first image (for backward compatibility)
513
+ if (newImages.length > 0) {
514
+ const base64Data = newImages[0].base64;
515
+ const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : newImages[0].uri;
516
+ setSelectedImage(previewImage);
217
517
  }
218
- } catch (error: any) {
219
- setLoadEarlierMsg(false);
220
- }
221
518
 
222
- // ?.then((res: any) => {
223
- // setLoadingOldMessages(true);
224
- // });
519
+ // Add new images to existing ones
520
+ setImages((currentImages) => [...currentImages, ...newImages]);
521
+
522
+ // Show action sheet if it's not visible
523
+ if (!isActionSheetVisible) {
524
+ setActionSheetVisible(true);
525
+ }
526
+ } else {
527
+ setLoading(false);
528
+ }
529
+ } catch (error) {
530
+ setLoading(false);
225
531
  }
226
- }, [totalCount, channelMessages]);
532
+ };
227
533
 
228
- // const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
229
- // return contentOffset.y <= 100; // 100px from top
230
- // };
534
+ // Add a state variable to track which message should show the skeleton
535
+ const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
536
+ // Add new state for tracking pending uploads
537
+ const [pendingUploads, setPendingUploads] = useState<Record<string, IMessageProps>>({});
538
+ const [uploadErrors, setUploadErrors] = useState<Record<string, string>>({});
231
539
 
232
- const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
233
- const paddingToTop = 60;
234
- return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
235
- };
540
+ // Add new state variable to track image upload status
541
+ const [isUploadingImage, setIsUploadingImage] = useState(false);
236
542
 
237
- const dataURLtoFile = (dataurl: any, filename: any) => {
238
- var arr = dataurl.split(','),
239
- mime = arr[0].match(/:(.*?);/)[1],
240
- bstr = atob(arr[1]),
241
- n = bstr.length,
242
- u8arr = new Uint8Array(n);
243
- while (n--) {
244
- u8arr[n] = bstr.charCodeAt(n);
543
+ // Ensure loader is hidden when all images are removed
544
+ useEffect(() => {
545
+ if (images.length === 0) {
546
+ setIsUploadingImage(false);
245
547
  }
246
- return new File([u8arr], filename, { type: mime });
247
- };
548
+ }, [images]);
248
549
 
249
- const onSelectImages = async () => {
250
- setImageLoading(true);
251
- let imageSource: any = await ImagePicker.launchImageLibraryAsync({
252
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
253
- allowsEditing: true,
254
- aspect: [4, 3],
255
- quality: 1,
256
- base64: true,
550
+ // Add toast hook for notifications
551
+ const toast = useToast();
552
+
553
+ // Add a helper function for removing messages from the UI when uploads fail
554
+ const removeMessageFromUI = useCallback((messageId: string) => {
555
+ // Remove from pending uploads
556
+ setPendingUploads((prev) => {
557
+ const newPending = { ...prev };
558
+ delete newPending[messageId];
559
+ return newPending;
257
560
  });
258
- // if (!imageSource.cancelled) {
259
- // const image = 'data:image/jpeg;base64,' + imageSource?.base64;
260
- // setImage(image);
261
- // const file = dataURLtoFile(image, 'inputImage.jpg');
262
- // setFiles((files) => files.concat(file));
263
- // setImages((images) => images.concat(imageSource as ImagePicker.ImageInfo));
264
- // }
265
- // if (imageSource.cancelled) setLoading(false);
266
-
267
- if (!imageSource.canceled) {
268
- const image = 'data:image/jpeg;base64,' + imageSource?.assets[0]?.base64;
269
- setImage(image);
270
- const file = dataURLtoFile(image, 'inputImage.jpg');
271
- setFiles((files) => files.concat(file));
272
- setImages((images) => images.concat(imageSource?.assets[0] as ImagePicker.ImagePickerAsset));
273
- }
274
561
 
275
- if (imageSource.canceled) setLoading(false);
276
- };
562
+ // Also remove any error state
563
+ setUploadErrors((prev) => {
564
+ const newErrors = { ...prev };
565
+ delete newErrors[messageId];
566
+ return newErrors;
567
+ });
277
568
 
278
- const createDirectChannel = useCallback(
279
- (msg: string) => {
280
- if (
281
- rest?.isCreateNewChannel &&
282
- rest?.newChannelData?.type === RoomType?.Direct &&
283
- rest?.newChannelData?.userIds?.length > 0
284
- ) {
285
- addDirectChannel({
286
- variables: {
287
- receiver: [...(rest?.newChannelData?.userIds ?? [])],
288
- displayName: 'DIRECT CHANNEL',
289
- },
290
- })
291
- ?.then(async (res) => {
292
- if (res?.data?.createDirectChannel?.id) {
293
- setChannelId(res?.data?.createDirectChannel?.id);
294
- const notificationData: IExpoNotificationData = {
295
- url: config.INBOX_MESSEGE_PATH,
296
- params: { channelId: res?.data?.createDirectChannel?.id, hideTabBar: true },
297
- screen: 'DialogMessages',
298
- other: { sound: Platform.OS === 'android' ? undefined : 'default' },
299
- };
300
- setLoading(true);
301
- await sendMsg({
302
- variables: {
303
- channelId: res?.data?.createDirectChannel?.id,
304
- content: msg,
305
- notificationParams: notificationData,
306
- },
307
- update: (cache, { data, errors }: any) => {
308
- if (!data || errors) {
309
- setLoading(false);
310
- return;
311
- }
312
- setChannelToTop(channelToTop + 1);
313
- setLoading(false);
314
- setMsg('');
315
- },
316
- });
317
- }
318
- })
319
- ?.catch((e: any) => console.log('error', JSON.stringify(e)));
569
+ // Reset upload state to ensure we don't get stuck with loading indicator
570
+ setIsUploadingImage(false);
571
+ }, []);
572
+
573
+ // Send message with file - fix to ensure images display without loading indicators
574
+ const sendMessageWithFileImpl = useCallback(async () => {
575
+ try {
576
+ // Generate a unique ID for the message
577
+ const postId = objectId();
578
+
579
+ // Set uploading state to true
580
+ setIsUploadingImage(true);
581
+
582
+ // Clear all loading states immediately
583
+ setLoading(false);
584
+ setUploadingMessageId(null);
585
+
586
+ // Safety check for images
587
+ if (!images || images.length === 0) {
588
+ setIsUploadingImage(false);
589
+ setLoading(false);
590
+ return { error: 'No images available to upload' };
320
591
  }
321
- },
322
- [rest],
323
- );
324
592
 
325
- const handleSend = useCallback(
326
- async (message: string) => {
327
- if (!channelId) return;
328
- if (!message && message != ' ' && images.length == 0) return;
593
+ // Store current values before clearing
594
+ const currentMessageText = messageText;
595
+ const currentImages = [...images];
596
+
597
+ // Prepare image URIs for optimistic UI update
598
+ const imageUris = currentImages.map((img) => img.uri || img.url);
599
+
600
+ // Clear UI immediately for next message
601
+ setMessageText('');
602
+ setSelectedImage('');
603
+ setImages([]);
604
+
605
+ // Create a client message with all local image URIs
606
+ const clientMessage: IMessageProps = {
607
+ _id: postId,
608
+ text: currentMessageText || ' ',
609
+ createdAt: new Date(),
610
+ user: {
611
+ _id: auth?.id || '',
612
+ name: `${auth?.givenName || ''} ${auth?.familyName || ''}`,
613
+ avatar: auth?.picture || '',
614
+ },
615
+ image: imageUris[0], // First image for compatibility with GiftedChat
616
+ images: imageUris, // All images for our custom renderer
617
+ sent: true,
618
+ received: true,
619
+ pending: false,
620
+ type: 'TEXT',
621
+ replies: { data: [], totalCount: 0 },
622
+ isShowThreadMessage: false,
623
+ };
329
624
 
625
+ // Add to displayed messages immediately
626
+ setPendingUploads((prev) => ({ ...prev, [postId]: clientMessage }));
627
+
628
+ // Prepare notification data
330
629
  const notificationData: IExpoNotificationData = {
331
630
  url: config.INBOX_MESSEGE_PATH,
332
631
  params: { channelId, hideTabBar: true },
@@ -334,366 +633,1017 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
334
633
  other: { sound: Platform.OS === 'android' ? undefined : 'default' },
335
634
  };
336
635
 
337
- if (images && images.length > 0) {
338
- const postId = objectId();
636
+ // Create optimistic message with minimal structure required for UI rendering
637
+ const optimisticMessage = {
638
+ __typename: 'Post' as const,
639
+ id: postId,
640
+ message: currentMessageText || ' ',
641
+ createdAt: new Date().toISOString(),
642
+ updatedAt: new Date().toISOString(),
643
+ author: {
644
+ __typename: 'UserAccount' as const,
645
+ id: auth?.id,
646
+ givenName: auth?.profile?.given_name || '',
647
+ familyName: auth?.profile?.family_name || '',
648
+ email: auth?.profile?.email || '',
649
+ username: auth?.profile?.nickname || '',
650
+ fullName: auth?.profile?.name || '',
651
+ picture: auth?.profile?.picture || '',
652
+ alias: [auth?.authUserId ?? ''] as string[],
653
+ tokens: [],
654
+ },
655
+ isDelivered: true,
656
+ isRead: false,
657
+ type: 'TEXT' as PostTypeEnum,
658
+ parentId: null,
659
+ fromServer: false,
660
+ channel: {
661
+ __typename: 'Channel' as const,
662
+ id: channelId,
663
+ },
664
+ // Required fields that Apollo expects in the cache
665
+ propsConfiguration: {
666
+ __typename: 'MachineConfiguration' as const,
667
+ id: null,
668
+ resource: '' as any,
669
+ contents: null,
670
+ keys: null,
671
+ target: null,
672
+ overrides: null,
673
+ },
674
+ props: {},
675
+ files: {
676
+ __typename: 'FilesInfo' as const,
677
+ data: imageUris.map((uri, index) => ({
678
+ __typename: 'FileInfo' as const,
679
+ id: `temp-file-${index}-${postId}`,
680
+ url: uri,
681
+ name: `image-${index}.jpg`,
682
+ extension: 'jpg',
683
+ mimeType: 'image/jpeg',
684
+ size: 0,
685
+ height: 300,
686
+ width: 300,
687
+ channel: null,
688
+ post: null,
689
+ refType: FileRefType.Post,
690
+ })),
691
+ totalCount: imageUris.length,
692
+ },
693
+ replies: {
694
+ __typename: 'Messages' as const,
695
+ data: [],
696
+ totalCount: 0,
697
+ },
698
+ };
339
699
 
340
- setLoading(true);
341
- const uploadResponse = await startUpload({
342
- file: images,
343
- saveUploadedFile: {
344
- variables: {
345
- postId,
346
- },
347
- },
348
- createUploadLink: {
349
- variables: {
350
- postId,
351
- },
352
- },
353
- });
354
- if (uploadResponse?.error) setLoading(false);
355
- const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
356
-
357
- if (uploadResponse.data) {
358
- setImage('');
359
- setFiles([]);
360
- setImages([]);
361
- //setLoading(false);
362
- const files = uploadedFiles?.map((f: any) => f.id) ?? null;
363
- await sendMsg({
364
- variables: {
365
- postId,
366
- channelId,
367
- content: message,
368
- files,
369
- notificationParams: notificationData,
700
+ // Start background processing without affecting UI
701
+ setTimeout(async () => {
702
+ try {
703
+ // Format images for upload
704
+ const imagesToUpload = currentImages.map((img) => ({
705
+ ...img,
706
+ uri: img.uri || img.url,
707
+ type: img.mimeType || 'image/jpeg',
708
+ name: img.fileName || `image_${Date.now()}.jpg`,
709
+ }));
710
+
711
+ // Upload the files in background - pass the array of images
712
+ const uploadResponse = await startUpload({
713
+ file: imagesToUpload,
714
+ saveUploadedFile: {
715
+ variables: { postId },
370
716
  },
371
- update: (cache, { data, errors }: any) => {
372
- if (!data || errors) {
373
- setLoading(false);
374
- return;
375
- }
376
- //Temporary fix.....//
377
- const newMessage: any = data?.sendMessage;
378
- setChannelMessages((oldMessages: any) =>
379
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
380
- );
381
- setTotalCount((t) => t + 1);
382
- //Temporary fix.....//
383
-
384
- setChannelToTop(channelToTop + 1);
385
- setLoading(false);
386
- setMsg('');
717
+ createUploadLink: {
718
+ variables: { postId },
387
719
  },
388
720
  });
389
- }
390
- } else {
391
- setLoading(true);
392
- await sendMsg({
393
- variables: {
394
- channelId,
395
- content: message,
396
- notificationParams: notificationData,
397
- },
398
- update: (cache, { data, errors }: any) => {
399
- if (!data || errors) {
400
- setLoading(false);
401
- return;
721
+
722
+ // If upload fails, show error notification
723
+ if (uploadResponse?.error) {
724
+ console.error('Upload error:', uploadResponse.error);
725
+
726
+ // Format error message
727
+ let errorMsg = 'Failed to upload image. Please try again.';
728
+ if (__DEV__ && uploadResponse.error) {
729
+ // In development, show actual error
730
+ errorMsg =
731
+ typeof uploadResponse.error === 'string'
732
+ ? uploadResponse.error
733
+ : uploadResponse.error.message || errorMsg;
402
734
  }
403
- //Temporary fix.....//
404
- const newMessage: any = data?.sendMessage;
405
- setChannelMessages((oldMessages: any) => uniqBy([...oldMessages, newMessage], ({ id }) => id));
406
- setTotalCount((t) => t + 1);
407
- //Temporary fix.....//
408
735
 
409
- setChannelToTop(channelToTop + 1);
736
+ // Show error notification
737
+ setNotificationType('error');
738
+ setErrorMessage(errorMsg);
739
+
740
+ // Store error in state
741
+ setUploadErrors((prev) => ({ ...prev, [postId]: errorMsg }));
742
+
743
+ // Remove the message from UI
744
+ removeMessageFromUI(postId);
745
+ setIsUploadingImage(false);
410
746
  setLoading(false);
411
- setMsg('');
412
- },
413
- });
747
+ return;
748
+ }
749
+
750
+ // Get uploaded file info
751
+ const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
752
+ const fileIds = uploadedFiles?.map((f: any) => f.id) ?? null;
753
+
754
+ // Send the message with uploaded files
755
+ if (fileIds?.length > 0) {
756
+ await sendMsg({
757
+ variables: {
758
+ postId,
759
+ channelId,
760
+ content: currentMessageText || ' ',
761
+ files: fileIds,
762
+ notificationParams: notificationData,
763
+ },
764
+ optimisticResponse: {
765
+ __typename: 'Mutation',
766
+ sendMessage: optimisticMessage,
767
+ },
768
+ update: (cache, { data }) => {
769
+ if (!data?.sendMessage) {
770
+ setIsUploadingImage(false);
771
+ setLoading(false);
772
+ return;
773
+ }
774
+ try {
775
+ // Let Apollo type policies handle the cache update
776
+ cache.writeQuery({
777
+ query: MESSAGES_DOCUMENT,
778
+ variables: {
779
+ channelId: channelId?.toString(),
780
+ parentId: null,
781
+ limit: MESSAGES_PER_PAGE,
782
+ skip: 0,
783
+ },
784
+ data: {
785
+ messages: {
786
+ __typename: 'Messages',
787
+ messagesRefId: channelId,
788
+ data: [data.sendMessage],
789
+ totalCount: 1, // Just one message
790
+ },
791
+ },
792
+ });
793
+
794
+ // Check if the server response has the actual image
795
+ const serverMessage = data.sendMessage;
796
+ const hasServerImage = serverMessage?.files?.data?.some((file) => file.url);
797
+
798
+ if (hasServerImage) {
799
+ // Now that server has the image, we can remove client version
800
+ removeMessageFromUI(postId);
801
+ }
802
+
803
+ setIsUploadingImage(false);
804
+ setLoading(false);
805
+ } catch (error) {
806
+ console.error('Cache update error:', error);
807
+
808
+ // Format error for notification
809
+ let errorMsg = 'Failed to update message.';
810
+ if (__DEV__ && error) {
811
+ // In development, show actual error
812
+ errorMsg = error.message
813
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
814
+ : 'Cache update failed';
815
+ }
816
+
817
+ setNotificationType('error');
818
+ setErrorMessage(errorMsg);
819
+ setIsUploadingImage(false);
820
+ setLoading(false);
821
+ }
822
+ },
823
+ });
824
+ } else {
825
+ setIsUploadingImage(false);
826
+ setLoading(false);
827
+ }
828
+ } catch (error) {
829
+ console.error('Background process error:', error);
830
+
831
+ // Format error for notification
832
+ let errorMsg = 'Failed to send image. Please try again.';
833
+ if (__DEV__ && error) {
834
+ // In development, show actual error
835
+ errorMsg = error.message
836
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
837
+ : 'Background process failed';
838
+ }
839
+
840
+ // Show error notification
841
+ setNotificationType('error');
842
+ setErrorMessage(errorMsg);
843
+ removeMessageFromUI(postId);
844
+ setIsUploadingImage(false);
845
+ setLoading(false);
846
+ }
847
+ }, 0);
848
+
849
+ // Return success immediately - UI already updated
850
+ return { success: true };
851
+ } catch (error) {
852
+ console.error('Send message error:', error);
853
+
854
+ // Format error for notification
855
+ let errorMsg = 'Failed to process image. Please try again.';
856
+ if (__DEV__ && error) {
857
+ // In development, show actual error
858
+ errorMsg = error.message
859
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
860
+ : 'Image processing failed';
414
861
  }
415
- },
416
- [setChannelMessages, channelId, images, channelToTop, expoTokens],
417
- );
418
862
 
863
+ // Show error notification
864
+ setNotificationType('error');
865
+ setErrorMessage(errorMsg);
866
+ setError(String(error));
867
+ setIsUploadingImage(false);
868
+ setLoading(false);
869
+ return { error: String(error) };
870
+ }
871
+ }, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth, removeMessageFromUI]);
872
+
873
+ // Create direct channel implementation
874
+ const createDirectChannelImpl = useCallback(async () => {
875
+ try {
876
+ setLoading(true);
877
+ if (
878
+ !rest?.isCreateNewChannel ||
879
+ rest?.newChannelData?.type !== RoomType?.Direct ||
880
+ !rest?.newChannelData?.userIds?.length
881
+ ) {
882
+ setLoading(false);
883
+ setNotificationType('error');
884
+ setErrorMessage(__DEV__ ? 'Invalid channel data' : 'Unable to create conversation');
885
+ return { error: 'Invalid channel data' };
886
+ }
887
+
888
+ // Store current message text
889
+ const currentMessageText = messageText;
890
+ // Clear message text immediately for better UX
891
+ setMessageText('');
892
+
893
+ const response = await addDirectChannel({
894
+ variables: {
895
+ receiver: [...(rest?.newChannelData?.userIds ?? [])],
896
+ displayName: 'DIRECT CHANNEL',
897
+ },
898
+ });
899
+
900
+ if (!response?.data?.createDirectChannel?.id) {
901
+ setLoading(false);
902
+ setNotificationType('error');
903
+ setErrorMessage(__DEV__ ? 'Failed to create channel' : 'Unable to create conversation');
904
+ return { error: 'Failed to create channel' };
905
+ }
906
+
907
+ const newChannelId = response.data.createDirectChannel.id;
908
+ setChannelId(newChannelId);
909
+
910
+ const notificationData: IExpoNotificationData = {
911
+ url: config.INBOX_MESSEGE_PATH,
912
+ params: { channelId: newChannelId, hideTabBar: true },
913
+ screen: 'DialogMessages',
914
+ other: { sound: Platform.OS === 'android' ? undefined : 'default' },
915
+ };
916
+
917
+ // Create unique message ID for optimistic response
918
+ const messageId = objectId();
919
+
920
+ // Create minimal optimistic message
921
+ const optimisticMessage = {
922
+ __typename: 'Post' as const,
923
+ id: messageId,
924
+ message: currentMessageText,
925
+ createdAt: new Date().toISOString(),
926
+ updatedAt: new Date().toISOString(),
927
+ author: {
928
+ __typename: 'UserAccount' as const,
929
+ id: auth?.id,
930
+ picture: auth?.picture || '',
931
+ givenName: auth?.givenName || '',
932
+ familyName: auth?.familyName || '',
933
+ email: auth?.email || '',
934
+ username: auth?.username || '',
935
+ alias: [] as string[],
936
+ tokens: auth?.token ? [...auth?.token] : [],
937
+ },
938
+ isDelivered: true,
939
+ isRead: false,
940
+ type: 'TEXT' as PostTypeEnum,
941
+ parentId: null,
942
+ fromServer: false,
943
+ channel: {
944
+ __typename: 'Channel' as const,
945
+ id: newChannelId,
946
+ },
947
+ // Required fields that Apollo expects in the cache
948
+ propsConfiguration: {
949
+ __typename: 'MachineConfiguration' as const,
950
+ id: null,
951
+ resource: '' as any,
952
+ contents: null,
953
+ keys: null,
954
+ target: null,
955
+ overrides: null,
956
+ },
957
+ props: {},
958
+ files: {
959
+ __typename: 'FilesInfo' as const,
960
+ data: [],
961
+ totalCount: 0,
962
+ },
963
+ replies: {
964
+ __typename: 'Messages' as const,
965
+ data: [],
966
+ totalCount: 0,
967
+ },
968
+ };
969
+
970
+ // Send message in the new channel
971
+ await sendMsg({
972
+ variables: {
973
+ channelId: newChannelId,
974
+ content: currentMessageText,
975
+ notificationParams: notificationData,
976
+ },
977
+ optimisticResponse: {
978
+ __typename: 'Mutation',
979
+ sendMessage: optimisticMessage,
980
+ },
981
+ update: (cache, { data }) => {
982
+ if (!data?.sendMessage) return;
983
+
984
+ try {
985
+ // For a new channel, simply write the initial message to the cache
986
+ // The type policies will handle it properly
987
+ cache.writeQuery({
988
+ query: MESSAGES_DOCUMENT,
989
+ variables: {
990
+ channelId: newChannelId,
991
+ parentId: null,
992
+ limit: MESSAGES_PER_PAGE,
993
+ skip: 0,
994
+ },
995
+ data: {
996
+ messages: {
997
+ __typename: 'Messages',
998
+ messagesRefId: newChannelId,
999
+ data: [data.sendMessage],
1000
+ totalCount: 1,
1001
+ },
1002
+ },
1003
+ });
1004
+ } catch (error) {
1005
+ console.error('Error updating cache:', error);
1006
+
1007
+ // Format error for notification
1008
+ let errorMsg = 'Failed to update message cache';
1009
+ if (__DEV__ && error) {
1010
+ // In development, show actual error
1011
+ errorMsg = error.message
1012
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
1013
+ : 'Cache update failed';
1014
+ }
1015
+
1016
+ setNotificationType('error');
1017
+ setErrorMessage(errorMsg);
1018
+ }
1019
+ },
1020
+ });
1021
+
1022
+ setLoading(false);
1023
+ return { channelId: newChannelId };
1024
+ } catch (error) {
1025
+ setLoading(false);
1026
+
1027
+ // Format error for notification
1028
+ let errorMsg = 'Failed to create conversation';
1029
+ if (__DEV__ && error) {
1030
+ // In development, show actual error
1031
+ errorMsg = error.message
1032
+ ? error.message.replace('[ApolloError: ', '').replace(']', '')
1033
+ : 'Channel creation failed';
1034
+ }
1035
+
1036
+ setNotificationType('error');
1037
+ setErrorMessage(errorMsg);
1038
+ setError(String(error));
1039
+ return { error: String(error) };
1040
+ }
1041
+ }, [rest, messageText, addDirectChannel, sendMsg, auth]);
1042
+
1043
+ // Optimize onFetchOld by adding debounce logic
1044
+ const onFetchOld = useCallback(() => {
1045
+ // Prevent multiple rapid calls
1046
+ if (fetchOldDebounceRef.current) return;
1047
+
1048
+ // Check if we need to fetch more messages
1049
+ if (totalCount > channelMessages.length && !loadingOldMessages) {
1050
+ // Set debounce
1051
+ fetchOldDebounceRef.current = true;
1052
+
1053
+ // Fetch more messages
1054
+ fetchMoreMessagesImpl();
1055
+
1056
+ // Clear debounce after a timeout
1057
+ setTimeout(() => {
1058
+ fetchOldDebounceRef.current = false;
1059
+ }, 1000);
1060
+ }
1061
+ }, [totalCount, channelMessages.length, loadingOldMessages, fetchMoreMessagesImpl]);
1062
+
1063
+ const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
1064
+ const paddingToTop = 60;
1065
+ return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
1066
+ };
1067
+
1068
+ // Modify the messageList function to ensure local images take precedence
419
1069
  const messageList = useMemo(() => {
420
- let currentDate = '';
421
- let res: any = [];
422
- const filteredMessages =
423
- channelMessages && channelMessages?.length > 0 ? uniqBy([...channelMessages], ({ id }: any) => id) : [];
424
- if (channelId && filteredMessages?.length > 0) {
425
- orderBy(channelMessages, ['createdAt'], ['desc']).map((msg) => {
426
- let message: IMessageProps = {
427
- _id: '',
428
- text: '',
429
- createdAt: 0,
1070
+ // Get pending upload messages as array
1071
+ const pendingMessages = Object.values(pendingUploads);
1072
+
1073
+ // If we have no server messages, just return pending messages
1074
+ if (!channelMessages || channelMessages.length === 0) {
1075
+ return pendingMessages;
1076
+ }
1077
+
1078
+ // Filter unique messages
1079
+ const filteredMessages = uniqBy(channelMessages, ({ id }) => id);
1080
+
1081
+ // Process server messages - skip any that have client versions
1082
+ const serverMessages = orderBy(filteredMessages, ['createdAt'], ['desc'])
1083
+ .map((msg) => {
1084
+ const date = new Date(msg.createdAt);
1085
+
1086
+ // Skip messages that are in pendingUploads - client version takes precedence
1087
+ if (pendingUploads[msg.id]) {
1088
+ return null;
1089
+ }
1090
+
1091
+ // Extract image URLs from files data
1092
+ let imageUrls: string[] = [];
1093
+ let primaryImageUrl = null;
1094
+
1095
+ if (msg.files && typeof msg.files === 'object') {
1096
+ const filesData = msg.files.data || (Array.isArray(msg.files) ? msg.files : null);
1097
+
1098
+ if (filesData && filesData.length > 0) {
1099
+ // Collect all image URLs
1100
+ imageUrls = filesData
1101
+ .filter((fileData) => fileData && typeof fileData === 'object' && fileData.url)
1102
+ .map((fileData) => fileData.url);
1103
+
1104
+ // Set primary image for GiftedChat compatibility
1105
+ if (imageUrls.length > 0) {
1106
+ primaryImageUrl = imageUrls[0];
1107
+ }
1108
+ }
1109
+ }
1110
+
1111
+ // Create formatted message
1112
+ return {
1113
+ _id: msg.id,
1114
+ text: msg.message,
1115
+ createdAt: date,
430
1116
  user: {
431
- _id: '',
432
- name: '',
433
- avatar: '',
1117
+ _id: msg.author?.id || '',
1118
+ name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
1119
+ avatar: msg.author?.picture || '',
434
1120
  },
435
- type: '',
1121
+ image: primaryImageUrl,
1122
+ images: imageUrls, // Store all images for custom rendering
1123
+ sent: msg?.isDelivered,
1124
+ received: msg?.isRead,
1125
+ type: msg?.type,
1126
+ propsConfiguration: msg?.propsConfiguration,
1127
+ replies: msg?.replies ?? [],
1128
+ isShowThreadMessage,
436
1129
  };
437
- const date = new Date(msg.createdAt);
438
- message._id = msg.id;
439
- message.text = msg.message;
440
- message.createdAt = date;
441
- (message.user = {
442
- _id: msg.author.id,
443
- name: msg.author.givenName + ' ' + msg.author.familyName,
444
- avatar: msg.author?.picture,
445
- }),
446
- (message.image = msg.files?.data[0]?.url),
447
- (message.sent = msg?.isDelivered),
448
- (message.received = msg?.isRead);
449
- message.type = msg?.type;
450
- message.propsConfiguration = msg?.propsConfiguration;
451
- message.replies = msg?.replies ?? [];
452
- message.isShowThreadMessage = isShowThreadMessage;
453
- res.push(message);
454
- });
1130
+ })
1131
+ .filter(Boolean); // Remove null entries
1132
+
1133
+ // Pending messages take precedence (they have local images)
1134
+ return [...pendingMessages, ...serverMessages];
1135
+ }, [channelMessages, pendingUploads, isShowThreadMessage]);
1136
+
1137
+ // Render the send button
1138
+ const renderSend = useCallback(
1139
+ (props) => {
1140
+ // If action sheet is visible, don't show the default send button
1141
+ // if (isActionSheetVisible) {
1142
+ // return null;
1143
+ // }
1144
+
1145
+ // Enable the send button if there's text OR we have images
1146
+ const hasContent = !!props.text || images?.length > 0;
1147
+ const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
1148
+ // const isDisabled = !canSend || isUploadingImage || loading;
1149
+ const isDisabled = !canSend;
1150
+
1151
+ return (
1152
+ <Send
1153
+ {...props}
1154
+ //disabled={isDisabled}
1155
+ containerStyle={{
1156
+ justifyContent: 'center',
1157
+ alignItems: 'center',
1158
+ height: 40,
1159
+ width: 44,
1160
+ marginRight: 4,
1161
+ marginBottom: 0,
1162
+ marginLeft: 4,
1163
+ }}
1164
+ >
1165
+ <View style={{ padding: 4 }}>
1166
+ <MaterialCommunityIcons
1167
+ name="send-circle"
1168
+ size={32}
1169
+ color={isDisabled ? colors.gray[400] : colors.blue[500]}
1170
+ />
1171
+ </View>
1172
+ </Send>
1173
+ );
1174
+ },
1175
+ [channelId, images, rest?.isCreateNewChannel, isUploadingImage, loading, isActionSheetVisible],
1176
+ );
1177
+
1178
+ // Add new handler to open the action sheet
1179
+ const openExpandableInput = useCallback(() => {
1180
+ console.log('Opening action sheet');
1181
+ setActionSheetVisible(true);
1182
+ }, []);
1183
+
1184
+ // Add a debug useEffect to log when visibility changes
1185
+ useEffect(() => {
1186
+ console.log('Action sheet visibility:', isActionSheetVisible);
1187
+ // Set appropriate bottom margin when action sheet visibility changes
1188
+ if (isActionSheetVisible) {
1189
+ setBottomMargin(0);
455
1190
  }
456
- return res?.length > 0 ? uniqBy([...res], ({ _id }: any) => _id) : [];
457
- //return res;
458
- }, [channelMessages, channelId]);
1191
+ }, [isActionSheetVisible]);
459
1192
 
460
- const renderSend = (props) => {
461
- return (
462
- <Send {...props} disabled={channelId || rest?.isCreateNewChannel ? false : true}>
463
- <Box>
464
- <MaterialCommunityIcons
465
- name="send-circle"
466
- style={{ marginBottom: 5, marginRight: 5 }}
467
- size={32}
468
- color={channelId || rest?.isCreateNewChannel ? '#2e64e5' : '#b8b2b2'}
469
- />
470
- </Box>
471
- </Send>
472
- );
1193
+ // Handle removing image from action sheet
1194
+ const handleRemoveImage = useCallback(
1195
+ (index: number) => {
1196
+ const newImages = [...images];
1197
+ newImages.splice(index, 1);
1198
+ setImages(newImages);
1199
+ if (newImages.length === 0) {
1200
+ setSelectedImage('');
1201
+ if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
1202
+ textInputRef.current.focus();
1203
+ }
1204
+ }
1205
+ },
1206
+ [images],
1207
+ );
1208
+
1209
+ // Add a new state to track when the action sheet updates text
1210
+ const [textUpdatedInActionSheet, setTextUpdatedInActionSheet] = useState(false);
1211
+
1212
+ // Handle when the action sheet is closed
1213
+ const handleActionSheetClose = useCallback(() => {
1214
+ // Mark that we closed the sheet with potential text update
1215
+ setTextUpdatedInActionSheet(true);
1216
+ setActionSheetVisible(false);
1217
+ // Reset bottom margin to 0 when closing the expandable input
1218
+ setBottomMargin(0);
1219
+ }, []);
1220
+
1221
+ // Handle sending from action sheet
1222
+ const handleActionSheetSend = () => {
1223
+ if (messageText.trim() || images.length > 0) {
1224
+ // Set uploading state to show spinner
1225
+ setIsUploadingImage(true);
1226
+
1227
+ // Create a message object in the format GiftedChat expects
1228
+ const messages = [
1229
+ {
1230
+ text: messageText,
1231
+ user: {
1232
+ _id: auth?.id || '',
1233
+ },
1234
+ createdAt: new Date(),
1235
+ },
1236
+ ];
1237
+
1238
+ // Use the existing handleSend function
1239
+ handleSend(messages);
1240
+
1241
+ // Close the action sheet
1242
+ setActionSheetVisible(false);
1243
+ }
473
1244
  };
474
1245
 
475
- const renderMessageText = (props: any) => {
476
- const { currentMessage } = props;
477
- const lastReply: any = currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
478
-
479
- if (currentMessage.type === 'ALERT') {
480
- const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
481
- let action: string = '';
482
- let actionId: any = '';
483
- let params: any = {};
484
-
485
- if (attachment?.callToAction?.extraParams) {
486
- const extraParams: any = attachment?.callToAction?.extraParams;
487
- const route: any = extraParams?.route ?? null;
488
- let path: any = null;
489
- let param: any = null;
490
- if (role && role == PreDefinedRole.Guest) {
491
- path = route?.guest?.name ? route?.guest?.name ?? null : null;
492
- param = route?.guest?.params ? route?.guest?.params ?? null : null;
493
- } else if (role && role == PreDefinedRole.Owner) {
494
- path = route?.host?.name ? route?.host?.name ?? null : null;
495
- param = route?.host?.params ? route?.host?.params ?? null : null;
496
- } else {
497
- path = route?.host?.name ? route?.host?.name ?? null : null;
498
- param = route?.host?.params ? route?.host?.params ?? null : null;
1246
+ // Update this useEffect to more reliably handle text syncing when action sheet closes
1247
+ useEffect(() => {
1248
+ // If action sheet just closed, ensure main input gets updated text
1249
+ if (!isActionSheetVisible && textUpdatedInActionSheet) {
1250
+ console.log('Action sheet closed with text:', messageText);
1251
+ // Reset the flag
1252
+ setTextUpdatedInActionSheet(false);
1253
+
1254
+ // Force GiftedChat to recognize the text change by creating a new state update
1255
+ const currentText = messageText;
1256
+ setMessageText('');
1257
+ setTimeout(() => {
1258
+ setMessageText(currentText);
1259
+ }, 50);
1260
+ }
1261
+ }, [isActionSheetVisible, textUpdatedInActionSheet]);
1262
+
1263
+ // Take a screenshot of the action sheet for debugging
1264
+ useEffect(() => {
1265
+ if (isActionSheetVisible && Platform.OS === 'ios') {
1266
+ console.log('Action sheet is visible, should show the input and options');
1267
+ }
1268
+ }, [isActionSheetVisible]);
1269
+
1270
+ // Handle send for messages
1271
+ const handleSend = useCallback(
1272
+ async (messages) => {
1273
+ // Extract message text from GiftedChat messages array
1274
+ const newMessageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
1275
+
1276
+ // Check if we can send a message (channel exists or we're creating one)
1277
+ if (!channelId && !rest?.isCreateNewChannel) {
1278
+ return;
1279
+ }
1280
+
1281
+ // Allow sending if we have text OR images (image-only messages are valid)
1282
+ const hasText = !!newMessageText && newMessageText !== ' ';
1283
+ const hasImages = images && images.length > 0;
1284
+
1285
+ if (!hasText && !hasImages) {
1286
+ return;
1287
+ }
1288
+
1289
+ // Update the message text state - now handled in send functions for better UX
1290
+ setMessageText(newMessageText);
1291
+
1292
+ // Set uploading state to show spinner
1293
+ // setIsUploadingImage(true);
1294
+ // setLoading(true);
1295
+
1296
+ // Handle direct channel creation if needed
1297
+ if (rest?.isCreateNewChannel && !channelId) {
1298
+ if (rest?.newChannelData?.type === RoomType?.Direct) {
1299
+ await createDirectChannelImpl();
499
1300
  }
1301
+ setIsUploadingImage(false);
1302
+ setLoading(false);
1303
+ return;
1304
+ }
500
1305
 
501
- action = path;
502
- params = { ...param };
503
- } else if (attachment?.callToAction?.link) {
504
- action = CALL_TO_ACTION_PATH;
505
- actionId = attachment?.callToAction?.link.split('/').pop();
506
- params = { reservationId: actionId };
1306
+ // Send message with or without image based on state
1307
+ if (hasImages) {
1308
+ await sendMessageWithFileImpl();
1309
+ } else {
1310
+ await sendMessageImpl();
507
1311
  }
508
1312
 
509
- return (
510
- <>
511
- {attachment?.callToAction && action ? (
512
- <Box bg={CALL_TO_ACTION_BOX_BGCOLOR} borderRadius={15} pb={'$2'}>
513
- <Button
514
- variant={'outline'}
515
- size={'sm'}
516
- borderColor={CALL_TO_ACTION_BUTTON_BORDERCOLOR}
517
- onPress={() => action && params && navigation.navigate(action, params)}
518
- // onPress={() => navigation.navigate(action, { reservationId: actionId })}
519
- >
520
- <ButtonText color={CALL_TO_ACTION_TEXT_COLOR}>
521
- {attachment.callToAction.title}
522
- </ButtonText>
523
- </Button>
524
- <MessageText
525
- {...props}
526
- textStyle={{
527
- left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
1313
+ setIsUploadingImage(false);
1314
+ setLoading(false);
1315
+
1316
+ // Focus the input field after sending
1317
+ setTimeout(() => {
1318
+ if (textInputRef.current) {
1319
+ textInputRef.current.focus();
1320
+ }
1321
+ }, 100);
1322
+ },
1323
+ [
1324
+ channelId,
1325
+ images,
1326
+ rest?.isCreateNewChannel,
1327
+ rest?.newChannelData?.type,
1328
+ createDirectChannelImpl,
1329
+ sendMessageWithFileImpl,
1330
+ sendMessageImpl,
1331
+ ],
1332
+ );
1333
+
1334
+ // Render message text with customizations for alerts and replies
1335
+ const renderMessageText = useCallback(
1336
+ (props: any) => {
1337
+ const { currentMessage } = props;
1338
+ const lastReply: any =
1339
+ currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
1340
+
1341
+ // Do not render anything if the message text is empty or only whitespace
1342
+ if (!currentMessage?.text || currentMessage.text.trim() === '') {
1343
+ return null;
1344
+ }
1345
+
1346
+ if (currentMessage.type === 'ALERT') {
1347
+ const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
1348
+ let action: string = '';
1349
+ let actionId: any = '';
1350
+ let params: any = {};
1351
+
1352
+ if (attachment?.callToAction?.extraParams) {
1353
+ const extraParams: any = attachment?.callToAction?.extraParams;
1354
+ const route: any = extraParams?.route ?? null;
1355
+ let path: any = null;
1356
+ let param: any = null;
1357
+ if (role && role == PreDefinedRole.Guest) {
1358
+ path = route?.guest?.name ? route?.guest?.name ?? null : null;
1359
+ param = route?.guest?.params ? route?.guest?.params ?? null : null;
1360
+ } else if (role && role == PreDefinedRole.Owner) {
1361
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1362
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1363
+ } else {
1364
+ path = route?.host?.name ? route?.host?.name ?? null : null;
1365
+ param = route?.host?.params ? route?.host?.params ?? null : null;
1366
+ }
1367
+
1368
+ action = path;
1369
+ params = { ...param };
1370
+ } else if (attachment?.callToAction?.link) {
1371
+ action = CALL_TO_ACTION_PATH;
1372
+ actionId = attachment?.callToAction?.link.split('/').pop();
1373
+ params = { reservationId: actionId };
1374
+ }
1375
+
1376
+ return (
1377
+ <>
1378
+ {attachment?.callToAction && action ? (
1379
+ <Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
1380
+ <MessageText
1381
+ {...props}
1382
+ containerStyle={{
1383
+ left: { paddingLeft: 0, marginLeft: 0 },
1384
+ }}
1385
+ textStyle={{
1386
+ left: { marginLeft: 0 },
1387
+ }}
1388
+ />
1389
+ <Button
1390
+ variant={'outline'}
1391
+ size={'sm'}
1392
+ className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}] my-2 rounded-full `}
1393
+ onPress={() => action && params && navigation.navigate(action, params)}
1394
+ >
1395
+ <ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
1396
+ {attachment.callToAction.title}
1397
+ </ButtonText>
1398
+ </Button>
1399
+ </Box>
1400
+ ) : (
1401
+ <TouchableHighlight
1402
+ underlayColor={'#c0c0c0'}
1403
+ style={{ width: '100%' }}
1404
+ onPress={() => {
1405
+ if (currentMessage?.isShowThreadMessage)
1406
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
1407
+ channelId: channelId,
1408
+ title: 'Message',
1409
+ postParentId: currentMessage?._id,
1410
+ isPostParentIdThread: true,
1411
+ });
528
1412
  }}
529
- />
530
- </Box>
531
- ) : (
532
- <TouchableHighlight
533
- underlayColor={'#c0c0c0'}
534
- style={{ width: '100%' }}
535
- onPress={() => {
536
- if (currentMessage?.isShowThreadMessage)
537
- navigation.navigate(config.THREAD_MESSEGE_PATH, {
538
- channelId: channelId,
539
- title: 'Message',
540
- postParentId: currentMessage?._id,
541
- isPostParentIdThread: true,
542
- });
543
- }}
544
- >
545
- <>
546
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
547
- {currentMessage?.replies?.data?.length > 0 && (
548
- <HStack space={'sm'} px={'$1'} alignItems={'center'}>
549
- <HStack>
550
- {currentMessage?.replies?.data
551
- ?.filter(
552
- (v: any, i: any, a: any) =>
553
- a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
554
- )
555
- ?.slice(0, 2)
556
- ?.reverse()
557
- ?.map((p: any, i: Number) => (
558
- <Avatar
559
- key={'conversations-view-key-' + i}
560
- bg={'transparent'}
561
- size={'sm'}
562
- >
563
- <AvatarFallbackText>
564
- {startCase(p?.author?.username?.charAt(0))}
565
- </AvatarFallbackText>
566
- <AvatarImage
567
- alt="user image"
568
- style={{
569
- borderRadius: 6,
570
- borderWidth: 2,
571
- borderColor: '#fff',
572
- }}
573
- source={{
574
- uri: p?.author?.picture,
575
- }}
576
- />
577
- </Avatar>
578
- ))}
1413
+ >
1414
+ <>
1415
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1416
+ {currentMessage?.replies?.data?.length > 0 && (
1417
+ <HStack space={'sm'} className="px-1 items-center">
1418
+ <HStack>
1419
+ {currentMessage?.replies?.data
1420
+ ?.filter(
1421
+ (v: any, i: any, a: any) =>
1422
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) ===
1423
+ i,
1424
+ )
1425
+ ?.slice(0, 2)
1426
+ ?.reverse()
1427
+ ?.map((p: any, i: Number) => (
1428
+ <Avatar
1429
+ key={'conversations-view-key-' + i}
1430
+ size={'sm'}
1431
+ className="bg-transparent"
1432
+ >
1433
+ <AvatarFallbackText>
1434
+ {startCase(p?.author?.username?.charAt(0))}
1435
+ </AvatarFallbackText>
1436
+ <AvatarImage
1437
+ alt="user image"
1438
+ style={{
1439
+ borderRadius: 6,
1440
+ borderWidth: 2,
1441
+ borderColor: '#fff',
1442
+ }}
1443
+ source={{
1444
+ uri: p?.author?.picture,
1445
+ }}
1446
+ />
1447
+ </Avatar>
1448
+ ))}
1449
+ </HStack>
1450
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
1451
+ {currentMessage?.replies?.totalCount}{' '}
1452
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
1453
+ </Text>
1454
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
1455
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
1456
+ </Text>
579
1457
  </HStack>
580
- <Text style={{ fontSize: 12 }} fontWeight={'$bold'} color={'$blue800'}>
581
- {currentMessage?.replies?.totalCount}{' '}
582
- {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
583
- </Text>
584
- <Text style={{ fontSize: 12 }} fontWeight={'$bold'} color={'$trueGray500'}>
585
- {lastReply ? createdAtText(lastReply?.createdAt) : ''}
586
- </Text>
587
- </HStack>
588
- )}
589
- </>
590
- </TouchableHighlight>
591
- )}
592
- {/* <MessageText
593
- {...props}
594
- textStyle={{ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 } }}
595
- /> */}
596
- </>
597
- );
598
- } else {
599
- return (
600
- <TouchableHighlight
601
- underlayColor={'#c0c0c0'}
602
- style={{ width: '100%' }}
603
- onPress={() => {
604
- if (currentMessage?.isShowThreadMessage)
605
- navigation.navigate(config.THREAD_MESSEGE_PATH, {
606
- channelId: channelId,
607
- title: 'Message',
608
- postParentId: currentMessage?._id,
609
- isPostParentIdThread: true,
610
- });
611
- }}
612
- >
613
- <>
614
- <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
615
- {currentMessage?.replies?.data?.length > 0 && (
616
- <HStack space={'sm'} px={'$1'} alignItems={'center'}>
617
- <HStack>
618
- {currentMessage?.replies?.data
619
- ?.filter(
620
- (v: any, i: any, a: any) =>
621
- a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
622
- )
623
- ?.slice(0, 2)
624
- ?.reverse()
625
- ?.map((p: any, i: Number) => (
626
- <Avatar
627
- key={'conversation-replies-key-' + i}
628
- bg={'transparent'}
629
- size={'sm'}
630
- >
631
- <AvatarFallbackText>
632
- {startCase(p?.author?.username?.charAt(0))}
633
- </AvatarFallbackText>
634
- <AvatarImage
635
- alt="user image"
636
- style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
637
- source={{
638
- uri: p?.author?.picture,
639
- }}
640
- />
641
- </Avatar>
642
- ))}
643
- </HStack>
644
- <Text style={{ fontSize: 12 }} fontWeight={'$bold'} color={'$blue800'}>
645
- {currentMessage?.replies?.totalCount}{' '}
646
- {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
647
- </Text>
648
- <Text style={{ fontSize: 12 }} fontWeight={'$bold'} color={'$trueGray500'}>
649
- {lastReply ? createdAtText(lastReply?.createdAt) : ''}
650
- </Text>
651
- </HStack>
1458
+ )}
1459
+ </>
1460
+ </TouchableHighlight>
652
1461
  )}
653
1462
  </>
654
- </TouchableHighlight>
655
- );
656
- }
657
- };
1463
+ );
1464
+ } else {
1465
+ return (
1466
+ <TouchableHighlight
1467
+ underlayColor={'#c0c0c0'}
1468
+ style={{ width: '100%' }}
1469
+ onPress={() => {
1470
+ if (currentMessage?.isShowThreadMessage)
1471
+ navigation.navigate(config.THREAD_MESSEGE_PATH, {
1472
+ channelId: channelId,
1473
+ title: 'Message',
1474
+ postParentId: currentMessage?._id,
1475
+ isPostParentIdThread: true,
1476
+ });
1477
+ }}
1478
+ >
1479
+ <>
1480
+ <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
1481
+ {currentMessage?.replies?.data?.length > 0 && (
1482
+ <HStack space={'sm'} className="px-1 items-center">
1483
+ <HStack>
1484
+ {currentMessage?.replies?.data
1485
+ ?.filter(
1486
+ (v: any, i: any, a: any) =>
1487
+ a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
1488
+ )
1489
+ ?.slice(0, 2)
1490
+ ?.reverse()
1491
+ ?.map((p: any, i: Number) => (
1492
+ <Avatar
1493
+ key={'conversation-replies-key-' + i}
1494
+ className="bg-transparent"
1495
+ size={'sm'}
1496
+ >
1497
+ <AvatarFallbackText>
1498
+ {startCase(p?.author?.username?.charAt(0))}
1499
+ </AvatarFallbackText>
1500
+ <AvatarImage
1501
+ alt="user image"
1502
+ style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
1503
+ source={{
1504
+ uri: p?.author?.picture,
1505
+ }}
1506
+ />
1507
+ </Avatar>
1508
+ ))}
1509
+ </HStack>
1510
+ <Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
1511
+ {currentMessage?.replies?.totalCount}{' '}
1512
+ {currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
1513
+ </Text>
1514
+ <Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
1515
+ {lastReply ? createdAtText(lastReply?.createdAt) : ''}
1516
+ </Text>
1517
+ </HStack>
1518
+ )}
1519
+ </>
1520
+ </TouchableHighlight>
1521
+ );
1522
+ }
1523
+ },
1524
+ [navigation, channelId, role],
1525
+ );
658
1526
 
1527
+ // Render action buttons (including image upload)
659
1528
  const renderActions = (props) => {
660
1529
  return (
661
1530
  <Actions
662
1531
  {...props}
663
- icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
1532
+ // options={{
1533
+ // ['Choose from Library']: onSelectImages,
1534
+ // ['Cancel']: () => {}, // Add this option to make the sheet dismissible
1535
+ // }}
1536
+ optionTintColor="#000000"
1537
+ cancelButtonIndex={1} // Set the Cancel option as the cancel button
1538
+ icon={() => (
1539
+ <TouchableOpacity
1540
+ onPress={onSelectImages}
1541
+ style={{
1542
+ width: 25,
1543
+ height: 25,
1544
+ borderRadius: 20,
1545
+ backgroundColor: '#f5f5f5',
1546
+ alignItems: 'center',
1547
+ justifyContent: 'center',
1548
+ marginRight: 8,
1549
+ }}
1550
+ >
1551
+ <MaterialIcons name="add" size={20} color="#888" />
1552
+ </TouchableOpacity>
1553
+ // <Box
1554
+ // style={{
1555
+ // width: 32,
1556
+ // height: 32,
1557
+ // alignItems: 'center',
1558
+ // justifyContent: 'center',
1559
+ // }}
1560
+ // >
1561
+ // <Ionicons name="image" size={24} color={colors.blue[500]} />
1562
+ // </Box>
1563
+ )}
1564
+ containerStyle={{
1565
+ alignItems: 'center',
1566
+ justifyContent: 'center',
1567
+ marginLeft: 20,
1568
+ marginBottom: 0,
1569
+ }}
664
1570
  />
665
1571
  );
666
1572
  };
667
1573
 
668
- const renderAccessory = (props) => {
1574
+ // Create a more visible and reliable image preview with cancel button
1575
+ const renderAccessory = useCallback(() => {
1576
+ if (!images.length) return null;
669
1577
  return (
670
- <Box>
671
- {selectedImage !== '' ? (
672
- <HStack alignItems={'center'}>
673
- <Image
674
- ml={'$3'}
675
- key={selectedImage}
676
- alt={'image'}
677
- source={{ uri: selectedImage }}
678
- size={'xs'}
679
- />
680
- <Button
681
- variant={'solid'}
682
- bg={'transparent'}
683
- //colorScheme={'secondary'}
684
- onPress={() => {
685
- setFiles([]);
686
- setImage('');
687
- setImages([]);
1578
+ <Box style={{ position: 'relative', height: 70, backgroundColor: 'transparent', justifyContent: 'center' }}>
1579
+ <ScrollView
1580
+ horizontal
1581
+ showsHorizontalScrollIndicator={false}
1582
+ style={{
1583
+ flexDirection: 'row',
1584
+ paddingLeft: 15,
1585
+ paddingRight: 5,
1586
+ }}
1587
+ contentContainerStyle={{
1588
+ alignItems: 'center',
1589
+ height: '100%',
1590
+ }}
1591
+ >
1592
+ {images.map((img, index) => (
1593
+ <View
1594
+ key={`image-preview-${index}`}
1595
+ style={{
1596
+ width: 40,
1597
+ height: 40,
1598
+ marginRight: 15,
1599
+ borderRadius: 4,
1600
+ backgroundColor: colors.gray[200],
1601
+ overflow: 'hidden',
1602
+ borderWidth: 1,
1603
+ borderColor: '#e0e0e0',
1604
+ position: 'relative',
1605
+ zIndex: 10,
688
1606
  }}
689
1607
  >
690
- <ButtonText color={'$black'}>Cancel</ButtonText>
691
- </Button>
692
- </HStack>
693
- ) : null}
1608
+ <Image
1609
+ source={{ uri: img.uri || img.url }}
1610
+ style={{ width: '100%', height: '100%' }}
1611
+ alt={`selected image ${index + 1}`}
1612
+ />
1613
+ {/* Cross button at top right */}
1614
+ <TouchableOpacity
1615
+ onPress={() => {
1616
+ const newImages = [...images];
1617
+ newImages.splice(index, 1);
1618
+ setImages(newImages);
1619
+ if (newImages.length === 0) {
1620
+ setSelectedImage('');
1621
+ if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
1622
+ textInputRef.current.focus();
1623
+ }
1624
+ }
1625
+ }}
1626
+ style={{
1627
+ position: 'absolute',
1628
+ top: -1,
1629
+ right: -1,
1630
+ backgroundColor: 'rgba(0,0,0,0.6)',
1631
+ borderRadius: 12,
1632
+ width: 20,
1633
+ height: 20,
1634
+ alignItems: 'center',
1635
+ justifyContent: 'center',
1636
+ zIndex: 9999,
1637
+ }}
1638
+ >
1639
+ <Ionicons name="close" size={16} color="white" />
1640
+ </TouchableOpacity>
1641
+ </View>
1642
+ ))}
1643
+ </ScrollView>
694
1644
  </Box>
695
1645
  );
696
- };
1646
+ }, [images]);
697
1647
 
698
1648
  const setImageViewerObject = (obj: any, v: boolean) => {
699
1649
  setImageObject(obj);
@@ -701,17 +1651,16 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
701
1651
  };
702
1652
 
703
1653
  const modalContent = React.useMemo(() => {
704
- if (!imageObject) return <></>;
1654
+ if (!imageObject || !imageObject.image) return null;
705
1655
  const { image, _id } = imageObject;
1656
+
706
1657
  return (
707
1658
  <CachedImage
708
1659
  style={{ width: '100%', height: '100%' }}
709
1660
  resizeMode={'cover'}
710
- // cacheKey={`${_id}-conversation-modal-image-key`}
711
- cacheKey={`${_id}-slack-bubble-imageKey`}
1661
+ cacheKey={`${_id}-modal-imageKey`}
712
1662
  source={{
713
1663
  uri: image,
714
- //headers: `Authorization: Bearer ${token}`,
715
1664
  expiresIn: 86400,
716
1665
  }}
717
1666
  alt={'image'}
@@ -719,24 +1668,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
719
1668
  );
720
1669
  }, [imageObject]);
721
1670
 
1671
+ // Update the message rendering to show images instantly without loaders
722
1672
  const renderMessage = useCallback(
723
1673
  (props: any) => {
724
- // const {
725
- // currentMessage: { text: currText },
726
- // } = props;
727
-
728
- //let messageTextStyle: any;
729
-
730
- // Make "pure emoji" messages much bigger than plain text.
731
- // if (currText && emojiUtils.isPureEmojiString(currText)) {
732
- // messageTextStyle = {
733
- // fontSize: 28,
734
- // // Emoji get clipped if lineHeight isn't increased; make it consistent across platforms.
735
- // lineHeight: Platform.OS === 'android' ? 34 : 30,
736
- // }
737
- // }
738
-
739
- // return <SlackMessage {...props} messageTextStyle={messageTextStyle} />;
1674
+ // For all messages, use the SlackMessage component directly
740
1675
  return (
741
1676
  <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
742
1677
  );
@@ -744,170 +1679,376 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
744
1679
  [isShowImageViewer],
745
1680
  );
746
1681
 
747
- // const renderMessage = (props: any) => {
748
- // // const {
749
- // // currentMessage: { text: currText },
750
- // // } = props;
751
-
752
- // //let messageTextStyle: any;
753
-
754
- // // Make "pure emoji" messages much bigger than plain text.
755
- // // if (currText && emojiUtils.isPureEmojiString(currText)) {
756
- // // messageTextStyle = {
757
- // // fontSize: 28,
758
- // // // Emoji get clipped if lineHeight isn't increased; make it consistent across platforms.
759
- // // lineHeight: Platform.OS === 'android' ? 34 : 30,
760
- // // }
761
- // // }
762
-
763
- // // return <SlackMessage {...props} messageTextStyle={messageTextStyle} />;
764
- // return <SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />;
765
- // };
766
-
767
1682
  let onScroll = false;
768
1683
 
1684
+ // Optimize onMomentumScrollBegin for better scroll performance
769
1685
  const onMomentumScrollBegin = async ({ nativeEvent }: any) => {
1686
+ // Set scroll state
770
1687
  onScroll = true;
771
- console.log('scroll top');
772
- if (!loadingOldMessages && isCloseToTop(nativeEvent) && totalCount > channelMessages?.length) {
773
- await onFetchOld();
1688
+
1689
+ // Use the debounced fetch function to prevent excessive calls
1690
+ if (isCloseToTop(nativeEvent)) {
1691
+ onFetchOld();
774
1692
  }
775
1693
  };
776
1694
 
777
1695
  const onEndReached = () => {
778
- console.log('on end reached');
779
1696
  if (!onScroll) return;
780
- // load messages, show ActivityIndicator
781
1697
  onScroll = false;
782
- // setLoadingOldMessages(true);
783
1698
  };
784
1699
 
785
- return (
786
- <>
787
- {loadEarlierMsg && <Spinner color={'$blue500'} />}
788
-
789
- <GiftedChat
790
- ref={messageRootListRef}
791
- wrapInSafeArea={false}
792
- renderLoading={() => <Spinner color={'$blue500'} />}
793
- messages={messageList}
794
- listViewProps={{
795
- onEndReached: onEndReached,
796
- onEndReachedThreshold: 0.5,
797
- onMomentumScrollBegin: onMomentumScrollBegin,
798
- }}
799
- // listViewProps={{
800
- // scrollEventThrottle: 400,
801
- // onScroll: ({ nativeEvent }) => { console.log('scroll')
802
- // if (!loadingOldMessages && isCloseToTop(nativeEvent)) {
803
- // onFetchOld();
804
- // }
805
- // }
806
- // }}
807
- onSend={(messages) =>
808
- rest?.isCreateNewChannel && !channelId
809
- ? rest?.newChannelData?.type === RoomType?.Direct
810
- ? createDirectChannel(messages[0]?.text ?? ' ')
811
- : null
812
- : channelId && handleSend(messages[0]?.text ?? ' ')
813
- }
814
- text={msg ? msg : ' '}
815
- onInputTextChanged={(text) => setMsg(text)}
816
- renderFooter={() =>
817
- loading ? <Spinner color={'$blue500'} /> : imageLoading ? <Spinner color={'$blue500'} /> : ''
818
- }
819
- scrollToBottom
820
- user={{
821
- // _id: currentUser?.id || '',
822
- _id: auth?.id || '',
1700
+ // Add a loader for when more messages are being loaded
1701
+ const renderLoadEarlier = useCallback(() => {
1702
+ return loadingOldMessages ? (
1703
+ <View
1704
+ style={{
1705
+ padding: 10,
1706
+ backgroundColor: 'rgba(255,255,255,0.8)',
1707
+ borderRadius: 10,
1708
+ marginTop: 10,
823
1709
  }}
824
- isTyping={true}
825
- alwaysShowSend={loading ? false : true}
826
- //onLoadEarlier={onFetchOld}
827
- //infiniteScroll={true}
828
- renderSend={renderSend}
829
- // loadEarlier={data?.messages?.totalCount > channelMessages.length}
830
- //isLoadingEarlier={loadEarlierMsg}
831
- //extraData={{ isLoadingEarlier: loadingOldMessages }}
832
- // renderLoadEarlier={() =>
833
- // !loadEarlierMsg && (
834
- // <Center py={2}>
835
- // <Button
836
- // onPress={() => onFetchOld()}
837
- // variant={'outline'}
838
- // _text={{ color: 'black', fontSize: 15, fontWeight: 'bold' }}
839
- // >
840
- // Load earlier messages
841
- // </Button>
842
- // </Center>
843
- // )
844
- // }
845
- renderMessageText={renderMessageText}
846
- minInputToolbarHeight={50}
847
- renderActions={channelId && renderActions}
848
- renderAccessory={renderAccessory}
849
- renderMessage={renderMessage}
850
- renderChatFooter={() => (
851
- <>
852
- <ImageViewerModal
853
- isVisible={isShowImageViewer}
854
- setVisible={setImageViewer}
855
- modalContent={modalContent}
856
- />
857
- <SubscriptionHandler
858
- channelId={channelId?.toString()}
859
- subscribeToNewMessages={() =>
860
- subscribeToMore({
861
- document: CHAT_MESSAGE_ADDED,
862
- variables: {
863
- channelId: channelId?.toString(),
864
- },
865
- updateQuery: (prev, { subscriptionData }: any) => {
866
- if (!subscriptionData.data) return prev;
867
- setSkip(0);
868
- const newMessage: any = subscriptionData?.data?.chatMessageAdded;
869
- const previousData = prev?.messages?.data
870
- ? [...prev.messages.data, newMessage]
871
- : [];
872
- const totalMsgCount = prev?.messages?.totalCount + 1;
873
- setChannelMessages((oldMessages: any) =>
874
- uniqBy([...oldMessages, newMessage], ({ id }) => id),
875
- );
876
- setTotalCount(totalMsgCount);
877
- const merged = {
878
- ...prev,
879
- messages: {
880
- ...prev?.messages,
881
- data: [...(prev?.messages?.data ?? []), newMessage],
882
- totalCount: totalMsgCount,
883
- },
884
- };
885
- return merged;
886
- // return Object.assign({}, prev, {
887
- // messages: {
888
- // data: [...prev.messages.data, newMessage],
889
- // totalCount: prev.messages.totalCount + 1,
890
- // },
891
- // });
892
- },
893
- })
894
- }
1710
+ >
1711
+ <Spinner size="small" color="#3b82f6" />
1712
+ </View>
1713
+ ) : null;
1714
+ }, [loadingOldMessages]);
1715
+
1716
+ // Add state for tracking input toolbar height
1717
+ const [inputToolbarHeight, setInputToolbarHeight] = useState(30);
1718
+
1719
+ // 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.
1720
+ const renderInputToolbar = useCallback(
1721
+ (props) => (
1722
+ <View style={{ backgroundColor: '#fff', paddingBottom: 4, paddingTop: 4 }}>
1723
+ <View
1724
+ style={{
1725
+ flexDirection: 'row',
1726
+ alignItems: 'center',
1727
+ minHeight: 44,
1728
+ maxHeight: 56,
1729
+ backgroundColor: '#fff',
1730
+ borderRadius: 22,
1731
+ marginHorizontal: 8,
1732
+ paddingHorizontal: 8,
1733
+ borderTopWidth: 1,
1734
+ borderTopColor: '#e0e0e0',
1735
+ }}
1736
+ >
1737
+ <TouchableOpacity
1738
+ onPress={onSelectImages}
1739
+ style={{
1740
+ width: 32,
1741
+ height: 32,
1742
+ borderRadius: 16,
1743
+ backgroundColor: '#fff',
1744
+ alignItems: 'center',
1745
+ justifyContent: 'center',
1746
+ marginRight: 8,
1747
+ }}
1748
+ >
1749
+ <MaterialIcons name="add" size={24} color="#888" />
1750
+ </TouchableOpacity>
1751
+ <TextInput
1752
+ ref={textInputRef}
1753
+ style={{
1754
+ flex: 1,
1755
+ //minHeight: 36,
1756
+ maxHeight: 44,
1757
+ backgroundColor: 'transparent',
1758
+ color: '#444',
1759
+ paddingHorizontal: 8,
1760
+ paddingVertical: 0,
1761
+ alignSelf: 'center',
1762
+ textAlignVertical: 'center',
1763
+ }}
1764
+ placeholder="Jot something down"
1765
+ placeholderTextColor={colors.gray[400]}
1766
+ multiline
1767
+ value={messageText}
1768
+ onChangeText={setMessageText}
1769
+ />
1770
+ <TouchableOpacity
1771
+ onPress={() => handleSend([{ text: messageText }])}
1772
+ // disabled={(!messageText.trim() && images.length === 0) || isUploadingImage || loading}
1773
+ disabled={false}
1774
+ style={{
1775
+ marginLeft: 8,
1776
+ // opacity: (!messageText.trim() && images.length === 0) || isUploadingImage || loading ? 0.5 : 1,
1777
+ opacity: !messageText.trim() && images.length === 0 ? 0.5 : 1,
1778
+ }}
1779
+ >
1780
+ <MaterialCommunityIcons
1781
+ name="send-circle"
1782
+ size={32}
1783
+ color={!messageText.trim() && images.length === 0 ? colors.gray[400] : colors.blue[500]}
1784
+ // color={
1785
+ // (!messageText.trim() && images.length === 0) || isUploadingImage || loading
1786
+ // ? colors.gray[400]
1787
+ // : colors.blue[500]
1788
+ // }
895
1789
  />
896
- </>
1790
+ </TouchableOpacity>
1791
+ </View>
1792
+ {/* Selected Images Row */}
1793
+ {images && images.length > 0 && (
1794
+ <ScrollView
1795
+ horizontal
1796
+ showsHorizontalScrollIndicator={false}
1797
+ style={{ marginTop: 4, marginLeft: 8 }}
1798
+ >
1799
+ {images.map((img, index) => (
1800
+ <View
1801
+ key={`image-preview-${index}`}
1802
+ style={{
1803
+ width: 48,
1804
+ height: 48,
1805
+ marginRight: 8,
1806
+ borderRadius: 6,
1807
+ overflow: 'hidden',
1808
+ position: 'relative',
1809
+ backgroundColor: colors.gray[200],
1810
+ }}
1811
+ >
1812
+ <Image
1813
+ source={{ uri: img.uri || img.url }}
1814
+ style={{ width: '100%', height: '100%' }}
1815
+ alt={`selected image ${index + 1}`}
1816
+ />
1817
+ <TouchableOpacity
1818
+ onPress={() => {
1819
+ handleRemoveImage(index);
1820
+ // handleRemoveImage already focuses input if needed
1821
+ }}
1822
+ style={{
1823
+ position: 'absolute',
1824
+ top: 2,
1825
+ right: 2,
1826
+ backgroundColor: 'rgba(0,0,0,0.6)',
1827
+ borderRadius: 10,
1828
+ width: 20,
1829
+ height: 20,
1830
+ alignItems: 'center',
1831
+ justifyContent: 'center',
1832
+ }}
1833
+ >
1834
+ <Ionicons name="close" size={14} color="white" />
1835
+ </TouchableOpacity>
1836
+ </View>
1837
+ ))}
1838
+ </ScrollView>
897
1839
  )}
898
- lightboxProps={{
899
- underlayColor: 'transparent',
900
- springConfig: { tension: 90000, friction: 90000 },
901
- disabled: true,
902
- }}
903
- />
904
- </>
1840
+ </View>
1841
+ ),
1842
+ [onSelectImages, messageText, images, isUploadingImage, loading, handleSend, handleRemoveImage],
905
1843
  );
906
- };
907
1844
 
908
- const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptionHandlerProps) => {
909
- useEffect(() => subscribeToNewMessages(), [channelId]);
910
- return <></>;
1845
+ // Create a memoized ImageViewerModal component
1846
+ const imageViewerModal = useMemo(
1847
+ () => (
1848
+ <ImageViewerModal isVisible={isShowImageViewer} setVisible={setImageViewer} modalContent={modalContent} />
1849
+ ),
1850
+ [isShowImageViewer, modalContent],
1851
+ );
1852
+
1853
+ // Create a memoized renderChatFooter function
1854
+ const renderChatFooter = useCallback(() => {
1855
+ return (
1856
+ <>
1857
+ {imageViewerModal}
1858
+ <SubscriptionHandler
1859
+ subscribeToMore={subscribe}
1860
+ document={CHAT_MESSAGE_ADDED}
1861
+ variables={{ channelId: channelId?.toString() }}
1862
+ updateQuery={undefined}
1863
+ />
1864
+ </>
1865
+ );
1866
+ }, [imageViewerModal, subscribe]);
1867
+
1868
+ // Add optimized listViewProps to reduce re-renders and improve list performance
1869
+ const listViewProps = useMemo(
1870
+ () => ({
1871
+ onEndReached: onEndReached,
1872
+ onEndReachedThreshold: 0.5,
1873
+ onMomentumScrollBegin: onMomentumScrollBegin,
1874
+ removeClippedSubviews: true, // Improve performance by unmounting components when not visible
1875
+ initialNumToRender: 10, // Reduce initial render amount
1876
+ maxToRenderPerBatch: 7, // Reduce number in each render batch
1877
+ windowSize: 7, // Reduce the window size
1878
+ updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
1879
+ keyExtractor: (item) => item._id, // Add explicit key extractor
1880
+ }),
1881
+ [onEndReached, onMomentumScrollBegin],
1882
+ );
1883
+
1884
+ // Debug helper function to inspect files in messages
1885
+ const debugFileData = useCallback((message: any, prefix: string = 'Message') => {
1886
+ if (__DEV__) {
1887
+ console.log(
1888
+ `${prefix} ID: ${message?.id}, ` +
1889
+ `Has files object: ${!!message?.files}, ` +
1890
+ `Files typename: ${message?.files?.__typename}, ` +
1891
+ `Files data exists: ${!!message?.files?.data}, ` +
1892
+ `Files count: ${message?.files?.data?.length || 0}`,
1893
+ );
1894
+
1895
+ if (message?.files?.data && message?.files?.data?.length > 0) {
1896
+ const file = message?.files?.data[0];
1897
+ console.log(
1898
+ `File[0] ID: ${file?.id}, ` +
1899
+ `URL: ${file?.url?.substring(0, 30)}..., ` +
1900
+ `Name: ${file?.name}, ` +
1901
+ `Type: ${file?.mimeType}`,
1902
+ );
1903
+ }
1904
+ }
1905
+ }, []);
1906
+
1907
+ // Return optimized component with performance improvements
1908
+ return (
1909
+ <KeyboardAvoidingView
1910
+ style={{ flex: 1, justifyContent: 'flex-end' }}
1911
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
1912
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
1913
+ >
1914
+ <View
1915
+ style={{
1916
+ flex: 1,
1917
+ backgroundColor: 'white',
1918
+ position: 'relative',
1919
+ marginBottom: images.length > 0 ? 5 : bottomMargin,
1920
+ }}
1921
+ >
1922
+ {errorMessage ? (
1923
+ <ErrorNotification
1924
+ message={errorMessage}
1925
+ onClose={() => setErrorMessage('')}
1926
+ type={notificationType}
1927
+ />
1928
+ ) : null}
1929
+
1930
+ {messageLoading && <Spinner color={'#3b82f6'} />}
1931
+ <GiftedChatInboxComponent
1932
+ ref={messageRootListRef}
1933
+ errorMessage={errorMessage}
1934
+ images={images}
1935
+ onSelectImages={onSelectImages}
1936
+ onRemoveImage={handleRemoveImage}
1937
+ selectedImage={selectedImage}
1938
+ setSelectedImage={setSelectedImage}
1939
+ isUploadingImage={isUploadingImage}
1940
+ loading={loading}
1941
+ wrapInSafeArea={true}
1942
+ renderLoading={() => <Spinner color={'#3b82f6'} />}
1943
+ messages={messageList}
1944
+ renderAvatar={null}
1945
+ showUserAvatar={false}
1946
+ listViewProps={{
1947
+ ...listViewProps,
1948
+ contentContainerStyle: {
1949
+ paddingBottom: inputToolbarHeight,
1950
+ },
1951
+ }}
1952
+ onSend={handleSend}
1953
+ text={messageText || ' '}
1954
+ onInputTextChanged={(text) => {
1955
+ setMessageText(text);
1956
+ }}
1957
+ renderFooter={() => (isUploadingImage ? <Spinner color={'#3b82f6'} /> : null)}
1958
+ scrollToBottom
1959
+ user={{
1960
+ _id: auth?.id || '',
1961
+ }}
1962
+ renderSend={renderSend}
1963
+ renderMessageText={renderMessageText}
1964
+ renderMessage={renderMessage}
1965
+ renderChatFooter={renderChatFooter}
1966
+ renderLoadEarlier={renderLoadEarlier}
1967
+ loadEarlier={totalCount > channelMessages.length}
1968
+ isLoadingEarlier={loadingOldMessages}
1969
+ placeholder="Jot something down"
1970
+ infiniteScroll={true}
1971
+ // renderChatEmpty={() => (
1972
+ // <><Text>Empty</Text>
1973
+ // {!loading && messageList && messageList?.length == 0 && (
1974
+ // <Box className="p-5">
1975
+ // <Center className="mt-6">
1976
+ // <Ionicons name="chatbubbles" size={30} />
1977
+ // <Text>You don't have any message yet!</Text>
1978
+ // </Center>
1979
+ // </Box>
1980
+ // )}
1981
+ // </>
1982
+ // )}
1983
+ />
1984
+
1985
+ {/* <GiftedChat
1986
+ ref={messageRootListRef}
1987
+ wrapInSafeArea={true}
1988
+ renderLoading={() => <Spinner color={'#3b82f6'} />}
1989
+ messages={messageList}
1990
+ renderAvatar={null}
1991
+ showUserAvatar={false}
1992
+ listViewProps={{
1993
+ ...listViewProps,
1994
+ contentContainerStyle: {
1995
+ paddingBottom: inputToolbarHeight,
1996
+ },
1997
+ }}
1998
+ onSend={handleSend}
1999
+ text={messageText || ' '}
2000
+ onInputTextChanged={(text) => {
2001
+ setMessageText(text);
2002
+ }}
2003
+ renderFooter={() => (isUploadingImage ? <Spinner color={'#3b82f6'} /> : null)}
2004
+ scrollToBottom
2005
+ user={{
2006
+ _id: auth?.id || '',
2007
+ }}
2008
+ isTyping={false}
2009
+ alwaysShowSend={true}
2010
+ renderSend={renderSend}
2011
+ renderMessageText={renderMessageText}
2012
+ renderInputToolbar={renderInputToolbar}
2013
+ // renderComposer={renderComposer}
2014
+ // minInputToolbarHeight={isActionSheetVisible ? 0 : 56}
2015
+ minInputToolbarHeight={inputToolbarHeight}
2016
+ renderActions={null}
2017
+ renderMessage={renderMessage}
2018
+ renderChatFooter={renderChatFooter}
2019
+ renderLoadEarlier={renderLoadEarlier}
2020
+ loadEarlier={totalCount > channelMessages.length}
2021
+ isLoadingEarlier={loadingOldMessages}
2022
+ bottomOffset={0}
2023
+ isKeyboardInternallyHandled={false}
2024
+ textInputProps={{
2025
+ multiline: true,
2026
+ returnKeyType: 'default',
2027
+ enablesReturnKeyAutomatically: true,
2028
+ placeholderTextColor: colors.gray[400],
2029
+ }}
2030
+ minComposerHeight={36}
2031
+ maxComposerHeight={100}
2032
+ placeholder="Jot something down"
2033
+ lightboxProps={{
2034
+ underlayColor: 'transparent',
2035
+ springConfig: { tension: 90000, friction: 90000 },
2036
+ disabled: true,
2037
+ }}
2038
+ infiniteScroll={false}
2039
+ renderAccessory={selectedImage ? renderAccessory : null}
2040
+ /> */}
2041
+ </View>
2042
+ </KeyboardAvoidingView>
2043
+ );
911
2044
  };
912
2045
 
913
- export const ConversationView = React.memo(ConversationViewComponent);
2046
+ // Export with React.memo to prevent unnecessary re-renders
2047
+ export const ConversationView = React.memo(ConversationViewComponent, (prevProps, nextProps) => {
2048
+ // Only re-render if these critical props change
2049
+ return (
2050
+ prevProps.channelId === nextProps.channelId &&
2051
+ prevProps.role === nextProps.role &&
2052
+ prevProps.isShowThreadMessage === nextProps.isShowThreadMessage
2053
+ );
2054
+ });