@messenger-box/platform-mobile 10.0.3-alpha.7 → 10.0.3-alpha.74
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.
- package/CHANGELOG.md +120 -0
- package/lib/compute.js +2 -3
- package/lib/compute.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/queries/inboxQueries.js +65 -0
- package/lib/queries/inboxQueries.js.map +1 -0
- package/lib/routes.json +2 -3
- package/lib/screens/inbox/DialogMessages.js +1 -1
- package/lib/screens/inbox/DialogMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreadMessages.js +4 -8
- package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreads.js +57 -12
- package/lib/screens/inbox/DialogThreads.js.map +1 -1
- package/lib/screens/inbox/Inbox.js +1 -1
- package/lib/screens/inbox/Inbox.js.map +1 -1
- package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
- package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
- package/lib/screens/inbox/components/CachedImage/index.js +168 -46
- package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
- package/lib/screens/inbox/components/DialogItem.js +169 -0
- package/lib/screens/inbox/components/DialogItem.js.map +1 -0
- package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
- package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +147 -31
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
- package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +6 -1
- package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
- package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
- package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
- package/lib/screens/inbox/components/ThreadsViewItem.js +66 -55
- package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
- package/lib/screens/inbox/config/config.js +2 -2
- package/lib/screens/inbox/config/config.js.map +1 -1
- package/lib/screens/inbox/containers/ConversationView.js +1111 -434
- package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
- package/lib/screens/inbox/containers/Dialogs.js +193 -80
- package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
- package/lib/screens/inbox/containers/ThreadConversationView.js +725 -216
- package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
- package/lib/screens/inbox/containers/ThreadsView.js +83 -50
- package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
- package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
- package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
- package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
- package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
- package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
- package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
- package/package.json +4 -4
- package/src/compute.ts +5 -6
- package/src/index.ts +2 -0
- package/src/navigation/InboxNavigation.tsx +3 -3
- package/src/queries/inboxQueries.ts +299 -0
- package/src/queries/index.d.ts +2 -0
- package/src/queries/index.ts +1 -0
- package/src/screens/inbox/DialogMessages.tsx +1 -1
- package/src/screens/inbox/DialogThreadMessages.tsx +7 -14
- package/src/screens/inbox/DialogThreads.tsx +55 -61
- package/src/screens/inbox/Inbox.tsx +1 -1
- package/src/screens/inbox/components/Actionsheet.tsx +30 -0
- package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
- package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
- package/src/screens/inbox/components/DialogItem.tsx +306 -0
- package/src/screens/inbox/components/DialogsHeader.tsx +6 -13
- package/src/screens/inbox/components/DialogsListItem.tsx +262 -198
- package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
- package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
- package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
- package/src/screens/inbox/components/ServiceDialogsListItem.tsx +337 -194
- package/src/screens/inbox/components/SlackInput.tsx +23 -0
- package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
- package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +1 -1
- package/src/screens/inbox/components/SmartLoader.tsx +61 -0
- package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
- package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +53 -55
- package/src/screens/inbox/components/ThreadsViewItem.tsx +178 -285
- package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
- package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
- package/src/screens/inbox/config/config.ts +2 -2
- package/src/screens/inbox/containers/ConversationView.tsx +1843 -702
- package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
- package/src/screens/inbox/containers/Dialogs.tsx +402 -204
- package/src/screens/inbox/containers/SupportServiceDialogs.tsx +4 -4
- package/src/screens/inbox/containers/ThreadConversationView.tsx +1350 -319
- package/src/screens/inbox/containers/ThreadsView.tsx +105 -193
- package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
- package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
- package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
- package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
- package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
- package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
- package/src/screens/inbox/index.ts +37 -0
- package/src/screens/inbox/machines/threadsMachine.ts +147 -0
- package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
- package/tsconfig.json +11 -54
- package/lib/screens/inbox/components/DialogsListItem.js +0 -171
- package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
- package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -171
- package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
|
@@ -0,0 +1,1467 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
AvatarFallbackText,
|
|
5
|
+
AvatarImage,
|
|
6
|
+
Box,
|
|
7
|
+
Button,
|
|
8
|
+
ButtonText,
|
|
9
|
+
HStack,
|
|
10
|
+
Icon,
|
|
11
|
+
Image,
|
|
12
|
+
Spinner,
|
|
13
|
+
Text,
|
|
14
|
+
Skeleton,
|
|
15
|
+
} from '@admin-layout/gluestack-ui-mobile';
|
|
16
|
+
import { Platform, TouchableHighlight, SafeAreaView, View } from 'react-native';
|
|
17
|
+
import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native';
|
|
18
|
+
import { navigationRef } from '@common-stack/client-react';
|
|
19
|
+
import { useSelector } from 'react-redux';
|
|
20
|
+
import { orderBy, startCase, uniqBy } from 'lodash-es';
|
|
21
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
22
|
+
import { encode as atob } from 'base-64';
|
|
23
|
+
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
|
24
|
+
import { Actions, GiftedChat, IMessage, MessageText, Send, Composer, InputToolbar } from 'react-native-gifted-chat';
|
|
25
|
+
import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo, FileRefType, PostTypeEnum } from 'common';
|
|
26
|
+
import {
|
|
27
|
+
OnChatMessageAddedDocument as CHAT_MESSAGE_ADDED,
|
|
28
|
+
useMessagesQuery,
|
|
29
|
+
useSendExpoNotificationOnPostMutation,
|
|
30
|
+
useSendMessagesMutation,
|
|
31
|
+
useViewChannelDetailQuery,
|
|
32
|
+
useAddDirectChannelMutation,
|
|
33
|
+
MessagesDocument,
|
|
34
|
+
} from 'common/graphql';
|
|
35
|
+
import { useUploadFilesNative } from '@messenger-box/platform-client';
|
|
36
|
+
import { objectId } from '@messenger-box/core';
|
|
37
|
+
import { userSelector } from '@adminide-stack/user-auth0-client';
|
|
38
|
+
import { format, isToday, isYesterday } from 'date-fns';
|
|
39
|
+
import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
|
|
40
|
+
import CachedImage from '../components/CachedImage';
|
|
41
|
+
import { config } from '../config';
|
|
42
|
+
import colors from 'tailwindcss/colors';
|
|
43
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
44
|
+
|
|
45
|
+
// Define an extended interface for ImagePickerAsset with url property
|
|
46
|
+
interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
|
|
47
|
+
url?: string;
|
|
48
|
+
fileName?: string;
|
|
49
|
+
mimeType?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
MESSAGES_PER_PAGE,
|
|
54
|
+
CALL_TO_ACTION_BOX_BGCOLOR,
|
|
55
|
+
CALL_TO_ACTION_PATH,
|
|
56
|
+
CALL_TO_ACTION_BUTTON_BORDERCOLOR,
|
|
57
|
+
CALL_TO_ACTION_TEXT_COLOR,
|
|
58
|
+
} = config;
|
|
59
|
+
|
|
60
|
+
const createdAtText = (value: string) => {
|
|
61
|
+
if (!value) return '';
|
|
62
|
+
let date = new Date(value);
|
|
63
|
+
if (isToday(date)) return 'Today';
|
|
64
|
+
if (isYesterday(date)) return 'Yesterday';
|
|
65
|
+
return format(new Date(value), 'MMM dd, yyyy');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
interface ISubscriptionHandlerProps {
|
|
69
|
+
subscribeToNewMessages: () => any;
|
|
70
|
+
channelId: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface IMessageProps extends IMessage {
|
|
74
|
+
type: string;
|
|
75
|
+
propsConfiguration?: any;
|
|
76
|
+
replies?: any;
|
|
77
|
+
isShowThreadMessage?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface AlertMessageAttachmentsInterface {
|
|
81
|
+
title: string;
|
|
82
|
+
isTitleHtml: boolean;
|
|
83
|
+
icon: string;
|
|
84
|
+
callToAction: {
|
|
85
|
+
title: string;
|
|
86
|
+
link: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Fix for the optimistic response types
|
|
91
|
+
type OptimisticPropsConfig = {
|
|
92
|
+
__typename: 'MachineConfiguration';
|
|
93
|
+
resource: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const ConversationViewComponent = ({ channelId: initialChannelId, role, isShowThreadMessage, ...rest }: any) => {
|
|
97
|
+
// Core state management using React hooks instead of XState
|
|
98
|
+
const [channelId, setChannelId] = useState<string | null>(initialChannelId || null);
|
|
99
|
+
const [messageText, setMessageText] = useState('');
|
|
100
|
+
const [skip, setSkip] = useState(0);
|
|
101
|
+
const [loading, setLoading] = useState(false);
|
|
102
|
+
const [loadingOldMessages, setLoadingOldMessages] = useState(false);
|
|
103
|
+
const [error, setError] = useState<string | null>(null);
|
|
104
|
+
const [selectedImage, setSelectedImage] = useState<string>('');
|
|
105
|
+
const [images, setImages] = useState<any[]>([]);
|
|
106
|
+
const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
|
|
107
|
+
const [imageObject, setImageObject] = useState<any>({});
|
|
108
|
+
|
|
109
|
+
// Create refs for various operations
|
|
110
|
+
const messageRootListRef = useRef<any>(null);
|
|
111
|
+
const isMounted = useRef(true);
|
|
112
|
+
const fetchOldDebounceRef = useRef(false);
|
|
113
|
+
|
|
114
|
+
// Navigation and auth
|
|
115
|
+
const auth: any = useSelector(userSelector);
|
|
116
|
+
const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
|
|
117
|
+
const navigation = useNavigation<any>();
|
|
118
|
+
const isFocused = useIsFocused();
|
|
119
|
+
|
|
120
|
+
// Apollo mutations
|
|
121
|
+
const [addDirectChannel] = useAddDirectChannelMutation();
|
|
122
|
+
const { startUpload } = useUploadFilesNative();
|
|
123
|
+
const [sendMsg] = useSendMessagesMutation();
|
|
124
|
+
const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
|
|
125
|
+
|
|
126
|
+
// Apollo query for messages
|
|
127
|
+
const {
|
|
128
|
+
data,
|
|
129
|
+
loading: messageLoading,
|
|
130
|
+
refetch,
|
|
131
|
+
fetchMore: fetchMoreMessages,
|
|
132
|
+
subscribeToMore,
|
|
133
|
+
} = useMessagesQuery({
|
|
134
|
+
variables: {
|
|
135
|
+
channelId: channelId?.toString(),
|
|
136
|
+
parentId: null,
|
|
137
|
+
limit: MESSAGES_PER_PAGE,
|
|
138
|
+
skip: skip,
|
|
139
|
+
},
|
|
140
|
+
skip: !channelId,
|
|
141
|
+
fetchPolicy: 'cache-and-network',
|
|
142
|
+
nextFetchPolicy: 'cache-first',
|
|
143
|
+
refetchWritePolicy: 'merge',
|
|
144
|
+
notifyOnNetworkStatusChange: true,
|
|
145
|
+
onCompleted: (queryData) => {
|
|
146
|
+
// MESSAGE QUERY COMPLETED
|
|
147
|
+
},
|
|
148
|
+
onError: (error) => {
|
|
149
|
+
setError(String(error));
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Extract messages from the query data
|
|
154
|
+
const channelMessages = useMemo(() => {
|
|
155
|
+
return data?.messages?.data || [];
|
|
156
|
+
}, [data?.messages?.data]);
|
|
157
|
+
|
|
158
|
+
// Get total message count
|
|
159
|
+
const totalCount = useMemo(() => {
|
|
160
|
+
return data?.messages?.totalCount || 0;
|
|
161
|
+
}, [data?.messages?.totalCount]);
|
|
162
|
+
|
|
163
|
+
// Clear messages when component unmounts
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
return () => {
|
|
166
|
+
isMounted.current = false;
|
|
167
|
+
};
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
// Update channelId from props or navigation params
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const currentChannelId = initialChannelId || currentRoute?.params?.channelId;
|
|
173
|
+
if (currentChannelId) {
|
|
174
|
+
setChannelId(currentChannelId);
|
|
175
|
+
}
|
|
176
|
+
}, [initialChannelId, currentRoute]);
|
|
177
|
+
|
|
178
|
+
// Focus/unfocus behavior
|
|
179
|
+
useFocusEffect(
|
|
180
|
+
React.useCallback(() => {
|
|
181
|
+
if (channelId) {
|
|
182
|
+
// Refresh messages when screen comes into focus
|
|
183
|
+
refetch();
|
|
184
|
+
}
|
|
185
|
+
return () => {
|
|
186
|
+
// Nothing needed on unfocus
|
|
187
|
+
};
|
|
188
|
+
}, [channelId, isFocused, refetch]),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Loading state for image selection
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (selectedImage) {
|
|
194
|
+
setLoading(false);
|
|
195
|
+
}
|
|
196
|
+
}, [selectedImage]);
|
|
197
|
+
|
|
198
|
+
// Fetch more messages function
|
|
199
|
+
const fetchMoreMessagesImpl = useCallback(async () => {
|
|
200
|
+
try {
|
|
201
|
+
setLoadingOldMessages(true);
|
|
202
|
+
const response = await fetchMoreMessages({
|
|
203
|
+
variables: {
|
|
204
|
+
channelId: channelId?.toString(),
|
|
205
|
+
parentId: null,
|
|
206
|
+
skip: channelMessages.length,
|
|
207
|
+
},
|
|
208
|
+
// updateQuery: (prev, { fetchMoreResult }) => {
|
|
209
|
+
// if (!fetchMoreResult || !fetchMoreResult.messages) return prev;
|
|
210
|
+
|
|
211
|
+
// // Create a new array of all messages deduped by ID
|
|
212
|
+
// const combinedMessages = [...prev.messages.data, ...fetchMoreResult.messages.data].filter(
|
|
213
|
+
// (message, index, self) => index === self.findIndex((m) => m.id === message.id),
|
|
214
|
+
// );
|
|
215
|
+
|
|
216
|
+
// return {
|
|
217
|
+
// ...prev,
|
|
218
|
+
// messages: {
|
|
219
|
+
// ...prev.messages,
|
|
220
|
+
// data: combinedMessages,
|
|
221
|
+
// totalCount: fetchMoreResult.messages.totalCount,
|
|
222
|
+
// __typename: prev.messages.__typename,
|
|
223
|
+
// },
|
|
224
|
+
// };
|
|
225
|
+
// },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
setLoadingOldMessages(false);
|
|
229
|
+
if (!response?.data?.messages?.data) {
|
|
230
|
+
return { error: 'No messages returned' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { messages: response.data.messages.data };
|
|
234
|
+
} catch (error) {
|
|
235
|
+
setLoadingOldMessages(false);
|
|
236
|
+
setError(String(error));
|
|
237
|
+
return { error: String(error) };
|
|
238
|
+
}
|
|
239
|
+
}, [channelId, channelMessages.length, fetchMoreMessages]);
|
|
240
|
+
|
|
241
|
+
// Send message function
|
|
242
|
+
const sendMessageImpl = useCallback(async () => {
|
|
243
|
+
try {
|
|
244
|
+
// Store the current message text and clear input immediately for better UX
|
|
245
|
+
const currentMessageText = messageText;
|
|
246
|
+
setMessageText('');
|
|
247
|
+
|
|
248
|
+
const notificationData: IExpoNotificationData = {
|
|
249
|
+
url: config.INBOX_MESSEGE_PATH,
|
|
250
|
+
params: { channelId, hideTabBar: true },
|
|
251
|
+
screen: 'DialogMessages',
|
|
252
|
+
other: { sound: Platform.OS === 'android' ? undefined : 'default' },
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Create optimistic message with consistent structure
|
|
256
|
+
const messageId = objectId();
|
|
257
|
+
const optimisticMessage = {
|
|
258
|
+
__typename: 'Post' as const,
|
|
259
|
+
id: messageId,
|
|
260
|
+
message: currentMessageText,
|
|
261
|
+
createdAt: new Date().toISOString(),
|
|
262
|
+
updatedAt: new Date().toISOString(),
|
|
263
|
+
author: {
|
|
264
|
+
__typename: 'UserAccount' as const,
|
|
265
|
+
id: auth?.id,
|
|
266
|
+
givenName: auth?.givenName || '',
|
|
267
|
+
familyName: auth?.familyName || '',
|
|
268
|
+
picture: auth?.picture || '',
|
|
269
|
+
username: auth?.username || '',
|
|
270
|
+
email: auth?.email || '',
|
|
271
|
+
alias: [] as string[],
|
|
272
|
+
tokens: [],
|
|
273
|
+
},
|
|
274
|
+
isDelivered: true,
|
|
275
|
+
isRead: false,
|
|
276
|
+
type: 'TEXT' as any,
|
|
277
|
+
parentId: null,
|
|
278
|
+
fromServer: false,
|
|
279
|
+
channel: {
|
|
280
|
+
__typename: 'Channel' as const,
|
|
281
|
+
id: channelId,
|
|
282
|
+
},
|
|
283
|
+
propsConfiguration: {
|
|
284
|
+
__typename: 'MachineConfiguration' as const,
|
|
285
|
+
resource: '' as any, // Cast to any to bypass the URI type check
|
|
286
|
+
},
|
|
287
|
+
props: {},
|
|
288
|
+
files: {
|
|
289
|
+
__typename: 'FilesInfo' as const,
|
|
290
|
+
data: [],
|
|
291
|
+
totalCount: 0,
|
|
292
|
+
},
|
|
293
|
+
replies: {
|
|
294
|
+
__typename: 'Messages' as const,
|
|
295
|
+
data: [],
|
|
296
|
+
totalCount: 0,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const response = await sendMsg({
|
|
301
|
+
variables: {
|
|
302
|
+
channelId,
|
|
303
|
+
content: currentMessageText,
|
|
304
|
+
notificationParams: notificationData,
|
|
305
|
+
},
|
|
306
|
+
optimisticResponse: {
|
|
307
|
+
__typename: 'Mutation',
|
|
308
|
+
sendMessage: optimisticMessage,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return { message: response.data?.sendMessage };
|
|
313
|
+
} catch (error) {
|
|
314
|
+
setLoading(false);
|
|
315
|
+
setError(String(error));
|
|
316
|
+
return { error: String(error) };
|
|
317
|
+
}
|
|
318
|
+
}, [channelId, messageText, sendMsg, auth]);
|
|
319
|
+
|
|
320
|
+
// Image selection handler
|
|
321
|
+
const onSelectImages = async () => {
|
|
322
|
+
setLoading(true);
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
let imageSource = await ImagePicker.launchImageLibraryAsync({
|
|
326
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
327
|
+
allowsEditing: true,
|
|
328
|
+
aspect: [4, 3],
|
|
329
|
+
quality: 0.8,
|
|
330
|
+
base64: true,
|
|
331
|
+
exif: false,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (!imageSource?.canceled) {
|
|
335
|
+
// Get the asset
|
|
336
|
+
const selectedAsset = imageSource?.assets?.[0];
|
|
337
|
+
if (!selectedAsset) {
|
|
338
|
+
setLoading(false);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Create a base64 image string for preview
|
|
343
|
+
const base64Data = selectedAsset.base64;
|
|
344
|
+
const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
|
|
345
|
+
|
|
346
|
+
// Format the asset for upload service requirements
|
|
347
|
+
const asset: ExtendedImagePickerAsset = {
|
|
348
|
+
...selectedAsset,
|
|
349
|
+
url: selectedAsset.uri,
|
|
350
|
+
fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
|
|
351
|
+
mimeType: 'image/jpeg',
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Update state with the new image
|
|
355
|
+
setSelectedImage(previewImage);
|
|
356
|
+
setImages([asset]);
|
|
357
|
+
} else {
|
|
358
|
+
setLoading(false);
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
setLoading(false);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Add a state variable to track which message should show the skeleton
|
|
366
|
+
const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
|
|
367
|
+
|
|
368
|
+
// Send message with file function - update to set and clear uploadingMessageId
|
|
369
|
+
const sendMessageWithFileImpl = useCallback(async () => {
|
|
370
|
+
try {
|
|
371
|
+
// For file uploads, we still need loading state since we need to wait for the file upload
|
|
372
|
+
setLoading(true);
|
|
373
|
+
|
|
374
|
+
// Generate a unique post ID for the message
|
|
375
|
+
const postId = objectId();
|
|
376
|
+
|
|
377
|
+
// Set the message ID that should show the skeleton
|
|
378
|
+
setUploadingMessageId(postId);
|
|
379
|
+
|
|
380
|
+
// Prepare notification data
|
|
381
|
+
const notificationData: IExpoNotificationData = {
|
|
382
|
+
url: config.INBOX_MESSEGE_PATH,
|
|
383
|
+
params: { channelId, hideTabBar: true },
|
|
384
|
+
screen: 'DialogMessages',
|
|
385
|
+
other: { sound: Platform.OS === 'android' ? undefined : 'default' },
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Safety check for images
|
|
389
|
+
if (!images || images.length === 0) {
|
|
390
|
+
setLoading(false);
|
|
391
|
+
setUploadingMessageId(null);
|
|
392
|
+
return { error: 'No images available to upload' };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Format the images for upload if needed
|
|
396
|
+
const imagesToUpload = images.map((img) => {
|
|
397
|
+
// Ensure the image has all required properties
|
|
398
|
+
return {
|
|
399
|
+
...img,
|
|
400
|
+
uri: img.uri || img.url, // Use either uri or url
|
|
401
|
+
type: 'image/jpeg',
|
|
402
|
+
name: img.fileName || `image_${Date.now()}.jpg`,
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Store current message text and clear inputs immediately for better UX
|
|
407
|
+
const currentMessageText = messageText;
|
|
408
|
+
setMessageText('');
|
|
409
|
+
|
|
410
|
+
// Create file info for optimistic response
|
|
411
|
+
const optimisticFileInfo = {
|
|
412
|
+
__typename: 'FileInfo' as const,
|
|
413
|
+
id: objectId(),
|
|
414
|
+
url: selectedImage,
|
|
415
|
+
name: imagesToUpload[0]?.name || 'image.jpg',
|
|
416
|
+
extension: 'jpg',
|
|
417
|
+
mimeType: 'image/jpeg',
|
|
418
|
+
size: 0,
|
|
419
|
+
refType: FileRefType.Post,
|
|
420
|
+
height: imagesToUpload[0]?.height || 0,
|
|
421
|
+
width: imagesToUpload[0]?.width || 0,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// Create optimistic message with file
|
|
425
|
+
const optimisticMessage = {
|
|
426
|
+
__typename: 'Post' as const,
|
|
427
|
+
id: postId,
|
|
428
|
+
message: currentMessageText || ' ',
|
|
429
|
+
createdAt: new Date().toISOString(),
|
|
430
|
+
updatedAt: new Date().toISOString(),
|
|
431
|
+
author: {
|
|
432
|
+
__typename: 'UserAccount' as const,
|
|
433
|
+
id: auth?.id,
|
|
434
|
+
givenName: auth?.givenName || '',
|
|
435
|
+
familyName: auth?.familyName || '',
|
|
436
|
+
picture: auth?.picture || '',
|
|
437
|
+
username: auth?.username || '',
|
|
438
|
+
email: auth?.email || '',
|
|
439
|
+
alias: [] as string[],
|
|
440
|
+
tokens: [],
|
|
441
|
+
},
|
|
442
|
+
isDelivered: true,
|
|
443
|
+
isRead: false,
|
|
444
|
+
type: 'TEXT' as any,
|
|
445
|
+
parentId: null,
|
|
446
|
+
fromServer: false,
|
|
447
|
+
channel: {
|
|
448
|
+
__typename: 'Channel' as const,
|
|
449
|
+
id: channelId,
|
|
450
|
+
},
|
|
451
|
+
propsConfiguration: {
|
|
452
|
+
__typename: 'MachineConfiguration' as const,
|
|
453
|
+
resource: '' as any, // Cast to any to bypass the URI type check
|
|
454
|
+
},
|
|
455
|
+
props: {},
|
|
456
|
+
files: {
|
|
457
|
+
__typename: 'FilesInfo' as const,
|
|
458
|
+
data: [
|
|
459
|
+
{
|
|
460
|
+
__typename: 'FileInfo' as const,
|
|
461
|
+
id: objectId(),
|
|
462
|
+
url: selectedImage,
|
|
463
|
+
name: imagesToUpload[0]?.name || 'image.jpg',
|
|
464
|
+
extension: 'jpg',
|
|
465
|
+
mimeType: 'image/jpeg',
|
|
466
|
+
size: 0,
|
|
467
|
+
refType: FileRefType.Post,
|
|
468
|
+
height: imagesToUpload[0]?.height || 0,
|
|
469
|
+
width: imagesToUpload[0]?.width || 0,
|
|
470
|
+
},
|
|
471
|
+
] as any,
|
|
472
|
+
totalCount: 1,
|
|
473
|
+
},
|
|
474
|
+
replies: {
|
|
475
|
+
__typename: 'Messages' as const,
|
|
476
|
+
data: [],
|
|
477
|
+
totalCount: 0,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Upload the files
|
|
482
|
+
const uploadResponse = await startUpload({
|
|
483
|
+
file: imagesToUpload,
|
|
484
|
+
saveUploadedFile: {
|
|
485
|
+
variables: { postId },
|
|
486
|
+
},
|
|
487
|
+
createUploadLink: {
|
|
488
|
+
variables: { postId },
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (uploadResponse?.error) {
|
|
493
|
+
setLoading(false);
|
|
494
|
+
setUploadingMessageId(null);
|
|
495
|
+
return { error: String(uploadResponse.error) };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Get uploaded file IDs
|
|
499
|
+
const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
|
|
500
|
+
const files = uploadedFiles?.map((f: any) => f.id) ?? null;
|
|
501
|
+
|
|
502
|
+
// Send the message with the uploaded files
|
|
503
|
+
const response = await sendMsg({
|
|
504
|
+
variables: {
|
|
505
|
+
postId,
|
|
506
|
+
channelId,
|
|
507
|
+
content: currentMessageText || ' ', // Use a space if no text
|
|
508
|
+
files,
|
|
509
|
+
notificationParams: notificationData,
|
|
510
|
+
},
|
|
511
|
+
optimisticResponse: {
|
|
512
|
+
__typename: 'Mutation',
|
|
513
|
+
sendMessage: optimisticMessage,
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (response?.data?.sendMessage) {
|
|
518
|
+
// Clear the images after successful send
|
|
519
|
+
setSelectedImage('');
|
|
520
|
+
setImages([]);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
setLoading(false);
|
|
524
|
+
setUploadingMessageId(null);
|
|
525
|
+
return { message: response.data?.sendMessage };
|
|
526
|
+
} catch (error) {
|
|
527
|
+
setLoading(false);
|
|
528
|
+
setUploadingMessageId(null);
|
|
529
|
+
setError(String(error));
|
|
530
|
+
return { error: String(error) };
|
|
531
|
+
}
|
|
532
|
+
}, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth]);
|
|
533
|
+
|
|
534
|
+
// Create direct channel implementation
|
|
535
|
+
const createDirectChannelImpl = useCallback(async () => {
|
|
536
|
+
try {
|
|
537
|
+
setLoading(true);
|
|
538
|
+
if (
|
|
539
|
+
!rest?.isCreateNewChannel ||
|
|
540
|
+
rest?.newChannelData?.type !== RoomType?.Direct ||
|
|
541
|
+
!rest?.newChannelData?.userIds?.length
|
|
542
|
+
) {
|
|
543
|
+
setLoading(false);
|
|
544
|
+
return { error: 'Invalid channel data' };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Store current message text
|
|
548
|
+
const currentMessageText = messageText;
|
|
549
|
+
// Clear message text immediately for better UX
|
|
550
|
+
setMessageText('');
|
|
551
|
+
|
|
552
|
+
const response = await addDirectChannel({
|
|
553
|
+
variables: {
|
|
554
|
+
receiver: [...(rest?.newChannelData?.userIds ?? [])],
|
|
555
|
+
displayName: 'DIRECT CHANNEL',
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (!response?.data?.createDirectChannel?.id) {
|
|
560
|
+
setLoading(false);
|
|
561
|
+
return { error: 'Failed to create channel' };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const newChannelId = response.data.createDirectChannel.id;
|
|
565
|
+
setChannelId(newChannelId);
|
|
566
|
+
|
|
567
|
+
const notificationData: IExpoNotificationData = {
|
|
568
|
+
url: config.INBOX_MESSEGE_PATH,
|
|
569
|
+
params: { channelId: newChannelId, hideTabBar: true },
|
|
570
|
+
screen: 'DialogMessages',
|
|
571
|
+
other: { sound: Platform.OS === 'android' ? undefined : 'default' },
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// Create unique message ID for optimistic response
|
|
575
|
+
const messageId = objectId();
|
|
576
|
+
|
|
577
|
+
// Fix the createDirectChannelImpl optimisticMessage with all required fields
|
|
578
|
+
const optimisticMessage = {
|
|
579
|
+
__typename: 'Post' as const,
|
|
580
|
+
id: messageId,
|
|
581
|
+
message: currentMessageText,
|
|
582
|
+
createdAt: new Date().toISOString(),
|
|
583
|
+
updatedAt: new Date().toISOString(),
|
|
584
|
+
author: {
|
|
585
|
+
__typename: 'UserAccount' as const,
|
|
586
|
+
id: auth?.id,
|
|
587
|
+
givenName: auth?.givenName || '',
|
|
588
|
+
familyName: auth?.familyName || '',
|
|
589
|
+
picture: auth?.picture || '',
|
|
590
|
+
username: auth?.username || '',
|
|
591
|
+
email: auth?.email || '',
|
|
592
|
+
alias: [] as string[],
|
|
593
|
+
tokens: [],
|
|
594
|
+
},
|
|
595
|
+
isDelivered: true,
|
|
596
|
+
isRead: false,
|
|
597
|
+
type: 'TEXT' as any,
|
|
598
|
+
parentId: null,
|
|
599
|
+
fromServer: false,
|
|
600
|
+
channel: {
|
|
601
|
+
__typename: 'Channel' as const,
|
|
602
|
+
id: newChannelId,
|
|
603
|
+
},
|
|
604
|
+
propsConfiguration: {
|
|
605
|
+
__typename: 'MachineConfiguration' as const,
|
|
606
|
+
resource: '' as any, // Cast to any to bypass the URI type check
|
|
607
|
+
},
|
|
608
|
+
props: {},
|
|
609
|
+
files: {
|
|
610
|
+
__typename: 'FilesInfo' as const,
|
|
611
|
+
data: [],
|
|
612
|
+
totalCount: 0,
|
|
613
|
+
},
|
|
614
|
+
replies: {
|
|
615
|
+
__typename: 'Messages' as const,
|
|
616
|
+
data: [],
|
|
617
|
+
totalCount: 0,
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// Send message in the new channel
|
|
622
|
+
await sendMsg({
|
|
623
|
+
variables: {
|
|
624
|
+
channelId: newChannelId,
|
|
625
|
+
content: currentMessageText,
|
|
626
|
+
notificationParams: notificationData,
|
|
627
|
+
},
|
|
628
|
+
optimisticResponse: {
|
|
629
|
+
__typename: 'Mutation',
|
|
630
|
+
sendMessage: optimisticMessage,
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
setLoading(false);
|
|
635
|
+
return { channelId: newChannelId };
|
|
636
|
+
} catch (error) {
|
|
637
|
+
setLoading(false);
|
|
638
|
+
setError(String(error));
|
|
639
|
+
return { error: String(error) };
|
|
640
|
+
}
|
|
641
|
+
}, [rest, messageText, addDirectChannel, sendMsg, auth]);
|
|
642
|
+
|
|
643
|
+
// Optimize onFetchOld by adding debounce logic
|
|
644
|
+
const onFetchOld = useCallback(() => {
|
|
645
|
+
// Prevent multiple rapid calls
|
|
646
|
+
if (fetchOldDebounceRef.current) return;
|
|
647
|
+
|
|
648
|
+
// Check if we need to fetch more messages
|
|
649
|
+
if (totalCount > channelMessages.length && !loadingOldMessages) {
|
|
650
|
+
// Set debounce
|
|
651
|
+
fetchOldDebounceRef.current = true;
|
|
652
|
+
|
|
653
|
+
// Fetch more messages
|
|
654
|
+
fetchMoreMessagesImpl();
|
|
655
|
+
|
|
656
|
+
// Clear debounce after a timeout
|
|
657
|
+
setTimeout(() => {
|
|
658
|
+
fetchOldDebounceRef.current = false;
|
|
659
|
+
}, 1000);
|
|
660
|
+
}
|
|
661
|
+
}, [totalCount, channelMessages.length, loadingOldMessages, fetchMoreMessagesImpl]);
|
|
662
|
+
|
|
663
|
+
const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
|
|
664
|
+
const paddingToTop = 60;
|
|
665
|
+
return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// Transform the message data for GiftedChat
|
|
669
|
+
const messageList = useMemo(() => {
|
|
670
|
+
// Short-circuit if no messages to process
|
|
671
|
+
if (!channelMessages || channelMessages.length === 0) {
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Use a more efficient approach - pre-filter messages once
|
|
676
|
+
const filteredMessages = uniqBy(channelMessages, ({ id }) => id);
|
|
677
|
+
|
|
678
|
+
// Skip processing if no filtered messages
|
|
679
|
+
if (filteredMessages.length === 0) {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Transform messages only once and return
|
|
684
|
+
return orderBy(filteredMessages, ['createdAt'], ['desc']).map((msg) => {
|
|
685
|
+
const date = new Date(msg.createdAt);
|
|
686
|
+
|
|
687
|
+
// Extract image URL from files data
|
|
688
|
+
let imageUrl = null;
|
|
689
|
+
if (msg.files?.data && msg.files.data.length > 0) {
|
|
690
|
+
const fileData = msg.files.data[0];
|
|
691
|
+
if (fileData && fileData.url) {
|
|
692
|
+
imageUrl = fileData.url;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Create message in a more direct way
|
|
697
|
+
return {
|
|
698
|
+
_id: msg.id,
|
|
699
|
+
text: msg.message,
|
|
700
|
+
createdAt: date,
|
|
701
|
+
user: {
|
|
702
|
+
_id: msg.author?.id || '',
|
|
703
|
+
name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
|
|
704
|
+
avatar: msg.author?.picture || '',
|
|
705
|
+
},
|
|
706
|
+
image: imageUrl,
|
|
707
|
+
sent: msg?.isDelivered,
|
|
708
|
+
received: msg?.isRead,
|
|
709
|
+
type: msg?.type,
|
|
710
|
+
propsConfiguration: msg?.propsConfiguration,
|
|
711
|
+
replies: msg?.replies ?? [],
|
|
712
|
+
isShowThreadMessage,
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
}, [channelMessages, isShowThreadMessage]);
|
|
716
|
+
|
|
717
|
+
// Render the send button
|
|
718
|
+
const renderSend = useCallback(
|
|
719
|
+
(props) => {
|
|
720
|
+
// Enable the send button if there's text OR we have images
|
|
721
|
+
const hasContent = !!props.text || images?.length > 0;
|
|
722
|
+
const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
<Send
|
|
726
|
+
{...props}
|
|
727
|
+
disabled={!canSend}
|
|
728
|
+
containerStyle={{
|
|
729
|
+
justifyContent: 'center',
|
|
730
|
+
alignItems: 'center',
|
|
731
|
+
height: 40,
|
|
732
|
+
width: 44,
|
|
733
|
+
marginRight: 4,
|
|
734
|
+
marginBottom: 0,
|
|
735
|
+
marginLeft: 4,
|
|
736
|
+
}}
|
|
737
|
+
>
|
|
738
|
+
<View style={{ padding: 4 }}>
|
|
739
|
+
<MaterialCommunityIcons
|
|
740
|
+
name="send-circle"
|
|
741
|
+
size={32}
|
|
742
|
+
color={canSend ? colors.blue[500] : colors.gray[400]}
|
|
743
|
+
/>
|
|
744
|
+
</View>
|
|
745
|
+
</Send>
|
|
746
|
+
);
|
|
747
|
+
},
|
|
748
|
+
[channelId, images, rest?.isCreateNewChannel],
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
// Handle send for messages
|
|
752
|
+
const handleSend = useCallback(
|
|
753
|
+
async (messages) => {
|
|
754
|
+
// Extract message text from GiftedChat messages array
|
|
755
|
+
const newMessageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
|
|
756
|
+
|
|
757
|
+
// Check if we can send a message (channel exists or we're creating one)
|
|
758
|
+
if (!channelId && !rest?.isCreateNewChannel) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Allow sending if we have text OR images (image-only messages are valid)
|
|
763
|
+
const hasText = !!newMessageText && newMessageText !== ' ';
|
|
764
|
+
const hasImages = images && images.length > 0;
|
|
765
|
+
|
|
766
|
+
if (!hasText && !hasImages) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Update the message text state - now handled in send functions for better UX
|
|
771
|
+
setMessageText(newMessageText);
|
|
772
|
+
|
|
773
|
+
// Handle direct channel creation if needed
|
|
774
|
+
if (rest?.isCreateNewChannel && !channelId) {
|
|
775
|
+
if (rest?.newChannelData?.type === RoomType?.Direct) {
|
|
776
|
+
createDirectChannelImpl();
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Send message with or without image based on state
|
|
782
|
+
if (hasImages) {
|
|
783
|
+
sendMessageWithFileImpl();
|
|
784
|
+
} else {
|
|
785
|
+
sendMessageImpl();
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
[
|
|
789
|
+
channelId,
|
|
790
|
+
images,
|
|
791
|
+
rest?.isCreateNewChannel,
|
|
792
|
+
rest?.newChannelData?.type,
|
|
793
|
+
createDirectChannelImpl,
|
|
794
|
+
sendMessageWithFileImpl,
|
|
795
|
+
sendMessageImpl,
|
|
796
|
+
],
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
// Render message text with customizations for alerts and replies
|
|
800
|
+
const renderMessageText = useCallback(
|
|
801
|
+
(props: any) => {
|
|
802
|
+
const { currentMessage } = props;
|
|
803
|
+
const lastReply: any =
|
|
804
|
+
currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
|
|
805
|
+
|
|
806
|
+
if (currentMessage.type === 'ALERT') {
|
|
807
|
+
const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
|
|
808
|
+
let action: string = '';
|
|
809
|
+
let actionId: any = '';
|
|
810
|
+
let params: any = {};
|
|
811
|
+
|
|
812
|
+
if (attachment?.callToAction?.extraParams) {
|
|
813
|
+
const extraParams: any = attachment?.callToAction?.extraParams;
|
|
814
|
+
const route: any = extraParams?.route ?? null;
|
|
815
|
+
let path: any = null;
|
|
816
|
+
let param: any = null;
|
|
817
|
+
if (role && role == PreDefinedRole.Guest) {
|
|
818
|
+
path = route?.guest?.name ? route?.guest?.name ?? null : null;
|
|
819
|
+
param = route?.guest?.params ? route?.guest?.params ?? null : null;
|
|
820
|
+
} else if (role && role == PreDefinedRole.Owner) {
|
|
821
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
822
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
823
|
+
} else {
|
|
824
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
825
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
action = path;
|
|
829
|
+
params = { ...param };
|
|
830
|
+
} else if (attachment?.callToAction?.link) {
|
|
831
|
+
action = CALL_TO_ACTION_PATH;
|
|
832
|
+
actionId = attachment?.callToAction?.link.split('/').pop();
|
|
833
|
+
params = { reservationId: actionId };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return (
|
|
837
|
+
<>
|
|
838
|
+
{attachment?.callToAction && action ? (
|
|
839
|
+
<Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
|
|
840
|
+
<Button
|
|
841
|
+
variant={'outline'}
|
|
842
|
+
size={'sm'}
|
|
843
|
+
className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
|
|
844
|
+
onPress={() => action && params && navigation.navigate(action, params)}
|
|
845
|
+
>
|
|
846
|
+
<ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
|
|
847
|
+
{attachment.callToAction.title}
|
|
848
|
+
</ButtonText>
|
|
849
|
+
</Button>
|
|
850
|
+
<MessageText
|
|
851
|
+
{...props}
|
|
852
|
+
textStyle={{
|
|
853
|
+
left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
|
|
854
|
+
}}
|
|
855
|
+
/>
|
|
856
|
+
</Box>
|
|
857
|
+
) : (
|
|
858
|
+
<TouchableHighlight
|
|
859
|
+
underlayColor={'#c0c0c0'}
|
|
860
|
+
style={{ width: '100%' }}
|
|
861
|
+
onPress={() => {
|
|
862
|
+
if (currentMessage?.isShowThreadMessage)
|
|
863
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
864
|
+
channelId: channelId,
|
|
865
|
+
title: 'Message',
|
|
866
|
+
postParentId: currentMessage?._id,
|
|
867
|
+
isPostParentIdThread: true,
|
|
868
|
+
});
|
|
869
|
+
}}
|
|
870
|
+
>
|
|
871
|
+
<>
|
|
872
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
873
|
+
{currentMessage?.replies?.data?.length > 0 && (
|
|
874
|
+
<HStack space={'sm'} className="px-1 items-center">
|
|
875
|
+
<HStack>
|
|
876
|
+
{currentMessage?.replies?.data
|
|
877
|
+
?.filter(
|
|
878
|
+
(v: any, i: any, a: any) =>
|
|
879
|
+
a.findIndex((t: any) => t?.author?.id === v?.author?.id) ===
|
|
880
|
+
i,
|
|
881
|
+
)
|
|
882
|
+
?.slice(0, 2)
|
|
883
|
+
?.reverse()
|
|
884
|
+
?.map((p: any, i: Number) => (
|
|
885
|
+
<Avatar
|
|
886
|
+
key={'conversations-view-key-' + i}
|
|
887
|
+
size={'sm'}
|
|
888
|
+
className="bg-transparent"
|
|
889
|
+
>
|
|
890
|
+
<AvatarFallbackText>
|
|
891
|
+
{startCase(p?.author?.username?.charAt(0))}
|
|
892
|
+
</AvatarFallbackText>
|
|
893
|
+
<AvatarImage
|
|
894
|
+
alt="user image"
|
|
895
|
+
style={{
|
|
896
|
+
borderRadius: 6,
|
|
897
|
+
borderWidth: 2,
|
|
898
|
+
borderColor: '#fff',
|
|
899
|
+
}}
|
|
900
|
+
source={{
|
|
901
|
+
uri: p?.author?.picture,
|
|
902
|
+
}}
|
|
903
|
+
/>
|
|
904
|
+
</Avatar>
|
|
905
|
+
))}
|
|
906
|
+
</HStack>
|
|
907
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
908
|
+
{currentMessage?.replies?.totalCount}{' '}
|
|
909
|
+
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
910
|
+
</Text>
|
|
911
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
912
|
+
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
913
|
+
</Text>
|
|
914
|
+
</HStack>
|
|
915
|
+
)}
|
|
916
|
+
</>
|
|
917
|
+
</TouchableHighlight>
|
|
918
|
+
)}
|
|
919
|
+
</>
|
|
920
|
+
);
|
|
921
|
+
} else {
|
|
922
|
+
return (
|
|
923
|
+
<TouchableHighlight
|
|
924
|
+
underlayColor={'#c0c0c0'}
|
|
925
|
+
style={{ width: '100%' }}
|
|
926
|
+
onPress={() => {
|
|
927
|
+
if (currentMessage?.isShowThreadMessage)
|
|
928
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
929
|
+
channelId: channelId,
|
|
930
|
+
title: 'Message',
|
|
931
|
+
postParentId: currentMessage?._id,
|
|
932
|
+
isPostParentIdThread: true,
|
|
933
|
+
});
|
|
934
|
+
}}
|
|
935
|
+
>
|
|
936
|
+
<>
|
|
937
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
938
|
+
{currentMessage?.replies?.data?.length > 0 && (
|
|
939
|
+
<HStack space={'sm'} className="px-1 items-center">
|
|
940
|
+
<HStack>
|
|
941
|
+
{currentMessage?.replies?.data
|
|
942
|
+
?.filter(
|
|
943
|
+
(v: any, i: any, a: any) =>
|
|
944
|
+
a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
|
|
945
|
+
)
|
|
946
|
+
?.slice(0, 2)
|
|
947
|
+
?.reverse()
|
|
948
|
+
?.map((p: any, i: Number) => (
|
|
949
|
+
<Avatar
|
|
950
|
+
key={'conversation-replies-key-' + i}
|
|
951
|
+
className="bg-transparent"
|
|
952
|
+
size={'sm'}
|
|
953
|
+
>
|
|
954
|
+
<AvatarFallbackText>
|
|
955
|
+
{startCase(p?.author?.username?.charAt(0))}
|
|
956
|
+
</AvatarFallbackText>
|
|
957
|
+
<AvatarImage
|
|
958
|
+
alt="user image"
|
|
959
|
+
style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
|
|
960
|
+
source={{
|
|
961
|
+
uri: p?.author?.picture,
|
|
962
|
+
}}
|
|
963
|
+
/>
|
|
964
|
+
</Avatar>
|
|
965
|
+
))}
|
|
966
|
+
</HStack>
|
|
967
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
968
|
+
{currentMessage?.replies?.totalCount}{' '}
|
|
969
|
+
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
970
|
+
</Text>
|
|
971
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
972
|
+
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
973
|
+
</Text>
|
|
974
|
+
</HStack>
|
|
975
|
+
)}
|
|
976
|
+
</>
|
|
977
|
+
</TouchableHighlight>
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
[navigation, channelId, role],
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
// Render action buttons (including image upload)
|
|
985
|
+
const renderActions = (props) => {
|
|
986
|
+
return (
|
|
987
|
+
<Actions
|
|
988
|
+
{...props}
|
|
989
|
+
options={{
|
|
990
|
+
['Choose from Library']: onSelectImages,
|
|
991
|
+
['Cancel']: () => {}, // Add this option to make the sheet dismissible
|
|
992
|
+
}}
|
|
993
|
+
optionTintColor="#000000"
|
|
994
|
+
cancelButtonIndex={1} // Set the Cancel option as the cancel button
|
|
995
|
+
icon={() => (
|
|
996
|
+
<Box
|
|
997
|
+
style={{
|
|
998
|
+
width: 32,
|
|
999
|
+
height: 32,
|
|
1000
|
+
alignItems: 'center',
|
|
1001
|
+
justifyContent: 'center',
|
|
1002
|
+
}}
|
|
1003
|
+
>
|
|
1004
|
+
<Ionicons name="image" size={24} color={colors.blue[500]} />
|
|
1005
|
+
</Box>
|
|
1006
|
+
)}
|
|
1007
|
+
containerStyle={{
|
|
1008
|
+
alignItems: 'center',
|
|
1009
|
+
justifyContent: 'center',
|
|
1010
|
+
marginLeft: 8,
|
|
1011
|
+
marginBottom: 0,
|
|
1012
|
+
}}
|
|
1013
|
+
/>
|
|
1014
|
+
);
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// Create a more visible and reliable image preview with cancel button
|
|
1018
|
+
const renderAccessory = useCallback(() => {
|
|
1019
|
+
if (!selectedImage) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return (
|
|
1024
|
+
<View
|
|
1025
|
+
style={{
|
|
1026
|
+
height: 70,
|
|
1027
|
+
backgroundColor: 'white',
|
|
1028
|
+
borderTopWidth: 1,
|
|
1029
|
+
borderTopColor: '#e0e0e0',
|
|
1030
|
+
flexDirection: 'row',
|
|
1031
|
+
alignItems: 'center',
|
|
1032
|
+
margin: 0,
|
|
1033
|
+
padding: 0,
|
|
1034
|
+
paddingVertical: 0,
|
|
1035
|
+
position: 'absolute',
|
|
1036
|
+
bottom: Platform.OS === 'ios' ? 105 : 95, // Position well above the input area
|
|
1037
|
+
left: 0,
|
|
1038
|
+
right: 0,
|
|
1039
|
+
zIndex: 1,
|
|
1040
|
+
elevation: 3,
|
|
1041
|
+
shadowColor: '#000',
|
|
1042
|
+
shadowOffset: { width: 0, height: -1 },
|
|
1043
|
+
shadowOpacity: 0.05,
|
|
1044
|
+
shadowRadius: 2,
|
|
1045
|
+
}}
|
|
1046
|
+
>
|
|
1047
|
+
<View
|
|
1048
|
+
style={{
|
|
1049
|
+
flex: 1,
|
|
1050
|
+
flexDirection: 'row',
|
|
1051
|
+
alignItems: 'center',
|
|
1052
|
+
paddingLeft: 15,
|
|
1053
|
+
paddingRight: 5,
|
|
1054
|
+
}}
|
|
1055
|
+
>
|
|
1056
|
+
<View
|
|
1057
|
+
style={{
|
|
1058
|
+
width: 56,
|
|
1059
|
+
height: 56,
|
|
1060
|
+
marginRight: 15,
|
|
1061
|
+
borderRadius: 4,
|
|
1062
|
+
backgroundColor: colors.gray[200],
|
|
1063
|
+
overflow: 'hidden',
|
|
1064
|
+
borderWidth: 1,
|
|
1065
|
+
borderColor: '#e0e0e0',
|
|
1066
|
+
}}
|
|
1067
|
+
>
|
|
1068
|
+
<Image
|
|
1069
|
+
key={selectedImage}
|
|
1070
|
+
alt={'selected image'}
|
|
1071
|
+
source={{ uri: selectedImage }}
|
|
1072
|
+
style={{
|
|
1073
|
+
width: '100%',
|
|
1074
|
+
height: '100%',
|
|
1075
|
+
}}
|
|
1076
|
+
size={'md'}
|
|
1077
|
+
/>
|
|
1078
|
+
{loading && (
|
|
1079
|
+
<View
|
|
1080
|
+
style={{
|
|
1081
|
+
position: 'absolute',
|
|
1082
|
+
top: 0,
|
|
1083
|
+
left: 0,
|
|
1084
|
+
right: 0,
|
|
1085
|
+
bottom: 0,
|
|
1086
|
+
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
|
1087
|
+
justifyContent: 'center',
|
|
1088
|
+
alignItems: 'center',
|
|
1089
|
+
}}
|
|
1090
|
+
>
|
|
1091
|
+
<Spinner size="small" color={colors.blue[500]} />
|
|
1092
|
+
</View>
|
|
1093
|
+
)}
|
|
1094
|
+
</View>
|
|
1095
|
+
|
|
1096
|
+
<View style={{ flex: 1 }}>
|
|
1097
|
+
<Text style={{ fontSize: 14, fontWeight: '400', color: colors.gray[800] }}>
|
|
1098
|
+
{images[0]?.fileName || 'image_' + new Date().getTime() + '.jpg'}
|
|
1099
|
+
</Text>
|
|
1100
|
+
<Text style={{ fontSize: 12, color: colors.gray[500], marginTop: 2 }}>
|
|
1101
|
+
{loading ? 'Preparing...' : 'Ready to send'}
|
|
1102
|
+
</Text>
|
|
1103
|
+
</View>
|
|
1104
|
+
|
|
1105
|
+
<TouchableHighlight
|
|
1106
|
+
underlayColor={'rgba(0,0,0,0.1)'}
|
|
1107
|
+
onPress={() => {
|
|
1108
|
+
setSelectedImage('');
|
|
1109
|
+
setImages([]);
|
|
1110
|
+
}}
|
|
1111
|
+
style={{
|
|
1112
|
+
backgroundColor: colors.red[500],
|
|
1113
|
+
borderRadius: 24,
|
|
1114
|
+
width: 36,
|
|
1115
|
+
height: 36,
|
|
1116
|
+
alignItems: 'center',
|
|
1117
|
+
justifyContent: 'center',
|
|
1118
|
+
marginRight: 10,
|
|
1119
|
+
}}
|
|
1120
|
+
>
|
|
1121
|
+
<Ionicons name="close" size={20} color="white" />
|
|
1122
|
+
</TouchableHighlight>
|
|
1123
|
+
</View>
|
|
1124
|
+
</View>
|
|
1125
|
+
);
|
|
1126
|
+
}, [selectedImage, loading, images]);
|
|
1127
|
+
|
|
1128
|
+
const setImageViewerObject = (obj: any, v: boolean) => {
|
|
1129
|
+
setImageObject(obj);
|
|
1130
|
+
setImageViewer(v);
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const modalContent = React.useMemo(() => {
|
|
1134
|
+
if (!imageObject || !imageObject.image) return null;
|
|
1135
|
+
const { image, _id } = imageObject;
|
|
1136
|
+
|
|
1137
|
+
return (
|
|
1138
|
+
<CachedImage
|
|
1139
|
+
style={{ width: '100%', height: '100%' }}
|
|
1140
|
+
resizeMode={'cover'}
|
|
1141
|
+
cacheKey={`${_id}-modal-imageKey`}
|
|
1142
|
+
source={{
|
|
1143
|
+
uri: image,
|
|
1144
|
+
expiresIn: 86400,
|
|
1145
|
+
}}
|
|
1146
|
+
alt={'image'}
|
|
1147
|
+
/>
|
|
1148
|
+
);
|
|
1149
|
+
}, [imageObject]);
|
|
1150
|
+
|
|
1151
|
+
// Create a skeleton component for message bubbles with images
|
|
1152
|
+
const renderMessage = useCallback(
|
|
1153
|
+
(props: any) => {
|
|
1154
|
+
// Check if this message ID matches the uploading message ID
|
|
1155
|
+
const isUploading = props.currentMessage._id === uploadingMessageId && loading;
|
|
1156
|
+
|
|
1157
|
+
if (isUploading && props.currentMessage.image) {
|
|
1158
|
+
// Return a custom message skeleton during upload
|
|
1159
|
+
return (
|
|
1160
|
+
<View
|
|
1161
|
+
style={{
|
|
1162
|
+
padding: 10,
|
|
1163
|
+
marginBottom: 10,
|
|
1164
|
+
marginRight: 10,
|
|
1165
|
+
alignSelf: 'flex-end',
|
|
1166
|
+
borderRadius: 15,
|
|
1167
|
+
backgroundColor: colors.gray[100],
|
|
1168
|
+
maxWidth: '80%',
|
|
1169
|
+
}}
|
|
1170
|
+
>
|
|
1171
|
+
{props.currentMessage.text && props.currentMessage.text.trim() !== '' && (
|
|
1172
|
+
<Box
|
|
1173
|
+
style={{
|
|
1174
|
+
height: 15,
|
|
1175
|
+
borderRadius: 4,
|
|
1176
|
+
backgroundColor: colors.gray[200],
|
|
1177
|
+
overflow: 'hidden',
|
|
1178
|
+
marginBottom: 8,
|
|
1179
|
+
}}
|
|
1180
|
+
>
|
|
1181
|
+
<Skeleton variant="rounded" style={{ flex: 1 }} />
|
|
1182
|
+
</Box>
|
|
1183
|
+
)}
|
|
1184
|
+
<Box
|
|
1185
|
+
style={{
|
|
1186
|
+
height: 150,
|
|
1187
|
+
width: 150,
|
|
1188
|
+
borderRadius: 10,
|
|
1189
|
+
backgroundColor: colors.gray[200],
|
|
1190
|
+
overflow: 'hidden',
|
|
1191
|
+
}}
|
|
1192
|
+
>
|
|
1193
|
+
<Skeleton variant="rounded" style={{ flex: 1 }} />
|
|
1194
|
+
</Box>
|
|
1195
|
+
</View>
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Use memo to prevent unnecessary re-renders of each message
|
|
1200
|
+
return (
|
|
1201
|
+
<SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
|
|
1202
|
+
);
|
|
1203
|
+
},
|
|
1204
|
+
[isShowImageViewer, uploadingMessageId, loading],
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
let onScroll = false;
|
|
1208
|
+
|
|
1209
|
+
// Optimize onMomentumScrollBegin for better scroll performance
|
|
1210
|
+
const onMomentumScrollBegin = async ({ nativeEvent }: any) => {
|
|
1211
|
+
// Set scroll state
|
|
1212
|
+
onScroll = true;
|
|
1213
|
+
|
|
1214
|
+
// Use the debounced fetch function to prevent excessive calls
|
|
1215
|
+
if (isCloseToTop(nativeEvent)) {
|
|
1216
|
+
onFetchOld();
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
const onEndReached = () => {
|
|
1221
|
+
if (!onScroll) return;
|
|
1222
|
+
onScroll = false;
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// Add a loader for when more messages are being loaded
|
|
1226
|
+
const renderLoadEarlier = useCallback(() => {
|
|
1227
|
+
return loadingOldMessages ? (
|
|
1228
|
+
<View
|
|
1229
|
+
style={{
|
|
1230
|
+
padding: 10,
|
|
1231
|
+
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
1232
|
+
borderRadius: 10,
|
|
1233
|
+
marginTop: 10,
|
|
1234
|
+
}}
|
|
1235
|
+
>
|
|
1236
|
+
<Spinner size="small" color="#3b82f6" />
|
|
1237
|
+
</View>
|
|
1238
|
+
) : null;
|
|
1239
|
+
}, [loadingOldMessages]);
|
|
1240
|
+
|
|
1241
|
+
// Add renderInputToolbar function
|
|
1242
|
+
const renderInputToolbar = useCallback((props) => {
|
|
1243
|
+
return (
|
|
1244
|
+
<InputToolbar
|
|
1245
|
+
{...props}
|
|
1246
|
+
containerStyle={{
|
|
1247
|
+
backgroundColor: 'white',
|
|
1248
|
+
borderTopWidth: 1,
|
|
1249
|
+
borderTopColor: colors.gray[200],
|
|
1250
|
+
paddingHorizontal: 4,
|
|
1251
|
+
paddingVertical: 0,
|
|
1252
|
+
paddingTop: 2,
|
|
1253
|
+
marginBottom: 0,
|
|
1254
|
+
marginTop: 0,
|
|
1255
|
+
}}
|
|
1256
|
+
primaryStyle={{
|
|
1257
|
+
alignItems: 'center',
|
|
1258
|
+
}}
|
|
1259
|
+
/>
|
|
1260
|
+
);
|
|
1261
|
+
}, []);
|
|
1262
|
+
|
|
1263
|
+
// Create a memoized ImageViewerModal component
|
|
1264
|
+
const imageViewerModal = useMemo(
|
|
1265
|
+
() => (
|
|
1266
|
+
<ImageViewerModal isVisible={isShowImageViewer} setVisible={setImageViewer} modalContent={modalContent} />
|
|
1267
|
+
),
|
|
1268
|
+
[isShowImageViewer, modalContent],
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
// Create a memoized subscription handler component
|
|
1272
|
+
const subscriptionHandler = useMemo(
|
|
1273
|
+
() => (
|
|
1274
|
+
<SubscriptionHandler
|
|
1275
|
+
channelId={channelId?.toString()}
|
|
1276
|
+
subscribeToNewMessages={() =>
|
|
1277
|
+
subscribeToMore({
|
|
1278
|
+
document: CHAT_MESSAGE_ADDED,
|
|
1279
|
+
variables: {
|
|
1280
|
+
channelId: channelId?.toString(),
|
|
1281
|
+
},
|
|
1282
|
+
// updateQuery: (prev, { subscriptionData }: any) => {
|
|
1283
|
+
// try {
|
|
1284
|
+
// // Check if we have valid subscription data
|
|
1285
|
+
// if (!subscriptionData?.data?.chatMessageAdded) {
|
|
1286
|
+
// return prev;
|
|
1287
|
+
// }
|
|
1288
|
+
|
|
1289
|
+
// const newMessage = subscriptionData.data.chatMessageAdded;
|
|
1290
|
+
|
|
1291
|
+
// // Check if message is from current user - skip update as it's handled by optimistic UI
|
|
1292
|
+
// if (newMessage.author?.id === auth?.id) {
|
|
1293
|
+
// return prev;
|
|
1294
|
+
// }
|
|
1295
|
+
|
|
1296
|
+
// // Check if we already have this message to avoid duplicates
|
|
1297
|
+
// const currentMessages = prev?.messages?.data || [];
|
|
1298
|
+
// if (currentMessages.some((msg) => msg.id === newMessage.id)) {
|
|
1299
|
+
// return prev;
|
|
1300
|
+
// }
|
|
1301
|
+
|
|
1302
|
+
// // Update Apollo cache
|
|
1303
|
+
// const updatedData = {
|
|
1304
|
+
// ...prev,
|
|
1305
|
+
// messages: {
|
|
1306
|
+
// ...prev.messages,
|
|
1307
|
+
// data: [newMessage, ...currentMessages],
|
|
1308
|
+
// totalCount: (prev?.messages?.totalCount || 0) + 1,
|
|
1309
|
+
// },
|
|
1310
|
+
// };
|
|
1311
|
+
|
|
1312
|
+
// return updatedData;
|
|
1313
|
+
// } catch (error) {
|
|
1314
|
+
// return prev;
|
|
1315
|
+
// }
|
|
1316
|
+
// },
|
|
1317
|
+
})
|
|
1318
|
+
}
|
|
1319
|
+
/>
|
|
1320
|
+
),
|
|
1321
|
+
[channelId, subscribeToMore, auth?.id],
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
// Create a memoized renderChatFooter function
|
|
1325
|
+
const renderChatFooter = useCallback(() => {
|
|
1326
|
+
return (
|
|
1327
|
+
<>
|
|
1328
|
+
{imageViewerModal}
|
|
1329
|
+
{subscriptionHandler}
|
|
1330
|
+
</>
|
|
1331
|
+
);
|
|
1332
|
+
}, [imageViewerModal, subscriptionHandler]);
|
|
1333
|
+
|
|
1334
|
+
// Add optimized listViewProps to reduce re-renders and improve list performance
|
|
1335
|
+
const listViewProps = useMemo(
|
|
1336
|
+
() => ({
|
|
1337
|
+
onEndReached: onEndReached,
|
|
1338
|
+
onEndReachedThreshold: 0.5,
|
|
1339
|
+
onMomentumScrollBegin: onMomentumScrollBegin,
|
|
1340
|
+
removeClippedSubviews: true, // Improve performance by unmounting components when not visible
|
|
1341
|
+
initialNumToRender: 10, // Reduce initial render amount
|
|
1342
|
+
maxToRenderPerBatch: 7, // Reduce number in each render batch
|
|
1343
|
+
windowSize: 7, // Reduce the window size
|
|
1344
|
+
updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
|
|
1345
|
+
keyExtractor: (item) => item._id, // Add explicit key extractor
|
|
1346
|
+
}),
|
|
1347
|
+
[onEndReached, onMomentumScrollBegin],
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
// Return optimized component with performance improvements
|
|
1351
|
+
return (
|
|
1352
|
+
<View
|
|
1353
|
+
style={{
|
|
1354
|
+
flex: 1,
|
|
1355
|
+
backgroundColor: 'white',
|
|
1356
|
+
position: 'relative',
|
|
1357
|
+
}}
|
|
1358
|
+
>
|
|
1359
|
+
{messageLoading && <Spinner color={'#3b82f6'} />}
|
|
1360
|
+
|
|
1361
|
+
{/* Render the image preview directly in the container so it's properly positioned */}
|
|
1362
|
+
{selectedImage ? renderAccessory() : null}
|
|
1363
|
+
|
|
1364
|
+
<GiftedChat
|
|
1365
|
+
ref={messageRootListRef}
|
|
1366
|
+
wrapInSafeArea={true}
|
|
1367
|
+
renderLoading={() => <Spinner color={'#3b82f6'} />}
|
|
1368
|
+
messages={messageList}
|
|
1369
|
+
listViewProps={{
|
|
1370
|
+
...listViewProps,
|
|
1371
|
+
contentContainerStyle: {
|
|
1372
|
+
paddingBottom: selectedImage ? 90 : 0, // Add padding at the bottom when image is selected
|
|
1373
|
+
},
|
|
1374
|
+
}}
|
|
1375
|
+
onSend={handleSend}
|
|
1376
|
+
text={messageText || ' '}
|
|
1377
|
+
onInputTextChanged={(text) => setMessageText(text)}
|
|
1378
|
+
renderFooter={() => (loading && !images.length ? <Spinner color={'#3b82f6'} /> : null)}
|
|
1379
|
+
scrollToBottom
|
|
1380
|
+
user={{
|
|
1381
|
+
_id: auth?.id || '',
|
|
1382
|
+
}}
|
|
1383
|
+
isTyping={false} // Setting to false to reduce animations
|
|
1384
|
+
alwaysShowSend={true} // Always show send button regardless of text content
|
|
1385
|
+
renderSend={renderSend}
|
|
1386
|
+
renderMessageText={renderMessageText}
|
|
1387
|
+
renderInputToolbar={renderInputToolbar}
|
|
1388
|
+
minInputToolbarHeight={50}
|
|
1389
|
+
renderActions={channelId && renderActions}
|
|
1390
|
+
renderMessage={renderMessage}
|
|
1391
|
+
renderChatFooter={renderChatFooter}
|
|
1392
|
+
renderLoadEarlier={renderLoadEarlier}
|
|
1393
|
+
loadEarlier={totalCount > channelMessages.length}
|
|
1394
|
+
isLoadingEarlier={loadingOldMessages}
|
|
1395
|
+
bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0} // Adjust bottom offset based on image preview
|
|
1396
|
+
textInputProps={{
|
|
1397
|
+
style: {
|
|
1398
|
+
borderWidth: 1,
|
|
1399
|
+
borderColor: colors.gray[300],
|
|
1400
|
+
backgroundColor: '#f8f8f8',
|
|
1401
|
+
borderRadius: 20,
|
|
1402
|
+
minHeight: 36,
|
|
1403
|
+
maxHeight: 80,
|
|
1404
|
+
color: '#000',
|
|
1405
|
+
padding: 8,
|
|
1406
|
+
paddingHorizontal: 15,
|
|
1407
|
+
fontSize: 16,
|
|
1408
|
+
flex: 1,
|
|
1409
|
+
marginVertical: 2,
|
|
1410
|
+
marginBottom: 0,
|
|
1411
|
+
},
|
|
1412
|
+
multiline: true,
|
|
1413
|
+
returnKeyType: 'default',
|
|
1414
|
+
enablesReturnKeyAutomatically: true,
|
|
1415
|
+
placeholderTextColor: colors.gray[400],
|
|
1416
|
+
}}
|
|
1417
|
+
minComposerHeight={36}
|
|
1418
|
+
maxComposerHeight={100}
|
|
1419
|
+
isKeyboardInternallyHandled={true}
|
|
1420
|
+
placeholder="Type a message..."
|
|
1421
|
+
lightboxProps={{
|
|
1422
|
+
underlayColor: 'transparent',
|
|
1423
|
+
springConfig: { tension: 90000, friction: 90000 },
|
|
1424
|
+
disabled: true,
|
|
1425
|
+
}}
|
|
1426
|
+
infiniteScroll={false} // Disable automatic loading
|
|
1427
|
+
/>
|
|
1428
|
+
</View>
|
|
1429
|
+
);
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: ISubscriptionHandlerProps) => {
|
|
1433
|
+
// Store the channelId in a ref to track changes
|
|
1434
|
+
const channelIdRef = useRef(channelId);
|
|
1435
|
+
|
|
1436
|
+
useEffect(() => {
|
|
1437
|
+
// Don't set up subscription if there's no channel ID
|
|
1438
|
+
if (!channelId) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Call the subscribe function and store the unsubscribe function
|
|
1443
|
+
const unsubscribe = subscribeToNewMessages();
|
|
1444
|
+
|
|
1445
|
+
// Update the ref with the current channelId
|
|
1446
|
+
channelIdRef.current = channelId;
|
|
1447
|
+
|
|
1448
|
+
// Return cleanup function
|
|
1449
|
+
return () => {
|
|
1450
|
+
if (unsubscribe && typeof unsubscribe === 'function') {
|
|
1451
|
+
unsubscribe();
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
}, [channelId, subscribeToNewMessages]);
|
|
1455
|
+
|
|
1456
|
+
return null;
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
// Export with React.memo to prevent unnecessary re-renders
|
|
1460
|
+
export const ConversationView = React.memo(ConversationViewComponent, (prevProps, nextProps) => {
|
|
1461
|
+
// Only re-render if these critical props change
|
|
1462
|
+
return (
|
|
1463
|
+
prevProps.channelId === nextProps.channelId &&
|
|
1464
|
+
prevProps.role === nextProps.role &&
|
|
1465
|
+
prevProps.isShowThreadMessage === nextProps.isShowThreadMessage
|
|
1466
|
+
);
|
|
1467
|
+
});
|