@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
|
@@ -14,8 +14,22 @@ import {
|
|
|
14
14
|
Spinner,
|
|
15
15
|
Text,
|
|
16
16
|
Skeleton,
|
|
17
|
+
Toast,
|
|
18
|
+
ToastTitle,
|
|
19
|
+
ToastDescription,
|
|
20
|
+
useToast,
|
|
21
|
+
Divider,
|
|
17
22
|
} from '@admin-layout/gluestack-ui-mobile';
|
|
18
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
Platform,
|
|
25
|
+
TouchableHighlight,
|
|
26
|
+
View,
|
|
27
|
+
TouchableOpacity,
|
|
28
|
+
ScrollView,
|
|
29
|
+
Animated,
|
|
30
|
+
Keyboard,
|
|
31
|
+
RefreshControl,
|
|
32
|
+
} from 'react-native';
|
|
19
33
|
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
|
|
20
34
|
import { useSelector } from 'react-redux';
|
|
21
35
|
import { orderBy, startCase, uniqBy } from 'lodash-es';
|
|
@@ -33,14 +47,11 @@ import {
|
|
|
33
47
|
PostTypeEnum,
|
|
34
48
|
} from 'common';
|
|
35
49
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
useThreadMessagesLazyQuery,
|
|
42
|
-
useGetPostThreadLazyQuery,
|
|
43
|
-
} from 'common/graphql';
|
|
50
|
+
CHAT_MESSAGE_ADDED,
|
|
51
|
+
useCreatePostThread,
|
|
52
|
+
useSendExpoNotification,
|
|
53
|
+
useGetPostThreadLazy,
|
|
54
|
+
} from '../../../queries/inboxQueries';
|
|
44
55
|
import { useUploadFilesNative } from '@messenger-box/platform-client';
|
|
45
56
|
import { objectId } from '@messenger-box/core';
|
|
46
57
|
import { format, isToday, isYesterday } from 'date-fns';
|
|
@@ -49,6 +60,8 @@ import { config } from '../config';
|
|
|
49
60
|
import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
|
|
50
61
|
import CachedImage from '../components/CachedImage';
|
|
51
62
|
import colors from 'tailwindcss/colors';
|
|
63
|
+
import ExpandableInputActionSheet from '../components/ExpandableInputActionSheet';
|
|
64
|
+
import GiftedChatInboxComponent from '../components/GiftedChatInboxComponent';
|
|
52
65
|
|
|
53
66
|
const {
|
|
54
67
|
MESSAGES_PER_PAGE,
|
|
@@ -69,6 +82,9 @@ const createdAtText = (value: string) => {
|
|
|
69
82
|
interface IMessageProps extends IMessage {
|
|
70
83
|
type: string;
|
|
71
84
|
propsConfiguration?: any;
|
|
85
|
+
images?: string[];
|
|
86
|
+
isOptimistic?: boolean;
|
|
87
|
+
isUploading?: boolean;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
export interface AlertMessageAttachmentsInterface {
|
|
@@ -93,13 +109,72 @@ interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
|
|
|
93
109
|
mimeType?: string;
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
// Custom notification component
|
|
113
|
+
const ErrorNotification = ({ message, onClose }) => {
|
|
114
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
// Fade in
|
|
118
|
+
Animated.timing(opacity, {
|
|
119
|
+
toValue: 1,
|
|
120
|
+
duration: 300,
|
|
121
|
+
useNativeDriver: true,
|
|
122
|
+
}).start();
|
|
123
|
+
|
|
124
|
+
// Auto hide after 4 seconds
|
|
125
|
+
const timer = setTimeout(() => {
|
|
126
|
+
Animated.timing(opacity, {
|
|
127
|
+
toValue: 0,
|
|
128
|
+
duration: 300,
|
|
129
|
+
useNativeDriver: true,
|
|
130
|
+
}).start(() => onClose && onClose());
|
|
131
|
+
}, 4000);
|
|
132
|
+
|
|
133
|
+
return () => clearTimeout(timer);
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Animated.View
|
|
138
|
+
style={{
|
|
139
|
+
position: 'absolute',
|
|
140
|
+
top: 10,
|
|
141
|
+
left: 10,
|
|
142
|
+
right: 10,
|
|
143
|
+
backgroundColor: '#f44336',
|
|
144
|
+
padding: 15,
|
|
145
|
+
borderRadius: 8,
|
|
146
|
+
shadowColor: '#000',
|
|
147
|
+
shadowOffset: { width: 0, height: 2 },
|
|
148
|
+
shadowOpacity: 0.25,
|
|
149
|
+
shadowRadius: 3.84,
|
|
150
|
+
elevation: 5,
|
|
151
|
+
zIndex: 1000,
|
|
152
|
+
opacity,
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<HStack className="items-center justify-between">
|
|
156
|
+
<Text style={{ color: 'white', fontWeight: 'bold' }}>Upload Failed</Text>
|
|
157
|
+
<TouchableOpacity onPress={onClose}>
|
|
158
|
+
<Ionicons name="close" size={20} color="white" />
|
|
159
|
+
</TouchableOpacity>
|
|
160
|
+
</HStack>
|
|
161
|
+
<Text style={{ color: 'white', marginTop: 5 }}>{message}</Text>
|
|
162
|
+
</Animated.View>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Add a utility function for file validation
|
|
167
|
+
const isValidFileUrl = (url: string | undefined): boolean => {
|
|
168
|
+
return !!url && typeof url === 'string' && url.length > 0;
|
|
169
|
+
};
|
|
170
|
+
|
|
96
171
|
const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any) => {
|
|
97
172
|
const { params } = useRoute<any>();
|
|
98
173
|
const [channelToTop, setChannelToTop] = useState(0);
|
|
99
174
|
const [channelMessages, setChannelMessages] = useState<any>([]);
|
|
100
175
|
const auth: any = useSelector(userSelector);
|
|
101
176
|
const [totalCount, setTotalCount] = useState<any>(0);
|
|
102
|
-
const [selectedImage,
|
|
177
|
+
const [selectedImage, setSelectedImage] = useState<string>('');
|
|
103
178
|
const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
|
|
104
179
|
const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
|
|
105
180
|
const navigation = useNavigation<any>();
|
|
@@ -118,15 +193,30 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
118
193
|
const [threadPost, setThreadPost] = useState<any[]>([]);
|
|
119
194
|
const [isScrollToBottom, setIsScrollToBottom] = useState(false);
|
|
120
195
|
const threadMessageListRef = useRef<any>(null);
|
|
196
|
+
const toast = useToast();
|
|
197
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
198
|
+
|
|
199
|
+
// Add state for expandable input action sheet
|
|
200
|
+
const [isActionSheetVisible, setActionSheetVisible] = useState(false);
|
|
201
|
+
const [textUpdatedInActionSheet, setTextUpdatedInActionSheet] = useState(false);
|
|
202
|
+
const [lastShownMsg, setLastShownMsg] = useState('');
|
|
203
|
+
const textInputRef = useRef(null);
|
|
121
204
|
|
|
122
205
|
// const [sendThreadMessage] = useSendThreadMessageMutation();
|
|
123
|
-
const [sendThreadMessage] =
|
|
124
|
-
const [sendExpoNotificationOnPostMutation] =
|
|
206
|
+
const [sendThreadMessage] = useCreatePostThread();
|
|
207
|
+
const [sendExpoNotificationOnPostMutation] = useSendExpoNotification();
|
|
125
208
|
|
|
126
209
|
const [
|
|
127
210
|
getThreadMessages,
|
|
128
211
|
{ data, loading: threadLoading, fetchMore: fetchMoreMessages, refetch: refetchThreadMessages, subscribeToMore },
|
|
129
|
-
] =
|
|
212
|
+
] = useGetPostThreadLazy({ fetchPolicy: 'cache-and-network' });
|
|
213
|
+
|
|
214
|
+
// Add new state for tracking uploads
|
|
215
|
+
const [pendingUploads, setPendingUploads] = useState<Record<string, any>>({});
|
|
216
|
+
const [uploadErrors, setUploadErrors] = useState<Record<string, string>>({});
|
|
217
|
+
const [isUploadingImage, setIsUploadingImage] = useState(false);
|
|
218
|
+
|
|
219
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
130
220
|
|
|
131
221
|
useFocusEffect(
|
|
132
222
|
React.useCallback(() => {
|
|
@@ -216,6 +306,8 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
216
306
|
const onFetchOld = useCallback(() => {
|
|
217
307
|
if (totalCount > channelMessages.length && !loadingOldMessages) {
|
|
218
308
|
setLoadEarlierMsg(true);
|
|
309
|
+
setLoadingOldMessages(true);
|
|
310
|
+
|
|
219
311
|
fetchMoreMessages({
|
|
220
312
|
variables: {
|
|
221
313
|
channelId: channelId?.toString(),
|
|
@@ -237,14 +329,24 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
237
329
|
.finally(() => {
|
|
238
330
|
setLoadEarlierMsg(false);
|
|
239
331
|
setLoadingOldMessages(false);
|
|
332
|
+
setRefreshing(false);
|
|
240
333
|
})
|
|
241
334
|
.catch((error: any) => {
|
|
242
335
|
setLoadEarlierMsg(false);
|
|
243
336
|
setLoadingOldMessages(false);
|
|
337
|
+
setRefreshing(false);
|
|
244
338
|
});
|
|
339
|
+
} else {
|
|
340
|
+
setRefreshing(false);
|
|
245
341
|
}
|
|
246
342
|
}, [parentId, channelId, totalCount, channelMessages]);
|
|
247
343
|
|
|
344
|
+
// Pull to refresh handler
|
|
345
|
+
const onRefresh = useCallback(() => {
|
|
346
|
+
setRefreshing(true);
|
|
347
|
+
onFetchOld();
|
|
348
|
+
}, [onFetchOld]);
|
|
349
|
+
|
|
248
350
|
// const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
|
|
249
351
|
// return contentOffset.y <= 100; // 100px from top
|
|
250
352
|
// };
|
|
@@ -266,81 +368,206 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
266
368
|
return new File([u8arr], filename, { type: mime });
|
|
267
369
|
};
|
|
268
370
|
|
|
371
|
+
// Add a helper function for removing messages from the UI when uploads fail
|
|
372
|
+
const removeMessageFromUI = useCallback((messageId: string) => {
|
|
373
|
+
// Remove from pending uploads
|
|
374
|
+
setPendingUploads((prev) => {
|
|
375
|
+
const newPending = { ...prev };
|
|
376
|
+
delete newPending[messageId];
|
|
377
|
+
return newPending;
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Also remove any error state
|
|
381
|
+
setUploadErrors((prev) => {
|
|
382
|
+
const newErrors = { ...prev };
|
|
383
|
+
delete newErrors[messageId];
|
|
384
|
+
return newErrors;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Reset upload state to ensure we don't get stuck with loading indicator
|
|
388
|
+
setIsUploadingImage(false);
|
|
389
|
+
}, []);
|
|
390
|
+
|
|
391
|
+
// const onSelectImages = async () => {
|
|
392
|
+
// setImageLoading(true);
|
|
393
|
+
|
|
394
|
+
// try {
|
|
395
|
+
// let imageSource = await ImagePicker.launchImageLibraryAsync({
|
|
396
|
+
// mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
397
|
+
// allowsEditing: true,
|
|
398
|
+
// aspect: [4, 3],
|
|
399
|
+
// quality: 0.8,
|
|
400
|
+
// base64: true,
|
|
401
|
+
// exif: false,
|
|
402
|
+
// });
|
|
403
|
+
|
|
404
|
+
// if (!imageSource?.canceled) {
|
|
405
|
+
// // Get the asset
|
|
406
|
+
// const selectedAsset = imageSource?.assets?.[0];
|
|
407
|
+
// if (!selectedAsset) {
|
|
408
|
+
// setImageLoading(false);
|
|
409
|
+
// return;
|
|
410
|
+
// }
|
|
411
|
+
|
|
412
|
+
// // Create a base64 image string for preview
|
|
413
|
+
// const base64Data = selectedAsset.base64;
|
|
414
|
+
// const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
|
|
415
|
+
|
|
416
|
+
// // Format the asset for upload service requirements
|
|
417
|
+
// const asset: ExtendedImagePickerAsset = {
|
|
418
|
+
// ...selectedAsset,
|
|
419
|
+
// url: selectedAsset.uri,
|
|
420
|
+
// fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
|
|
421
|
+
// mimeType: 'image/jpeg',
|
|
422
|
+
// };
|
|
423
|
+
|
|
424
|
+
// // Update state with the new image
|
|
425
|
+
// setSelectedImage(previewImage);
|
|
426
|
+
// setImages([asset]);
|
|
427
|
+
// setImageLoading(false);
|
|
428
|
+
// } else {
|
|
429
|
+
// setImageLoading(false);
|
|
430
|
+
// }
|
|
431
|
+
// } catch (error) {
|
|
432
|
+
// console.error('Error selecting image:', error);
|
|
433
|
+
// setImageLoading(false);
|
|
434
|
+
// }
|
|
435
|
+
// };
|
|
436
|
+
|
|
437
|
+
// Image selection handler
|
|
269
438
|
const onSelectImages = async () => {
|
|
270
|
-
|
|
439
|
+
setLoading(true);
|
|
271
440
|
|
|
272
441
|
try {
|
|
273
442
|
let imageSource = await ImagePicker.launchImageLibraryAsync({
|
|
274
443
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
275
|
-
allowsEditing:
|
|
444
|
+
allowsEditing: false,
|
|
276
445
|
aspect: [4, 3],
|
|
277
446
|
quality: 0.8,
|
|
278
447
|
base64: true,
|
|
279
448
|
exif: false,
|
|
449
|
+
allowsMultipleSelection: true, // Enable multiple selection
|
|
280
450
|
});
|
|
281
451
|
|
|
282
452
|
if (!imageSource?.canceled) {
|
|
283
|
-
// Get
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
286
|
-
|
|
453
|
+
// Get all selected assets
|
|
454
|
+
const selectedAssets = imageSource?.assets || [];
|
|
455
|
+
if (selectedAssets.length === 0) {
|
|
456
|
+
setLoading(false);
|
|
287
457
|
return;
|
|
288
458
|
}
|
|
289
459
|
|
|
290
|
-
//
|
|
291
|
-
const
|
|
292
|
-
|
|
460
|
+
// Process all selected images
|
|
461
|
+
const newImages = selectedAssets.map((selectedAsset) => {
|
|
462
|
+
// Create a base64 image string for preview
|
|
463
|
+
const base64Data = selectedAsset.base64;
|
|
464
|
+
const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
|
|
465
|
+
|
|
466
|
+
// Format the asset for upload service requirements
|
|
467
|
+
const asset: ExtendedImagePickerAsset = {
|
|
468
|
+
...selectedAsset,
|
|
469
|
+
url: selectedAsset.uri,
|
|
470
|
+
fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
|
|
471
|
+
mimeType: 'image/jpeg',
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return asset;
|
|
475
|
+
});
|
|
293
476
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
};
|
|
477
|
+
// Set preview for the first image (for backward compatibility)
|
|
478
|
+
if (newImages.length > 0) {
|
|
479
|
+
const base64Data = newImages[0].base64;
|
|
480
|
+
const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : newImages[0].uri;
|
|
481
|
+
setSelectedImage(previewImage);
|
|
482
|
+
}
|
|
301
483
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
setImageLoading(false);
|
|
484
|
+
// Add new images to existing ones
|
|
485
|
+
setImages((currentImages) => [...currentImages, ...newImages]);
|
|
486
|
+
setLoading(false); // Set loading to false after successful selection
|
|
306
487
|
} else {
|
|
307
|
-
|
|
488
|
+
setLoading(false);
|
|
308
489
|
}
|
|
309
490
|
} catch (error) {
|
|
310
|
-
|
|
311
|
-
setImageLoading(false);
|
|
491
|
+
setLoading(false);
|
|
312
492
|
}
|
|
313
493
|
};
|
|
314
494
|
|
|
495
|
+
// Action sheet handlers
|
|
496
|
+
const showActionSheet = useCallback(() => {
|
|
497
|
+
setLastShownMsg(msg); // Store current message text when opening sheet
|
|
498
|
+
setActionSheetVisible(true);
|
|
499
|
+
}, [msg]);
|
|
500
|
+
|
|
501
|
+
const handleActionSheetClose = useCallback(() => {
|
|
502
|
+
// Close the sheet
|
|
503
|
+
setActionSheetVisible(false);
|
|
504
|
+
|
|
505
|
+
// If user didn't make any changes, restore the original message
|
|
506
|
+
if (!textUpdatedInActionSheet) {
|
|
507
|
+
setMsg(lastShownMsg);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Reset the flag
|
|
511
|
+
setTextUpdatedInActionSheet(false);
|
|
512
|
+
}, [textUpdatedInActionSheet, lastShownMsg]);
|
|
513
|
+
|
|
514
|
+
const handleActionSheetSend = () => {
|
|
515
|
+
// Send the message using the existing handleSend function
|
|
516
|
+
handleSend(msg);
|
|
517
|
+
|
|
518
|
+
// Close the action sheet
|
|
519
|
+
setActionSheetVisible(false);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// Add focus/blur handlers for the input field
|
|
523
|
+
useEffect(() => {
|
|
524
|
+
// When showing the action sheet, we should hide the keyboard for the main input
|
|
525
|
+
if (isActionSheetVisible && Platform.OS === 'ios') {
|
|
526
|
+
Keyboard.dismiss();
|
|
527
|
+
}
|
|
528
|
+
}, [isActionSheetVisible]);
|
|
529
|
+
|
|
530
|
+
// Keep track of whether the input is in focus to help sync UI states
|
|
531
|
+
const [isGiftedInputFocused, setIsGiftedInputFocused] = useState(false);
|
|
532
|
+
|
|
315
533
|
const handleSend = useCallback(
|
|
316
534
|
async (message: string) => {
|
|
535
|
+
const newMessageText = message && message.length > 0 ? message || ' ' : ' ';
|
|
536
|
+
// Check if we can send a message (channel and thread exists)
|
|
537
|
+
|
|
317
538
|
if (!channelId) return;
|
|
318
|
-
|
|
539
|
+
|
|
540
|
+
// Allow sending if we have text OR images (image-only messages are valid)
|
|
541
|
+
const hasText = !!newMessageText && newMessageText !== ' ';
|
|
542
|
+
const hasImages = images && images.length > 0;
|
|
543
|
+
|
|
544
|
+
if (!hasText && !hasImages) return;
|
|
545
|
+
|
|
546
|
+
// Start loading state
|
|
547
|
+
setLoading(true);
|
|
319
548
|
|
|
320
549
|
const postId = objectId();
|
|
321
550
|
const currentDate = new Date();
|
|
322
551
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Create optimistic message with common properties
|
|
552
|
+
// Store current values before clearing
|
|
553
|
+
const currentMessageText = message || '';
|
|
554
|
+
const currentImages = [...images];
|
|
555
|
+
|
|
556
|
+
// Clear UI immediately for next message
|
|
557
|
+
setMsg('');
|
|
558
|
+
setSelectedImage('');
|
|
559
|
+
setImages([]);
|
|
560
|
+
setFiles([]);
|
|
561
|
+
|
|
562
|
+
// Close the action sheet if it's open
|
|
563
|
+
if (isActionSheetVisible) {
|
|
564
|
+
setActionSheetVisible(false);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Create optimistic message with all images for immediate display
|
|
341
568
|
const optimisticMessage = {
|
|
342
569
|
id: postId,
|
|
343
|
-
message:
|
|
570
|
+
message: currentMessageText || (currentImages.length > 0 ? ' ' : ''),
|
|
344
571
|
createdAt: currentDate.toISOString(),
|
|
345
572
|
author: {
|
|
346
573
|
id: auth?.profile?.id,
|
|
@@ -348,60 +575,127 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
348
575
|
familyName: auth?.profile?.family_name,
|
|
349
576
|
picture: auth?.profile?.picture,
|
|
350
577
|
},
|
|
351
|
-
isDelivered: false,
|
|
578
|
+
isDelivered: false, // Mark as not delivered yet during upload
|
|
352
579
|
isRead: false,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
580
|
+
isOptimistic: true, // Flag to identify optimistic messages
|
|
581
|
+
files:
|
|
582
|
+
currentImages.length > 0
|
|
583
|
+
? {
|
|
584
|
+
data: currentImages
|
|
585
|
+
.filter((img) => img && (img.uri || img.url)) // Filter out invalid images
|
|
586
|
+
.map((img) => ({
|
|
587
|
+
id: objectId(),
|
|
588
|
+
url: img.uri || img.url,
|
|
589
|
+
localUri: img.uri || img.url, // Keep local URI for preview
|
|
590
|
+
name: img.fileName || `image_${Date.now()}.jpg`,
|
|
591
|
+
extension: 'jpg',
|
|
592
|
+
mimeType: 'image/jpeg',
|
|
593
|
+
refType: FileRefType.Post,
|
|
594
|
+
height: img.height || 0,
|
|
595
|
+
width: img.width || 0,
|
|
596
|
+
isUploading: true, // Flag to indicate this file is uploading
|
|
597
|
+
})),
|
|
598
|
+
totalCount: currentImages.filter((img) => img && (img.uri || img.url)).length, // Only count valid images
|
|
599
|
+
}
|
|
600
|
+
: undefined,
|
|
359
601
|
};
|
|
360
602
|
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
603
|
+
// Only add optimistic message if there's content to send
|
|
604
|
+
if (
|
|
605
|
+
(optimisticMessage.message && optimisticMessage.message.trim() !== '') ||
|
|
606
|
+
(optimisticMessage.files && optimisticMessage.files.data && optimisticMessage.files.data.length > 0)
|
|
607
|
+
) {
|
|
608
|
+
setChannelMessages((oldMessages: any) => uniqBy([optimisticMessage, ...oldMessages], ({ id }) => id));
|
|
609
|
+
setIsScrollToBottom(true);
|
|
610
|
+
}
|
|
364
611
|
|
|
365
612
|
try {
|
|
366
613
|
let fileIds: string[] | null = null;
|
|
367
614
|
|
|
368
615
|
// Handle file uploads if images exist
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
616
|
+
if (currentImages && currentImages.length > 0) {
|
|
617
|
+
// Filter out any invalid images before upload
|
|
618
|
+
const validImages = currentImages.filter((img) => img && (img.uri || img.url));
|
|
619
|
+
|
|
620
|
+
if (validImages.length === 0) {
|
|
621
|
+
// No valid images to upload, just send the text message
|
|
622
|
+
setIsUploadingImage(false);
|
|
623
|
+
} else {
|
|
624
|
+
setIsUploadingImage(true);
|
|
625
|
+
|
|
626
|
+
// Format images for upload
|
|
627
|
+
const imagesToUpload = validImages.map((img) => {
|
|
628
|
+
return {
|
|
629
|
+
...img,
|
|
630
|
+
uri: img.uri || img.url, // Use either uri or url
|
|
631
|
+
type: img.mimeType || 'image/jpeg',
|
|
632
|
+
name: img.fileName || `image_${Date.now()}.jpg`,
|
|
633
|
+
};
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Track upload progress with messageId
|
|
637
|
+
setUploadingMessageId(postId);
|
|
638
|
+
setPendingUploads((prev) => ({
|
|
639
|
+
...prev,
|
|
640
|
+
[postId]: {
|
|
641
|
+
images: validImages,
|
|
642
|
+
message: currentMessageText,
|
|
386
643
|
},
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
644
|
+
}));
|
|
645
|
+
|
|
646
|
+
const uploadResponse = await startUpload({
|
|
647
|
+
file: imagesToUpload as unknown as ImagePicker.ImagePickerAsset[],
|
|
648
|
+
saveUploadedFile: {
|
|
649
|
+
variables: {
|
|
650
|
+
postId,
|
|
651
|
+
},
|
|
391
652
|
},
|
|
392
|
-
|
|
393
|
-
|
|
653
|
+
createUploadLink: {
|
|
654
|
+
variables: {
|
|
655
|
+
postId,
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
});
|
|
394
659
|
|
|
395
|
-
|
|
660
|
+
if (uploadResponse?.error) {
|
|
661
|
+
throw new Error(uploadResponse.error.toString());
|
|
662
|
+
}
|
|
396
663
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
664
|
+
const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
|
|
665
|
+
if (uploadResponse.data) {
|
|
666
|
+
fileIds = uploadedFiles?.map((f: any) => f.id) ?? null;
|
|
667
|
+
|
|
668
|
+
// Update message with uploaded file information
|
|
669
|
+
setChannelMessages((oldMessages: any) => {
|
|
670
|
+
return oldMessages.map((msg) => {
|
|
671
|
+
if (msg.id === postId && msg.isOptimistic) {
|
|
672
|
+
// Update the uploaded files with real URLs from server
|
|
673
|
+
return {
|
|
674
|
+
...msg,
|
|
675
|
+
isOptimistic: false,
|
|
676
|
+
files: {
|
|
677
|
+
...msg.files,
|
|
678
|
+
data: uploadedFiles
|
|
679
|
+
.filter((file) => isValidFileUrl(file.url))
|
|
680
|
+
.map((file, index) => ({
|
|
681
|
+
...msg.files.data[index],
|
|
682
|
+
url: file.url,
|
|
683
|
+
isUploading: false,
|
|
684
|
+
})),
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return msg;
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
}
|
|
403
692
|
|
|
404
|
-
|
|
693
|
+
// Clear pending upload now that it's complete
|
|
694
|
+
setPendingUploads((prev) => {
|
|
695
|
+
const newPending = { ...prev };
|
|
696
|
+
delete newPending[postId];
|
|
697
|
+
return newPending;
|
|
698
|
+
});
|
|
405
699
|
}
|
|
406
700
|
}
|
|
407
701
|
|
|
@@ -413,11 +707,34 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
413
707
|
postThreadId: postThread && postThread?.id,
|
|
414
708
|
postParentId: !parentId || parentId === 0 ? null : parentId,
|
|
415
709
|
threadMessageInput: {
|
|
416
|
-
content:
|
|
710
|
+
content: currentMessageText,
|
|
417
711
|
files: fileIds,
|
|
418
712
|
role,
|
|
419
713
|
},
|
|
420
714
|
},
|
|
715
|
+
optimisticResponse: {
|
|
716
|
+
createPostThread: {
|
|
717
|
+
__typename: 'ThreadMessageSent',
|
|
718
|
+
lastMessage: {
|
|
719
|
+
__typename: 'Post',
|
|
720
|
+
id: postId,
|
|
721
|
+
message: currentMessageText || ' ',
|
|
722
|
+
createdAt: new Date().toISOString(),
|
|
723
|
+
author: {
|
|
724
|
+
__typename: 'UserAccount',
|
|
725
|
+
id: auth?.profile?.id,
|
|
726
|
+
givenName: auth?.profile?.given_name,
|
|
727
|
+
familyName: auth?.profile?.family_name,
|
|
728
|
+
picture: auth?.profile?.picture,
|
|
729
|
+
},
|
|
730
|
+
channel: { __typename: 'Channel', id: channelId.toString() },
|
|
731
|
+
} as any, // Type assertion to avoid TypeScript errors
|
|
732
|
+
data: {
|
|
733
|
+
__typename: 'PostThread',
|
|
734
|
+
id: postThread?.id || objectId(),
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
},
|
|
421
738
|
update: (cache, { data, errors }: any) => {
|
|
422
739
|
if (!data || errors) return;
|
|
423
740
|
|
|
@@ -432,21 +749,57 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
432
749
|
sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
|
|
433
750
|
},
|
|
434
751
|
});
|
|
435
|
-
|
|
436
|
-
// Update the optimistic message to show it's delivered
|
|
437
|
-
setChannelMessages((oldMessages: any) =>
|
|
438
|
-
oldMessages.map((msg: any) => (msg.id === postId ? { ...msg, isDelivered: true } : msg)),
|
|
439
|
-
);
|
|
440
752
|
} catch (error) {
|
|
441
753
|
console.error('Error sending message:', error);
|
|
754
|
+
|
|
755
|
+
// Format error message based on environment
|
|
756
|
+
let formattedErrorMessage = 'Failed to upload image. Please try again.';
|
|
757
|
+
|
|
758
|
+
if (__DEV__) {
|
|
759
|
+
// Show detailed error in development
|
|
760
|
+
if (error.name === 'ApolloError') {
|
|
761
|
+
formattedErrorMessage = error.message
|
|
762
|
+
.replace('[ApolloError: ', '')
|
|
763
|
+
.replace(']', '')
|
|
764
|
+
.split(';')[0]; // Take first part of error for clarity
|
|
765
|
+
} else {
|
|
766
|
+
formattedErrorMessage = error.message || 'Unknown error occurred';
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
// Show friendly message in production
|
|
770
|
+
if (error.message?.includes('network') || error.message?.includes('timeout')) {
|
|
771
|
+
formattedErrorMessage = 'Network error. Please check your connection.';
|
|
772
|
+
} else if (error.message?.includes('permission')) {
|
|
773
|
+
formattedErrorMessage = 'Permission denied for this operation.';
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Show custom error notification
|
|
778
|
+
setErrorMessage(formattedErrorMessage);
|
|
779
|
+
|
|
442
780
|
// Remove the optimistic message if there was an error
|
|
443
781
|
setChannelMessages((oldMessages: any) => oldMessages.filter((msg) => msg.id !== postId));
|
|
782
|
+
|
|
783
|
+
// Clean up any pending upload states
|
|
784
|
+
removeMessageFromUI(postId);
|
|
444
785
|
} finally {
|
|
445
786
|
setLoading(false);
|
|
787
|
+
setIsUploadingImage(false);
|
|
446
788
|
setUploadingMessageId(null);
|
|
447
789
|
}
|
|
448
790
|
},
|
|
449
|
-
[
|
|
791
|
+
[
|
|
792
|
+
auth,
|
|
793
|
+
channelId,
|
|
794
|
+
channelToTop,
|
|
795
|
+
images,
|
|
796
|
+
parentId,
|
|
797
|
+
postThread,
|
|
798
|
+
selectedImage,
|
|
799
|
+
setChannelMessages,
|
|
800
|
+
removeMessageFromUI,
|
|
801
|
+
role,
|
|
802
|
+
],
|
|
450
803
|
);
|
|
451
804
|
|
|
452
805
|
const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
|
|
@@ -467,6 +820,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
467
820
|
}
|
|
468
821
|
};
|
|
469
822
|
|
|
823
|
+
// Improved messageList function to better handle images
|
|
470
824
|
const messageList = useMemo(() => {
|
|
471
825
|
let currentDate = '';
|
|
472
826
|
let res: any = [];
|
|
@@ -489,17 +843,56 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
489
843
|
message._id = msg.id;
|
|
490
844
|
message.text = msg.message;
|
|
491
845
|
message.createdAt = date;
|
|
492
|
-
|
|
846
|
+
message.user = {
|
|
493
847
|
_id: msg?.author?.id ?? auth?.profile?.id,
|
|
494
848
|
name:
|
|
495
849
|
msg?.author?.givenName ??
|
|
496
850
|
auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
|
|
497
851
|
auth?.profile?.family_name,
|
|
498
852
|
avatar: msg?.author?.picture ?? auth?.profile?.picture,
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// Handle image uploads with proper UI feedback
|
|
856
|
+
message.isOptimistic = msg?.isOptimistic || false;
|
|
857
|
+
message.isUploading = msg.files?.data?.some((file) => file.isUploading) || false;
|
|
858
|
+
|
|
859
|
+
// Improved image handling - ensure we have valid image URLs
|
|
860
|
+
if (msg.files?.data?.length > 0) {
|
|
861
|
+
// Filter out any invalid files first
|
|
862
|
+
const validFiles = msg.files.data.filter(
|
|
863
|
+
(file) =>
|
|
864
|
+
file && (file.url || file.localUri) && typeof (file.url || file.localUri) === 'string',
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
if (validFiles.length > 0) {
|
|
868
|
+
// For the first image (used in single image display)
|
|
869
|
+
const firstFile = validFiles[0];
|
|
870
|
+
const imageUrl = firstFile.isUploading
|
|
871
|
+
? firstFile.localUri || firstFile.url
|
|
872
|
+
: firstFile.url || firstFile.localUri;
|
|
873
|
+
|
|
874
|
+
if (imageUrl) {
|
|
875
|
+
message.image = imageUrl;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// For multiple images
|
|
879
|
+
if (validFiles.length > 1) {
|
|
880
|
+
message.images = validFiles
|
|
881
|
+
.map((file) =>
|
|
882
|
+
file.isUploading ? file.localUri || file.url : file.url || file.localUri,
|
|
883
|
+
)
|
|
884
|
+
.filter((url) => !!url); // Filter out any undefined or empty URLs
|
|
885
|
+
|
|
886
|
+
// If no valid URLs after filtering, don't set the images property
|
|
887
|
+
if (message.images.length === 0) {
|
|
888
|
+
delete message.images;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
message.sent = msg?.isDelivered;
|
|
895
|
+
message.received = msg?.isRead;
|
|
503
896
|
message.type = msg?.type;
|
|
504
897
|
message.propsConfiguration = msg?.propsConfiguration;
|
|
505
898
|
res.push(message);
|
|
@@ -509,22 +902,55 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
509
902
|
//return res;
|
|
510
903
|
}, [channelMessages]);
|
|
511
904
|
|
|
512
|
-
const renderSend = useCallback(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
style={{ marginBottom: 5, marginRight: 5 }}
|
|
519
|
-
size={32}
|
|
520
|
-
color="#2e64e5"
|
|
521
|
-
/>
|
|
522
|
-
</Box>
|
|
523
|
-
</Send>
|
|
524
|
-
);
|
|
525
|
-
}, []);
|
|
905
|
+
const renderSend = useCallback(
|
|
906
|
+
(props) => {
|
|
907
|
+
// If action sheet is visible, don't show the default send button
|
|
908
|
+
if (isActionSheetVisible) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
526
911
|
|
|
527
|
-
|
|
912
|
+
// Enable the send button if there's text OR we have images
|
|
913
|
+
const hasContent = !!props.text || images?.length > 0;
|
|
914
|
+
const canSend = channelId && hasContent;
|
|
915
|
+
const isDisabled = !canSend || isUploadingImage || loading;
|
|
916
|
+
|
|
917
|
+
return (
|
|
918
|
+
<Send
|
|
919
|
+
{...props}
|
|
920
|
+
disabled={isDisabled}
|
|
921
|
+
containerStyle={{
|
|
922
|
+
justifyContent: 'center',
|
|
923
|
+
alignItems: 'center',
|
|
924
|
+
height: 40,
|
|
925
|
+
width: 44,
|
|
926
|
+
marginRight: 4,
|
|
927
|
+
marginBottom: 0,
|
|
928
|
+
marginLeft: 4,
|
|
929
|
+
}}
|
|
930
|
+
// onSend={(messages, shouldReset) => {
|
|
931
|
+
// // Always use our custom handler to ensure images are handled properly
|
|
932
|
+
// console.log('messages', messages);
|
|
933
|
+
// // handleSend(props.text || ' ');
|
|
934
|
+
// }}
|
|
935
|
+
>
|
|
936
|
+
<View style={{ padding: 4 }}>
|
|
937
|
+
{isUploadingImage || loading ? (
|
|
938
|
+
<Spinner size="small" color={colors.blue[500]} />
|
|
939
|
+
) : (
|
|
940
|
+
<MaterialCommunityIcons
|
|
941
|
+
name="send-circle"
|
|
942
|
+
size={32}
|
|
943
|
+
color={isDisabled ? colors.gray[400] : colors.blue[500]}
|
|
944
|
+
/>
|
|
945
|
+
)}
|
|
946
|
+
</View>
|
|
947
|
+
</Send>
|
|
948
|
+
);
|
|
949
|
+
},
|
|
950
|
+
[channelId, images, isUploadingImage, loading, isActionSheetVisible],
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
const renderMessageText1 = useCallback(
|
|
528
954
|
(props: any) => {
|
|
529
955
|
const { currentMessage } = props;
|
|
530
956
|
if (currentMessage.type === 'ALERT') {
|
|
@@ -589,6 +1015,195 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
589
1015
|
[navigation, role],
|
|
590
1016
|
);
|
|
591
1017
|
|
|
1018
|
+
const renderMessageText = useCallback(
|
|
1019
|
+
(props: any) => {
|
|
1020
|
+
const { currentMessage } = props;
|
|
1021
|
+
const lastReply: any =
|
|
1022
|
+
currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
|
|
1023
|
+
|
|
1024
|
+
// Do not render anything if the message text is empty or only whitespace
|
|
1025
|
+
if (!currentMessage?.text || currentMessage.text.trim() === '') {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (currentMessage.type === 'ALERT') {
|
|
1030
|
+
const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
|
|
1031
|
+
let action: string = '';
|
|
1032
|
+
let actionId: any = '';
|
|
1033
|
+
let params: any = {};
|
|
1034
|
+
|
|
1035
|
+
if (attachment?.callToAction?.extraParams) {
|
|
1036
|
+
const extraParams: any = attachment?.callToAction?.extraParams;
|
|
1037
|
+
const route: any = extraParams?.route ?? null;
|
|
1038
|
+
let path: any = null;
|
|
1039
|
+
let param: any = null;
|
|
1040
|
+
if (role && role == PreDefinedRole.Guest) {
|
|
1041
|
+
path = route?.guest?.name ? route?.guest?.name ?? null : null;
|
|
1042
|
+
param = route?.guest?.params ? route?.guest?.params ?? null : null;
|
|
1043
|
+
} else if (role && role == PreDefinedRole.Owner) {
|
|
1044
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
1045
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
1046
|
+
} else {
|
|
1047
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
1048
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
action = path;
|
|
1052
|
+
params = { ...param };
|
|
1053
|
+
} else if (attachment?.callToAction?.link) {
|
|
1054
|
+
action = CALL_TO_ACTION_PATH;
|
|
1055
|
+
actionId = attachment?.callToAction?.link.split('/').pop();
|
|
1056
|
+
params = { reservationId: actionId };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return (
|
|
1060
|
+
<>
|
|
1061
|
+
{attachment?.callToAction && action ? (
|
|
1062
|
+
<Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
|
|
1063
|
+
<Button
|
|
1064
|
+
variant={'outline'}
|
|
1065
|
+
size={'sm'}
|
|
1066
|
+
className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
|
|
1067
|
+
onPress={() => action && params && navigation.navigate(action, params)}
|
|
1068
|
+
>
|
|
1069
|
+
<ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
|
|
1070
|
+
{attachment.callToAction.title}
|
|
1071
|
+
</ButtonText>
|
|
1072
|
+
</Button>
|
|
1073
|
+
<MessageText
|
|
1074
|
+
{...props}
|
|
1075
|
+
textStyle={{
|
|
1076
|
+
left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
|
|
1077
|
+
}}
|
|
1078
|
+
/>
|
|
1079
|
+
</Box>
|
|
1080
|
+
) : (
|
|
1081
|
+
<TouchableHighlight
|
|
1082
|
+
underlayColor={'#c0c0c0'}
|
|
1083
|
+
style={{ width: '100%' }}
|
|
1084
|
+
onPress={() => {
|
|
1085
|
+
if (currentMessage?.isShowThreadMessage)
|
|
1086
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
1087
|
+
channelId: channelId,
|
|
1088
|
+
title: 'Message',
|
|
1089
|
+
postParentId: currentMessage?._id,
|
|
1090
|
+
isPostParentIdThread: true,
|
|
1091
|
+
});
|
|
1092
|
+
}}
|
|
1093
|
+
>
|
|
1094
|
+
<>
|
|
1095
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
1096
|
+
{currentMessage?.replies?.data?.length > 0 && (
|
|
1097
|
+
<HStack space={'sm'} className="px-1 items-center">
|
|
1098
|
+
<HStack>
|
|
1099
|
+
{currentMessage?.replies?.data
|
|
1100
|
+
?.filter(
|
|
1101
|
+
(v: any, i: any, a: any) =>
|
|
1102
|
+
a.findIndex((t: any) => t?.author?.id === v?.author?.id) ===
|
|
1103
|
+
i,
|
|
1104
|
+
)
|
|
1105
|
+
?.slice(0, 2)
|
|
1106
|
+
?.reverse()
|
|
1107
|
+
?.map((p: any, i: Number) => (
|
|
1108
|
+
<Avatar
|
|
1109
|
+
key={'conversations-view-key-' + i}
|
|
1110
|
+
size={'sm'}
|
|
1111
|
+
className="bg-transparent"
|
|
1112
|
+
>
|
|
1113
|
+
<AvatarFallbackText>
|
|
1114
|
+
{startCase(p?.author?.username?.charAt(0))}
|
|
1115
|
+
</AvatarFallbackText>
|
|
1116
|
+
<AvatarImage
|
|
1117
|
+
alt="user image"
|
|
1118
|
+
style={{
|
|
1119
|
+
borderRadius: 6,
|
|
1120
|
+
borderWidth: 2,
|
|
1121
|
+
borderColor: '#fff',
|
|
1122
|
+
}}
|
|
1123
|
+
source={{
|
|
1124
|
+
uri: p?.author?.picture,
|
|
1125
|
+
}}
|
|
1126
|
+
/>
|
|
1127
|
+
</Avatar>
|
|
1128
|
+
))}
|
|
1129
|
+
</HStack>
|
|
1130
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
1131
|
+
{currentMessage?.replies?.totalCount}{' '}
|
|
1132
|
+
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
1133
|
+
</Text>
|
|
1134
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
1135
|
+
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
1136
|
+
</Text>
|
|
1137
|
+
</HStack>
|
|
1138
|
+
)}
|
|
1139
|
+
</>
|
|
1140
|
+
</TouchableHighlight>
|
|
1141
|
+
)}
|
|
1142
|
+
</>
|
|
1143
|
+
);
|
|
1144
|
+
} else {
|
|
1145
|
+
return (
|
|
1146
|
+
<TouchableHighlight
|
|
1147
|
+
underlayColor={'#c0c0c0'}
|
|
1148
|
+
style={{ width: '100%' }}
|
|
1149
|
+
onPress={() => {
|
|
1150
|
+
if (currentMessage?.isShowThreadMessage)
|
|
1151
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
1152
|
+
channelId: channelId,
|
|
1153
|
+
title: 'Message',
|
|
1154
|
+
postParentId: currentMessage?._id,
|
|
1155
|
+
isPostParentIdThread: true,
|
|
1156
|
+
});
|
|
1157
|
+
}}
|
|
1158
|
+
>
|
|
1159
|
+
<>
|
|
1160
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
1161
|
+
{currentMessage?.replies?.data?.length > 0 && (
|
|
1162
|
+
<HStack space={'sm'} className="px-1 items-center">
|
|
1163
|
+
<HStack>
|
|
1164
|
+
{currentMessage?.replies?.data
|
|
1165
|
+
?.filter(
|
|
1166
|
+
(v: any, i: any, a: any) =>
|
|
1167
|
+
a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
|
|
1168
|
+
)
|
|
1169
|
+
?.slice(0, 2)
|
|
1170
|
+
?.reverse()
|
|
1171
|
+
?.map((p: any, i: Number) => (
|
|
1172
|
+
<Avatar
|
|
1173
|
+
key={'conversation-replies-key-' + i}
|
|
1174
|
+
className="bg-transparent"
|
|
1175
|
+
size={'sm'}
|
|
1176
|
+
>
|
|
1177
|
+
<AvatarFallbackText>
|
|
1178
|
+
{startCase(p?.author?.username?.charAt(0))}
|
|
1179
|
+
</AvatarFallbackText>
|
|
1180
|
+
<AvatarImage
|
|
1181
|
+
alt="user image"
|
|
1182
|
+
style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
|
|
1183
|
+
source={{
|
|
1184
|
+
uri: p?.author?.picture,
|
|
1185
|
+
}}
|
|
1186
|
+
/>
|
|
1187
|
+
</Avatar>
|
|
1188
|
+
))}
|
|
1189
|
+
</HStack>
|
|
1190
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
1191
|
+
{currentMessage?.replies?.totalCount}{' '}
|
|
1192
|
+
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
1193
|
+
</Text>
|
|
1194
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
1195
|
+
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
1196
|
+
</Text>
|
|
1197
|
+
</HStack>
|
|
1198
|
+
)}
|
|
1199
|
+
</>
|
|
1200
|
+
</TouchableHighlight>
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
[navigation, channelId, role],
|
|
1205
|
+
);
|
|
1206
|
+
|
|
592
1207
|
// const renderActions = useCallback((props) => {
|
|
593
1208
|
// return (
|
|
594
1209
|
// <Actions
|
|
@@ -599,189 +1214,167 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
599
1214
|
// }, [onSelectImages]);
|
|
600
1215
|
|
|
601
1216
|
// Render action buttons (including image upload)
|
|
602
|
-
const renderActions = (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// <HStack space="sm" className="items-center">
|
|
634
|
-
// {imageLoading ? (
|
|
635
|
-
// <Box className="w-12 h-12 rounded-md bg-gray-100 overflow-hidden flex items-center justify-center">
|
|
636
|
-
// <Spinner size="small" color={colors.blue[500]} />
|
|
637
|
-
// </Box>
|
|
638
|
-
// ) : (
|
|
639
|
-
// <Box className="relative">
|
|
640
|
-
// <Image
|
|
641
|
-
// className="rounded-md"
|
|
642
|
-
// key={selectedImage}
|
|
643
|
-
// alt={'Selected image'}
|
|
644
|
-
// source={{ uri: selectedImage }}
|
|
645
|
-
// style={{
|
|
646
|
-
// width: 48,
|
|
647
|
-
// height: 48,
|
|
648
|
-
// borderRadius: 4,
|
|
649
|
-
// }}
|
|
650
|
-
// />
|
|
651
|
-
// <Box className="absolute -top-1 -right-1 bg-white rounded-full shadow-sm">
|
|
652
|
-
// <Button
|
|
653
|
-
// variant="link"
|
|
654
|
-
// size="xs"
|
|
655
|
-
// onPress={() => {
|
|
656
|
-
// setFiles([]);
|
|
657
|
-
// setImage('');
|
|
658
|
-
// setImages([]);
|
|
659
|
-
// }}
|
|
660
|
-
// >
|
|
661
|
-
// <MaterialIcons name="cancel" size={16} color={colors.red[500]} />
|
|
662
|
-
// </Button>
|
|
663
|
-
// </Box>
|
|
664
|
-
// </Box>
|
|
665
|
-
// )}
|
|
666
|
-
// <Text className="text-sm text-gray-600 ml-2">
|
|
667
|
-
// {imageLoading ? 'Processing image...' : 'Ready to send'}
|
|
668
|
-
// </Text>
|
|
669
|
-
// </HStack>
|
|
670
|
-
// </HStack>
|
|
671
|
-
// </Box>
|
|
672
|
-
// );
|
|
673
|
-
// }, [selectedImage, imageLoading]);
|
|
1217
|
+
const renderActions = useCallback(
|
|
1218
|
+
(props) => {
|
|
1219
|
+
return (
|
|
1220
|
+
<Actions
|
|
1221
|
+
{...props}
|
|
1222
|
+
onPressActionButton={onSelectImages} // Direct action when pressing the button
|
|
1223
|
+
options={{
|
|
1224
|
+
['Choose from Library']: () => {
|
|
1225
|
+
// Call the same image selection function used in the ActionSheet
|
|
1226
|
+
onSelectImages();
|
|
1227
|
+
},
|
|
1228
|
+
['Cancel']: () => {}, // Add this option to make the sheet dismissible
|
|
1229
|
+
}}
|
|
1230
|
+
optionTintColor="#000000"
|
|
1231
|
+
cancelButtonIndex={1} // Set the Cancel option as the cancel button
|
|
1232
|
+
icon={() => (
|
|
1233
|
+
<Box className="w-8 h-8 items-center justify-center">
|
|
1234
|
+
<Ionicons name="image" size={24} color={colors.blue[500]} />
|
|
1235
|
+
</Box>
|
|
1236
|
+
)}
|
|
1237
|
+
containerStyle={{
|
|
1238
|
+
alignItems: 'center',
|
|
1239
|
+
justifyContent: 'center',
|
|
1240
|
+
marginLeft: 8,
|
|
1241
|
+
marginBottom: 0,
|
|
1242
|
+
}}
|
|
1243
|
+
/>
|
|
1244
|
+
);
|
|
1245
|
+
},
|
|
1246
|
+
[onSelectImages],
|
|
1247
|
+
);
|
|
674
1248
|
|
|
1249
|
+
// Create a more visible and reliable image preview with cancel button
|
|
675
1250
|
const renderAccessory = useCallback(() => {
|
|
676
|
-
if (!
|
|
677
|
-
return null;
|
|
678
|
-
}
|
|
679
|
-
|
|
1251
|
+
if (!images.length) return null;
|
|
680
1252
|
return (
|
|
681
|
-
<
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
borderTopWidth: 1,
|
|
686
|
-
borderTopColor: '#e0e0e0',
|
|
687
|
-
flexDirection: 'row',
|
|
688
|
-
alignItems: 'center',
|
|
689
|
-
margin: 0,
|
|
690
|
-
padding: 0,
|
|
691
|
-
paddingVertical: 0,
|
|
692
|
-
position: 'absolute',
|
|
693
|
-
bottom: Platform.OS === 'ios' ? 105 : 95, // Position well above the input area
|
|
694
|
-
left: 0,
|
|
695
|
-
right: 0,
|
|
696
|
-
zIndex: 1,
|
|
697
|
-
elevation: 3,
|
|
698
|
-
shadowColor: '#000',
|
|
699
|
-
shadowOffset: { width: 0, height: -1 },
|
|
700
|
-
shadowOpacity: 0.05,
|
|
701
|
-
shadowRadius: 2,
|
|
702
|
-
}}
|
|
703
|
-
>
|
|
704
|
-
<View
|
|
1253
|
+
<Box style={{ position: 'relative', height: 70, backgroundColor: 'transparent', justifyContent: 'center' }}>
|
|
1254
|
+
<ScrollView
|
|
1255
|
+
horizontal
|
|
1256
|
+
showsHorizontalScrollIndicator={false}
|
|
705
1257
|
style={{
|
|
706
|
-
flex: 1,
|
|
707
1258
|
flexDirection: 'row',
|
|
708
|
-
alignItems: 'center',
|
|
709
1259
|
paddingLeft: 15,
|
|
710
1260
|
paddingRight: 5,
|
|
711
1261
|
}}
|
|
1262
|
+
contentContainerStyle={{
|
|
1263
|
+
alignItems: 'center',
|
|
1264
|
+
height: '100%',
|
|
1265
|
+
}}
|
|
712
1266
|
>
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
height: 56,
|
|
717
|
-
marginRight: 15,
|
|
718
|
-
borderRadius: 4,
|
|
719
|
-
backgroundColor: colors.gray[200],
|
|
720
|
-
overflow: 'hidden',
|
|
721
|
-
borderWidth: 1,
|
|
722
|
-
borderColor: '#e0e0e0',
|
|
723
|
-
}}
|
|
724
|
-
>
|
|
725
|
-
<Image
|
|
726
|
-
key={selectedImage}
|
|
727
|
-
alt={'selected image'}
|
|
728
|
-
source={{ uri: selectedImage }}
|
|
1267
|
+
{images.map((img, index) => (
|
|
1268
|
+
<View
|
|
1269
|
+
key={`image-preview-${index}`}
|
|
729
1270
|
style={{
|
|
730
|
-
width:
|
|
731
|
-
height:
|
|
1271
|
+
width: 40,
|
|
1272
|
+
height: 40,
|
|
1273
|
+
marginRight: 15,
|
|
1274
|
+
borderRadius: 4,
|
|
1275
|
+
backgroundColor: colors.gray[200],
|
|
1276
|
+
overflow: 'hidden',
|
|
1277
|
+
borderWidth: 1,
|
|
1278
|
+
borderColor: '#e0e0e0',
|
|
1279
|
+
position: 'relative',
|
|
1280
|
+
zIndex: 10,
|
|
732
1281
|
}}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1282
|
+
>
|
|
1283
|
+
<Image
|
|
1284
|
+
alt={`selected image ${index + 1}`}
|
|
1285
|
+
source={{ uri: img.uri || img.url }}
|
|
1286
|
+
style={{ width: '100%', height: '100%' }}
|
|
1287
|
+
size={'md'}
|
|
1288
|
+
/>
|
|
1289
|
+
{/* Cross button at top right */}
|
|
1290
|
+
<TouchableOpacity
|
|
1291
|
+
onPress={() => {
|
|
1292
|
+
const newImages = [...images];
|
|
1293
|
+
newImages.splice(index, 1);
|
|
1294
|
+
setImages(newImages);
|
|
1295
|
+
if (newImages.length === 0) {
|
|
1296
|
+
setSelectedImage('');
|
|
1297
|
+
}
|
|
1298
|
+
}}
|
|
737
1299
|
style={{
|
|
738
1300
|
position: 'absolute',
|
|
739
|
-
top:
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1301
|
+
top: -1,
|
|
1302
|
+
right: -1,
|
|
1303
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
1304
|
+
borderRadius: 12,
|
|
1305
|
+
width: 20,
|
|
1306
|
+
height: 20,
|
|
745
1307
|
alignItems: 'center',
|
|
1308
|
+
justifyContent: 'center',
|
|
1309
|
+
zIndex: 9999,
|
|
746
1310
|
}}
|
|
747
1311
|
>
|
|
748
|
-
<
|
|
749
|
-
</
|
|
750
|
-
|
|
751
|
-
|
|
1312
|
+
<Ionicons name="close" size={16} color="white" />
|
|
1313
|
+
</TouchableOpacity>
|
|
1314
|
+
</View>
|
|
1315
|
+
))}
|
|
1316
|
+
</ScrollView>
|
|
1317
|
+
</Box>
|
|
1318
|
+
);
|
|
1319
|
+
}, [images]);
|
|
752
1320
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1321
|
+
// Add a custom Composer for Slack-like input
|
|
1322
|
+
const renderComposer = useCallback(
|
|
1323
|
+
(props) => (
|
|
1324
|
+
<TouchableOpacity
|
|
1325
|
+
style={{
|
|
1326
|
+
flexDirection: 'row',
|
|
1327
|
+
alignItems: 'center',
|
|
1328
|
+
backgroundColor: '#fff',
|
|
1329
|
+
minHeight: 48,
|
|
1330
|
+
maxHeight: 48,
|
|
1331
|
+
paddingHorizontal: 12,
|
|
1332
|
+
paddingVertical: 0,
|
|
1333
|
+
marginHorizontal: 8,
|
|
1334
|
+
flex: 1,
|
|
1335
|
+
}}
|
|
1336
|
+
activeOpacity={0.7}
|
|
1337
|
+
onPress={() => {
|
|
1338
|
+
// Use the callback we defined to show the action sheet
|
|
1339
|
+
setActionSheetVisible(true);
|
|
1340
|
+
}}
|
|
1341
|
+
>
|
|
1342
|
+
{/* Left + button */}
|
|
1343
|
+
<TouchableOpacity
|
|
1344
|
+
onPress={(e) => {
|
|
1345
|
+
e.stopPropagation(); // Prevent parent TouchableOpacity from triggering
|
|
1346
|
+
onSelectImages();
|
|
1347
|
+
}}
|
|
1348
|
+
style={{
|
|
1349
|
+
width: 25,
|
|
1350
|
+
height: 25,
|
|
1351
|
+
borderRadius: 20,
|
|
1352
|
+
backgroundColor: '#f5f5f5',
|
|
1353
|
+
alignItems: 'center',
|
|
1354
|
+
justifyContent: 'center',
|
|
1355
|
+
marginRight: 8,
|
|
1356
|
+
}}
|
|
1357
|
+
>
|
|
1358
|
+
<MaterialIcons name="add" size={20} color="#888" />
|
|
1359
|
+
</TouchableOpacity>
|
|
1360
|
+
|
|
1361
|
+
{/* Text placeholder - no need for actual input component */}
|
|
1362
|
+
<Text
|
|
1363
|
+
style={{
|
|
1364
|
+
fontSize: 16,
|
|
1365
|
+
color: msg ? colors.gray[800] : colors.gray[400],
|
|
1366
|
+
flex: 1,
|
|
1367
|
+
}}
|
|
1368
|
+
numberOfLines={1}
|
|
1369
|
+
ellipsizeMode="tail"
|
|
1370
|
+
>
|
|
1371
|
+
{msg ? msg : 'Jot something down'}
|
|
1372
|
+
</Text>
|
|
1373
|
+
</TouchableOpacity>
|
|
1374
|
+
),
|
|
1375
|
+
[msg, onSelectImages],
|
|
1376
|
+
);
|
|
761
1377
|
|
|
762
|
-
<TouchableHighlight
|
|
763
|
-
underlayColor={'rgba(0,0,0,0.1)'}
|
|
764
|
-
onPress={() => {
|
|
765
|
-
setFiles([]);
|
|
766
|
-
setImage('');
|
|
767
|
-
setImages([]);
|
|
768
|
-
}}
|
|
769
|
-
style={{
|
|
770
|
-
backgroundColor: colors.red[500],
|
|
771
|
-
borderRadius: 24,
|
|
772
|
-
width: 36,
|
|
773
|
-
height: 36,
|
|
774
|
-
alignItems: 'center',
|
|
775
|
-
justifyContent: 'center',
|
|
776
|
-
marginRight: 10,
|
|
777
|
-
}}
|
|
778
|
-
>
|
|
779
|
-
<Ionicons name="close" size={20} color="white" />
|
|
780
|
-
</TouchableHighlight>
|
|
781
|
-
</View>
|
|
782
|
-
</View>
|
|
783
|
-
);
|
|
784
|
-
}, [selectedImage, loading, images]);
|
|
785
1378
|
const setImageViewerObject = useCallback((obj: any, v: boolean) => {
|
|
786
1379
|
setImageObject(obj);
|
|
787
1380
|
setImageViewer(v);
|
|
@@ -804,75 +1397,85 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
804
1397
|
);
|
|
805
1398
|
}, [imageObject]);
|
|
806
1399
|
|
|
807
|
-
const
|
|
1400
|
+
const renderMessage1 = useCallback(
|
|
808
1401
|
(props: any) => {
|
|
809
1402
|
const { currentMessage } = props;
|
|
810
1403
|
|
|
811
|
-
//
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
</Box>
|
|
821
|
-
)}
|
|
822
|
-
{currentMessage.image && (
|
|
823
|
-
<Box className="h-[150px] w-[150px] rounded-lg bg-gray-200 overflow-hidden mt-1">
|
|
824
|
-
<Box className="flex-1 items-center justify-center">
|
|
825
|
-
<Spinner size="small" color={colors.blue[500]} />
|
|
826
|
-
</Box>
|
|
827
|
-
<Skeleton variant="rounded" className="flex-1" />
|
|
828
|
-
</Box>
|
|
829
|
-
)}
|
|
830
|
-
</Box>
|
|
831
|
-
);
|
|
832
|
-
}
|
|
1404
|
+
// Add special props for handling image upload states
|
|
1405
|
+
const customProps = {
|
|
1406
|
+
...props,
|
|
1407
|
+
isShowImageViewer: isShowImageViewer,
|
|
1408
|
+
setImageViewer: setImageViewerObject,
|
|
1409
|
+
// Pass custom flags to indicate uploading/optimistic states
|
|
1410
|
+
isUploading: currentMessage.isUploading || false,
|
|
1411
|
+
isOptimistic: currentMessage.isOptimistic || false,
|
|
1412
|
+
};
|
|
833
1413
|
|
|
1414
|
+
// Use SlackMessage component with our enhanced props
|
|
1415
|
+
return <SlackMessage {...customProps} />;
|
|
1416
|
+
},
|
|
1417
|
+
[isShowImageViewer, setImageViewerObject],
|
|
1418
|
+
);
|
|
1419
|
+
const renderMessage = useCallback(
|
|
1420
|
+
(props: any) => {
|
|
1421
|
+
// For all messages, use the SlackMessage component directly
|
|
834
1422
|
return (
|
|
835
1423
|
<SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
|
|
836
1424
|
);
|
|
837
1425
|
},
|
|
838
|
-
[
|
|
1426
|
+
[isShowImageViewer],
|
|
839
1427
|
);
|
|
840
1428
|
|
|
841
|
-
const renderInputToolbar = useCallback(
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1429
|
+
const renderInputToolbar = useCallback(
|
|
1430
|
+
(props) => {
|
|
1431
|
+
if (isActionSheetVisible) return null;
|
|
1432
|
+
|
|
1433
|
+
return (
|
|
1434
|
+
<InputToolbar
|
|
1435
|
+
{...props}
|
|
1436
|
+
containerStyle={{
|
|
1437
|
+
backgroundColor: 'transparent',
|
|
1438
|
+
padding: 0,
|
|
1439
|
+
margin: 0,
|
|
1440
|
+
borderTopWidth: 1,
|
|
1441
|
+
borderTopColor: colors.gray[200],
|
|
1442
|
+
}}
|
|
1443
|
+
primaryStyle={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}
|
|
1444
|
+
// Add action slot for direct image selection button
|
|
1445
|
+
// renderActions={() => (
|
|
1446
|
+
// <TouchableOpacity
|
|
1447
|
+
// onPress={onSelectImages}
|
|
1448
|
+
// style={{
|
|
1449
|
+
// padding: 8,
|
|
1450
|
+
// marginLeft: 4,
|
|
1451
|
+
// marginBottom: Platform.OS === 'ios' ? 0 : 3,
|
|
1452
|
+
// }}
|
|
1453
|
+
// >
|
|
1454
|
+
// <Ionicons name="image" size={24} color={colors.blue[500]} />
|
|
1455
|
+
// </TouchableOpacity>
|
|
1456
|
+
// )}
|
|
1457
|
+
/>
|
|
1458
|
+
);
|
|
1459
|
+
},
|
|
1460
|
+
[isActionSheetVisible, onSelectImages],
|
|
1461
|
+
);
|
|
861
1462
|
|
|
862
1463
|
const renderLoadEarlier = useCallback(() => {
|
|
863
|
-
return loadingOldMessages ? (
|
|
1464
|
+
return loadingOldMessages && !refreshing ? (
|
|
864
1465
|
<Box
|
|
865
1466
|
style={{
|
|
866
1467
|
padding: 10,
|
|
867
1468
|
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
868
1469
|
borderRadius: 10,
|
|
869
1470
|
marginTop: 10,
|
|
1471
|
+
alignItems: 'center',
|
|
870
1472
|
}}
|
|
871
1473
|
>
|
|
872
1474
|
<Spinner size="small" color={colors.blue[500]} />
|
|
1475
|
+
<Text style={{ fontSize: 12, color: colors.gray[600], marginTop: 4 }}>Loading earlier messages...</Text>
|
|
873
1476
|
</Box>
|
|
874
1477
|
) : null;
|
|
875
|
-
}, [loadingOldMessages]);
|
|
1478
|
+
}, [loadingOldMessages, refreshing]);
|
|
876
1479
|
|
|
877
1480
|
let onScroll = false;
|
|
878
1481
|
|
|
@@ -888,8 +1491,69 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
888
1491
|
onScroll = false;
|
|
889
1492
|
};
|
|
890
1493
|
|
|
1494
|
+
const renderChatFooter = useCallback(() => {
|
|
1495
|
+
return (
|
|
1496
|
+
<>
|
|
1497
|
+
<ImageViewerModal
|
|
1498
|
+
isVisible={isShowImageViewer}
|
|
1499
|
+
setVisible={setImageViewer}
|
|
1500
|
+
modalContent={modalContent}
|
|
1501
|
+
/>
|
|
1502
|
+
<SubscriptionHandler
|
|
1503
|
+
channelId={channelId}
|
|
1504
|
+
subscribeToNewMessages={() =>
|
|
1505
|
+
subscribeToMore({
|
|
1506
|
+
document: CHAT_MESSAGE_ADDED,
|
|
1507
|
+
variables: {
|
|
1508
|
+
channelId: channelId?.toString(),
|
|
1509
|
+
postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
|
|
1510
|
+
},
|
|
1511
|
+
updateQuery: (prev, { subscriptionData }: any) => {
|
|
1512
|
+
if (!subscriptionData.data) return prev;
|
|
1513
|
+
const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
|
|
1514
|
+
const prevReplyCount: any = prev?.getPostThread?.replyCount;
|
|
1515
|
+
const newReplyCount = prevReplyCount || 0 + 1;
|
|
1516
|
+
const replies = prev?.getPostThread?.replies || [];
|
|
1517
|
+
setChannelMessages((oldMessages: any) =>
|
|
1518
|
+
uniqBy([...oldMessages, newMessage], ({ id }) => id),
|
|
1519
|
+
);
|
|
1520
|
+
setTotalCount(newReplyCount);
|
|
1521
|
+
return Object.assign({}, prev, {
|
|
1522
|
+
getPostThread: {
|
|
1523
|
+
...prev?.getPostThread,
|
|
1524
|
+
lastReplyAt: newMessage.createdAt,
|
|
1525
|
+
replies: [newMessage, ...replies],
|
|
1526
|
+
replyCount: newReplyCount,
|
|
1527
|
+
updatedAt: newMessage.createdAt,
|
|
1528
|
+
},
|
|
1529
|
+
});
|
|
1530
|
+
},
|
|
1531
|
+
})
|
|
1532
|
+
}
|
|
1533
|
+
/>
|
|
1534
|
+
</>
|
|
1535
|
+
);
|
|
1536
|
+
}, [channelId, isShowImageViewer, modalContent, parentId, setImageViewer, subscribeToMore]);
|
|
1537
|
+
|
|
1538
|
+
const handleRemoveImage = useCallback(
|
|
1539
|
+
(index: number) => {
|
|
1540
|
+
const newImages = [...images];
|
|
1541
|
+
newImages.splice(index, 1);
|
|
1542
|
+
setImages(newImages);
|
|
1543
|
+
if (newImages.length === 0) {
|
|
1544
|
+
setSelectedImage('');
|
|
1545
|
+
if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
|
|
1546
|
+
textInputRef.current.focus();
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
[images],
|
|
1551
|
+
);
|
|
1552
|
+
|
|
891
1553
|
return (
|
|
892
|
-
|
|
1554
|
+
<View style={{ flex: 1, marginTop: -40 }}>
|
|
1555
|
+
{errorMessage ? <ErrorNotification message={errorMessage} onClose={() => setErrorMessage('')} /> : null}
|
|
1556
|
+
{loading && <Spinner color={'#3b82f6'} />}
|
|
893
1557
|
{isPostParentIdThread && (
|
|
894
1558
|
<>
|
|
895
1559
|
{threadPost?.length > 0 && (
|
|
@@ -940,18 +1604,112 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
940
1604
|
)}
|
|
941
1605
|
</>
|
|
942
1606
|
)}
|
|
943
|
-
<
|
|
1607
|
+
<GiftedChatInboxComponent
|
|
944
1608
|
ref={threadMessageListRef}
|
|
1609
|
+
onRemoveImage={handleRemoveImage}
|
|
1610
|
+
images={images}
|
|
1611
|
+
onSelectImages={onSelectImages}
|
|
1612
|
+
selectedImage={selectedImage}
|
|
1613
|
+
setSelectedImage={setSelectedImage}
|
|
1614
|
+
isUploadingImage={isUploadingImage}
|
|
1615
|
+
loading={loading}
|
|
945
1616
|
wrapInSafeArea={true}
|
|
1617
|
+
inverted={true}
|
|
946
1618
|
renderLoading={() => <Skeleton variant="rounded" style={{ flex: 1 }} />}
|
|
947
1619
|
messages={messageList}
|
|
948
1620
|
listViewProps={{
|
|
949
1621
|
onEndReached: onEndReached,
|
|
950
1622
|
onEndReachedThreshold: 0.5,
|
|
951
1623
|
onMomentumScrollBegin: onMomentumScrollBegin,
|
|
1624
|
+
style: { marginTop: 0 },
|
|
1625
|
+
contentContainerStyle: {
|
|
1626
|
+
paddingTop: 0,
|
|
1627
|
+
paddingBottom: selectedImage ? 90 : 0,
|
|
1628
|
+
},
|
|
1629
|
+
refreshControl: (
|
|
1630
|
+
<RefreshControl
|
|
1631
|
+
refreshing={refreshing}
|
|
1632
|
+
onRefresh={onRefresh}
|
|
1633
|
+
colors={[colors.blue[500]]}
|
|
1634
|
+
tintColor={colors.blue[500]}
|
|
1635
|
+
title="Loading earlier messages..."
|
|
1636
|
+
titleColor={colors.gray[600]}
|
|
1637
|
+
/>
|
|
1638
|
+
),
|
|
952
1639
|
}}
|
|
953
1640
|
onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
|
|
954
|
-
text={msg
|
|
1641
|
+
text={msg || ' '}
|
|
1642
|
+
onInputTextChanged={(text) => setMsg(text)}
|
|
1643
|
+
renderFooter={() => null}
|
|
1644
|
+
scrollToBottom
|
|
1645
|
+
user={{
|
|
1646
|
+
_id: auth?.id || '',
|
|
1647
|
+
}}
|
|
1648
|
+
placeholder="Jot something down"
|
|
1649
|
+
infiniteScroll={true}
|
|
1650
|
+
renderSend={renderSend}
|
|
1651
|
+
renderMessageText={renderMessageText}
|
|
1652
|
+
renderMessage={renderMessage}
|
|
1653
|
+
renderLoadEarlier={renderLoadEarlier}
|
|
1654
|
+
loadEarlier={totalCount > channelMessages.length}
|
|
1655
|
+
isLoadingEarlier={loadingOldMessages}
|
|
1656
|
+
textInputProps={{
|
|
1657
|
+
multiline: true,
|
|
1658
|
+
returnKeyType: 'default',
|
|
1659
|
+
enablesReturnKeyAutomatically: true,
|
|
1660
|
+
placeholderTextColor: colors.gray[400],
|
|
1661
|
+
// Allow direct editing in the main input too for better UX
|
|
1662
|
+
editable: true,
|
|
1663
|
+
onFocus: () => setIsGiftedInputFocused(true),
|
|
1664
|
+
onBlur: () => setIsGiftedInputFocused(false),
|
|
1665
|
+
}}
|
|
1666
|
+
renderChatFooter={renderChatFooter}
|
|
1667
|
+
messagesContainerStyle={{
|
|
1668
|
+
...(messageList?.length == 0 ? { transform: [{ scaleY: -1 }] } : {}),
|
|
1669
|
+
// marginTop: -40, // Negative margin to remove the top padding
|
|
1670
|
+
paddingTop: 0,
|
|
1671
|
+
}}
|
|
1672
|
+
// renderChatEmpty={() => (
|
|
1673
|
+
// <>
|
|
1674
|
+
// {!threadLoading && messageList && messageList?.length == 0 && (
|
|
1675
|
+
// <Box className="p-5">
|
|
1676
|
+
// <Center className="mt-6">
|
|
1677
|
+
// <Ionicons name="chatbubbles" size={30} />
|
|
1678
|
+
// <Text>You don't have any message yet!</Text>
|
|
1679
|
+
// </Center>
|
|
1680
|
+
// </Box>
|
|
1681
|
+
// )}
|
|
1682
|
+
// </>
|
|
1683
|
+
// )}
|
|
1684
|
+
/>
|
|
1685
|
+
{/* <GiftedChat
|
|
1686
|
+
ref={threadMessageListRef}
|
|
1687
|
+
wrapInSafeArea={true}
|
|
1688
|
+
inverted={true}
|
|
1689
|
+
renderLoading={() => <Skeleton variant="rounded" style={{ flex: 1 }} />}
|
|
1690
|
+
messages={messageList}
|
|
1691
|
+
listViewProps={{
|
|
1692
|
+
onEndReached: onEndReached,
|
|
1693
|
+
onEndReachedThreshold: 0.5,
|
|
1694
|
+
onMomentumScrollBegin: onMomentumScrollBegin,
|
|
1695
|
+
style: { marginTop: 0 },
|
|
1696
|
+
contentContainerStyle: {
|
|
1697
|
+
paddingTop: 0,
|
|
1698
|
+
paddingBottom: selectedImage ? 90 : 0,
|
|
1699
|
+
},
|
|
1700
|
+
refreshControl: (
|
|
1701
|
+
<RefreshControl
|
|
1702
|
+
refreshing={refreshing}
|
|
1703
|
+
onRefresh={onRefresh}
|
|
1704
|
+
colors={[colors.blue[500]]}
|
|
1705
|
+
tintColor={colors.blue[500]}
|
|
1706
|
+
title="Loading earlier messages..."
|
|
1707
|
+
titleColor={colors.gray[600]}
|
|
1708
|
+
/>
|
|
1709
|
+
),
|
|
1710
|
+
}}
|
|
1711
|
+
onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
|
|
1712
|
+
text={msg || ' '}
|
|
955
1713
|
onInputTextChanged={(text) => setMsg(text)}
|
|
956
1714
|
renderFooter={() => null}
|
|
957
1715
|
scrollToBottom
|
|
@@ -964,80 +1722,33 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
964
1722
|
renderSend={renderSend}
|
|
965
1723
|
renderMessageText={renderMessageText}
|
|
966
1724
|
renderInputToolbar={renderInputToolbar}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
renderAccessory={
|
|
1725
|
+
renderComposer={renderComposer}
|
|
1726
|
+
minInputToolbarHeight={isActionSheetVisible ? 0 : 56}
|
|
1727
|
+
renderAccessory={images.length > 0 ? renderAccessory : null}
|
|
970
1728
|
renderMessage={renderMessage}
|
|
971
1729
|
renderLoadEarlier={renderLoadEarlier}
|
|
972
1730
|
loadEarlier={totalCount > channelMessages.length}
|
|
973
1731
|
isLoadingEarlier={loadingOldMessages}
|
|
974
|
-
bottomOffset={Platform.OS === 'ios' ? 10 : 0}
|
|
1732
|
+
bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0}
|
|
975
1733
|
textInputProps={{
|
|
976
|
-
style: {
|
|
977
|
-
borderWidth: 1,
|
|
978
|
-
borderColor: colors.gray[300],
|
|
979
|
-
backgroundColor: '#f8f8f8',
|
|
980
|
-
borderRadius: 20,
|
|
981
|
-
minHeight: 36,
|
|
982
|
-
maxHeight: 80,
|
|
983
|
-
color: '#000',
|
|
984
|
-
padding: 8,
|
|
985
|
-
paddingHorizontal: 15,
|
|
986
|
-
fontSize: 16,
|
|
987
|
-
flex: 1,
|
|
988
|
-
marginVertical: 2,
|
|
989
|
-
marginBottom: 0,
|
|
990
|
-
},
|
|
991
1734
|
multiline: true,
|
|
992
1735
|
returnKeyType: 'default',
|
|
993
1736
|
enablesReturnKeyAutomatically: true,
|
|
994
1737
|
placeholderTextColor: colors.gray[400],
|
|
1738
|
+
// Allow direct editing in the main input too for better UX
|
|
1739
|
+
editable: true,
|
|
1740
|
+
onFocus: () => setIsGiftedInputFocused(true),
|
|
1741
|
+
onBlur: () => setIsGiftedInputFocused(false),
|
|
995
1742
|
}}
|
|
996
1743
|
minComposerHeight={36}
|
|
997
1744
|
maxComposerHeight={100}
|
|
998
|
-
|
|
999
|
-
renderChatFooter={
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
/>
|
|
1006
|
-
<SubscriptionHandler
|
|
1007
|
-
channelId={channelId}
|
|
1008
|
-
subscribeToNewMessages={() =>
|
|
1009
|
-
subscribeToMore({
|
|
1010
|
-
document: CHAT_MESSAGE_ADDED,
|
|
1011
|
-
variables: {
|
|
1012
|
-
channelId: channelId?.toString(),
|
|
1013
|
-
postParentId: !parentId || parentId == 0 ? null : parentId?.toString(),
|
|
1014
|
-
},
|
|
1015
|
-
updateQuery: (prev, { subscriptionData }: any) => {
|
|
1016
|
-
if (!subscriptionData.data) return prev;
|
|
1017
|
-
const newMessage: any = subscriptionData?.data?.threadChatMessageAdded;
|
|
1018
|
-
const prevReplyCount: any = prev?.getPostThread?.replyCount;
|
|
1019
|
-
const newReplyCount = prevReplyCount || 0 + 1;
|
|
1020
|
-
const replies = prev?.getPostThread?.replies || [];
|
|
1021
|
-
setChannelMessages((oldMessages: any) =>
|
|
1022
|
-
uniqBy([...oldMessages, newMessage], ({ id }) => id),
|
|
1023
|
-
);
|
|
1024
|
-
setTotalCount(newReplyCount);
|
|
1025
|
-
return Object.assign({}, prev, {
|
|
1026
|
-
getPostThread: {
|
|
1027
|
-
...prev?.getPostThread,
|
|
1028
|
-
lastReplyAt: newMessage.createdAt,
|
|
1029
|
-
replies: [newMessage, ...replies],
|
|
1030
|
-
replyCount: newReplyCount,
|
|
1031
|
-
updatedAt: newMessage.createdAt,
|
|
1032
|
-
},
|
|
1033
|
-
});
|
|
1034
|
-
},
|
|
1035
|
-
})
|
|
1036
|
-
}
|
|
1037
|
-
/>
|
|
1038
|
-
</>
|
|
1039
|
-
)}
|
|
1040
|
-
messagesContainerStyle={messageList?.length == 0 && { transform: [{ scaleY: -1 }] }}
|
|
1745
|
+
isKeyboardInternallyHandled={false}
|
|
1746
|
+
renderChatFooter={renderChatFooter}
|
|
1747
|
+
messagesContainerStyle={{
|
|
1748
|
+
...(messageList?.length == 0 ? { transform: [{ scaleY: -1 }] } : {}),
|
|
1749
|
+
// marginTop: -40, // Negative margin to remove the top padding
|
|
1750
|
+
paddingTop: 0,
|
|
1751
|
+
}}
|
|
1041
1752
|
renderChatEmpty={() => (
|
|
1042
1753
|
<>
|
|
1043
1754
|
{!threadLoading && messageList && messageList?.length == 0 && (
|
|
@@ -1055,8 +1766,36 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
1055
1766
|
springConfig: { tension: 90000, friction: 90000 },
|
|
1056
1767
|
disabled: true,
|
|
1057
1768
|
}}
|
|
1058
|
-
/>
|
|
1059
|
-
|
|
1769
|
+
/> */}
|
|
1770
|
+
|
|
1771
|
+
{/* Add the expandable action sheet */}
|
|
1772
|
+
{/* {isActionSheetVisible && <Divider />}
|
|
1773
|
+
{isActionSheetVisible && (
|
|
1774
|
+
<ExpandableInputActionSheet
|
|
1775
|
+
isVisible={isActionSheetVisible}
|
|
1776
|
+
onClose={handleActionSheetClose}
|
|
1777
|
+
onSend={handleActionSheetSend}
|
|
1778
|
+
onSelectImages={onSelectImages}
|
|
1779
|
+
text={msg}
|
|
1780
|
+
onChangeText={(text) => {
|
|
1781
|
+
setMsg(text);
|
|
1782
|
+
setTextUpdatedInActionSheet(true);
|
|
1783
|
+
}}
|
|
1784
|
+
textInputRef={textInputRef}
|
|
1785
|
+
isSendDisabled={(!msg.trim() && images.length === 0) || isUploadingImage}
|
|
1786
|
+
selectedImages={images}
|
|
1787
|
+
loading={loading || isUploadingImage}
|
|
1788
|
+
onRemoveImage={(index) => {
|
|
1789
|
+
const newImages = [...images];
|
|
1790
|
+
newImages.splice(index, 1);
|
|
1791
|
+
setImages(newImages);
|
|
1792
|
+
if (newImages.length === 0) {
|
|
1793
|
+
setSelectedImage('');
|
|
1794
|
+
}
|
|
1795
|
+
}}
|
|
1796
|
+
/>
|
|
1797
|
+
)} */}
|
|
1798
|
+
</View>
|
|
1060
1799
|
);
|
|
1061
1800
|
};
|
|
1062
1801
|
|