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