@messenger-box/platform-mobile 10.0.3-alpha.16 → 10.0.3-alpha.162

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