@messenger-box/platform-mobile 10.0.3-alpha.5 → 10.0.3-alpha.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/lib/compute.js +2 -3
- package/lib/compute.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/queries/inboxQueries.js +65 -0
- package/lib/queries/inboxQueries.js.map +1 -0
- package/lib/routes.json +2 -3
- package/lib/screens/inbox/DialogMessages.js +1 -1
- package/lib/screens/inbox/DialogMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreadMessages.js +4 -8
- package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreads.js +57 -12
- package/lib/screens/inbox/DialogThreads.js.map +1 -1
- package/lib/screens/inbox/Inbox.js +1 -1
- package/lib/screens/inbox/Inbox.js.map +1 -1
- package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
- package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
- package/lib/screens/inbox/components/CachedImage/index.js +168 -46
- package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
- package/lib/screens/inbox/components/DialogItem.js +169 -0
- package/lib/screens/inbox/components/DialogItem.js.map +1 -0
- package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
- package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +147 -31
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
- package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +6 -1
- package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
- package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
- package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
- package/lib/screens/inbox/components/ThreadsViewItem.js +66 -55
- package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
- package/lib/screens/inbox/config/config.js +2 -2
- package/lib/screens/inbox/config/config.js.map +1 -1
- package/lib/screens/inbox/containers/ConversationView.js +1111 -434
- package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
- package/lib/screens/inbox/containers/Dialogs.js +193 -80
- package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
- package/lib/screens/inbox/containers/ThreadConversationView.js +725 -216
- package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
- package/lib/screens/inbox/containers/ThreadsView.js +83 -50
- package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
- package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
- package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
- package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
- package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
- package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
- package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
- package/package.json +4 -4
- package/src/compute.ts +5 -6
- package/src/index.ts +2 -0
- package/src/navigation/InboxNavigation.tsx +3 -3
- package/src/queries/inboxQueries.ts +299 -0
- package/src/queries/index.d.ts +2 -0
- package/src/queries/index.ts +1 -0
- package/src/screens/inbox/DialogMessages.tsx +1 -1
- package/src/screens/inbox/DialogThreadMessages.tsx +7 -14
- package/src/screens/inbox/DialogThreads.tsx +55 -61
- package/src/screens/inbox/Inbox.tsx +1 -1
- package/src/screens/inbox/components/Actionsheet.tsx +30 -0
- package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
- package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
- package/src/screens/inbox/components/DialogItem.tsx +306 -0
- package/src/screens/inbox/components/DialogsHeader.tsx +6 -13
- package/src/screens/inbox/components/DialogsListItem.tsx +262 -198
- package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
- package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
- package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
- package/src/screens/inbox/components/ServiceDialogsListItem.tsx +337 -194
- package/src/screens/inbox/components/SlackInput.tsx +23 -0
- package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
- package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +1 -1
- package/src/screens/inbox/components/SmartLoader.tsx +61 -0
- package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
- package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +53 -55
- package/src/screens/inbox/components/ThreadsViewItem.tsx +178 -285
- package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
- package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
- package/src/screens/inbox/config/config.ts +2 -2
- package/src/screens/inbox/containers/ConversationView.tsx +1843 -702
- package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
- package/src/screens/inbox/containers/Dialogs.tsx +402 -204
- package/src/screens/inbox/containers/SupportServiceDialogs.tsx +4 -4
- package/src/screens/inbox/containers/ThreadConversationView.tsx +1350 -319
- package/src/screens/inbox/containers/ThreadsView.tsx +105 -193
- package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
- package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
- package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
- package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
- package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
- package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
- package/src/screens/inbox/index.ts +37 -0
- package/src/screens/inbox/machines/threadsMachine.ts +147 -0
- package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
- package/tsconfig.json +11 -54
- package/lib/screens/inbox/components/DialogsListItem.js +0 -171
- package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
- package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -171
- package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
|
@@ -13,25 +13,45 @@ import {
|
|
|
13
13
|
Image,
|
|
14
14
|
Spinner,
|
|
15
15
|
Text,
|
|
16
|
+
Skeleton,
|
|
17
|
+
Toast,
|
|
18
|
+
ToastTitle,
|
|
19
|
+
ToastDescription,
|
|
20
|
+
useToast,
|
|
21
|
+
Divider,
|
|
16
22
|
} from '@admin-layout/gluestack-ui-mobile';
|
|
17
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
Platform,
|
|
25
|
+
TouchableHighlight,
|
|
26
|
+
View,
|
|
27
|
+
TouchableOpacity,
|
|
28
|
+
ScrollView,
|
|
29
|
+
Animated,
|
|
30
|
+
Keyboard,
|
|
31
|
+
RefreshControl,
|
|
32
|
+
} from 'react-native';
|
|
18
33
|
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
|
|
19
34
|
import { useSelector } from 'react-redux';
|
|
20
35
|
import { orderBy, startCase, uniqBy } from 'lodash-es';
|
|
21
36
|
import * as ImagePicker from 'expo-image-picker';
|
|
22
37
|
import { encode as atob } from 'base-64';
|
|
23
38
|
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
|
|
24
|
-
import { Actions, GiftedChat, IMessage, MessageText, Send } from 'react-native-gifted-chat';
|
|
25
|
-
import {
|
|
39
|
+
import { Actions, GiftedChat, IMessage, MessageText, Send, InputToolbar, Composer } from 'react-native-gifted-chat';
|
|
40
|
+
import {
|
|
41
|
+
IPost,
|
|
42
|
+
IPostThread,
|
|
43
|
+
PreDefinedRole,
|
|
44
|
+
IExpoNotificationData,
|
|
45
|
+
IFileInfo,
|
|
46
|
+
FileRefType,
|
|
47
|
+
PostTypeEnum,
|
|
48
|
+
} from 'common';
|
|
26
49
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
useThreadMessagesLazyQuery,
|
|
33
|
-
useGetPostThreadLazyQuery,
|
|
34
|
-
} from 'common/lib/generated/generated.js';
|
|
50
|
+
CHAT_MESSAGE_ADDED,
|
|
51
|
+
useCreatePostThread,
|
|
52
|
+
useSendExpoNotification,
|
|
53
|
+
useGetPostThreadLazy,
|
|
54
|
+
} from '../../../queries/inboxQueries';
|
|
35
55
|
import { useUploadFilesNative } from '@messenger-box/platform-client';
|
|
36
56
|
import { objectId } from '@messenger-box/core';
|
|
37
57
|
import { format, isToday, isYesterday } from 'date-fns';
|
|
@@ -39,6 +59,9 @@ import { userSelector } from '@adminide-stack/user-auth0-client';
|
|
|
39
59
|
import { config } from '../config';
|
|
40
60
|
import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
|
|
41
61
|
import CachedImage from '../components/CachedImage';
|
|
62
|
+
import colors from 'tailwindcss/colors';
|
|
63
|
+
import ExpandableInputActionSheet from '../components/ExpandableInputActionSheet';
|
|
64
|
+
import GiftedChatInboxComponent from '../components/GiftedChatInboxComponent';
|
|
42
65
|
|
|
43
66
|
const {
|
|
44
67
|
MESSAGES_PER_PAGE,
|
|
@@ -59,6 +82,9 @@ const createdAtText = (value: string) => {
|
|
|
59
82
|
interface IMessageProps extends IMessage {
|
|
60
83
|
type: string;
|
|
61
84
|
propsConfiguration?: any;
|
|
85
|
+
images?: string[];
|
|
86
|
+
isOptimistic?: boolean;
|
|
87
|
+
isUploading?: boolean;
|
|
62
88
|
}
|
|
63
89
|
|
|
64
90
|
export interface AlertMessageAttachmentsInterface {
|
|
@@ -76,21 +102,88 @@ interface IThreadSubscriptionHandlerProps {
|
|
|
76
102
|
channelId: string;
|
|
77
103
|
}
|
|
78
104
|
|
|
105
|
+
// Define an extended interface for ImagePickerAsset with url property
|
|
106
|
+
interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
|
|
107
|
+
url?: string;
|
|
108
|
+
fileName?: string;
|
|
109
|
+
mimeType?: string;
|
|
110
|
+
}
|
|
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
|
+
|
|
79
171
|
const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParentIdThread, role }: any) => {
|
|
80
172
|
const { params } = useRoute<any>();
|
|
81
173
|
const [channelToTop, setChannelToTop] = useState(0);
|
|
82
174
|
const [channelMessages, setChannelMessages] = useState<any>([]);
|
|
83
175
|
const auth: any = useSelector(userSelector);
|
|
84
176
|
const [totalCount, setTotalCount] = useState<any>(0);
|
|
85
|
-
const [selectedImage,
|
|
177
|
+
const [selectedImage, setSelectedImage] = useState<string>('');
|
|
86
178
|
const [loadingOldMessages, setLoadingOldMessages] = useState<boolean>(false);
|
|
87
179
|
const [loadEarlierMsg, setLoadEarlierMsg] = useState(false);
|
|
88
180
|
const navigation = useNavigation<any>();
|
|
89
181
|
const [files, setFiles] = useState<File[]>([]);
|
|
90
|
-
const [images, setImages] = useState<
|
|
182
|
+
const [images, setImages] = useState<ExtendedImagePickerAsset[]>([]);
|
|
91
183
|
const [msg, setMsg] = useState<string>('');
|
|
92
184
|
const [loading, setLoading] = useState(false);
|
|
93
185
|
const [imageLoading, setImageLoading] = useState(false);
|
|
186
|
+
const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
|
|
94
187
|
const [expoTokens, setExpoTokens] = useState<any[]>([]);
|
|
95
188
|
const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
|
|
96
189
|
const [imageObject, setImageObject] = useState<any>({});
|
|
@@ -100,15 +193,30 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
100
193
|
const [threadPost, setThreadPost] = useState<any[]>([]);
|
|
101
194
|
const [isScrollToBottom, setIsScrollToBottom] = useState(false);
|
|
102
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);
|
|
103
204
|
|
|
104
205
|
// const [sendThreadMessage] = useSendThreadMessageMutation();
|
|
105
|
-
const [sendThreadMessage] =
|
|
106
|
-
const [sendExpoNotificationOnPostMutation] =
|
|
206
|
+
const [sendThreadMessage] = useCreatePostThread();
|
|
207
|
+
const [sendExpoNotificationOnPostMutation] = useSendExpoNotification();
|
|
107
208
|
|
|
108
209
|
const [
|
|
109
210
|
getThreadMessages,
|
|
110
211
|
{ data, loading: threadLoading, fetchMore: fetchMoreMessages, refetch: refetchThreadMessages, subscribeToMore },
|
|
111
|
-
] =
|
|
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);
|
|
112
220
|
|
|
113
221
|
useFocusEffect(
|
|
114
222
|
React.useCallback(() => {
|
|
@@ -117,12 +225,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
117
225
|
navigation.setOptions({
|
|
118
226
|
title: params?.title ?? 'Thread',
|
|
119
227
|
headerLeft: (props: any) => (
|
|
120
|
-
<Button
|
|
121
|
-
bg={'transparent'}
|
|
122
|
-
$pressed-bg="$trueGray200"
|
|
123
|
-
$active-bg={'$trueGray200'}
|
|
124
|
-
onPress={() => navigation.goBack()}
|
|
125
|
-
>
|
|
228
|
+
<Button className="bg-transparent active:bg-gray-200 " onPress={() => navigation.goBack()}>
|
|
126
229
|
<MaterialIcons size={20} name="arrow-back-ios" color={'black'} />
|
|
127
230
|
</Button>
|
|
128
231
|
),
|
|
@@ -203,6 +306,8 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
203
306
|
const onFetchOld = useCallback(() => {
|
|
204
307
|
if (totalCount > channelMessages.length && !loadingOldMessages) {
|
|
205
308
|
setLoadEarlierMsg(true);
|
|
309
|
+
setLoadingOldMessages(true);
|
|
310
|
+
|
|
206
311
|
fetchMoreMessages({
|
|
207
312
|
variables: {
|
|
208
313
|
channelId: channelId?.toString(),
|
|
@@ -224,14 +329,24 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
224
329
|
.finally(() => {
|
|
225
330
|
setLoadEarlierMsg(false);
|
|
226
331
|
setLoadingOldMessages(false);
|
|
332
|
+
setRefreshing(false);
|
|
227
333
|
})
|
|
228
334
|
.catch((error: any) => {
|
|
229
335
|
setLoadEarlierMsg(false);
|
|
230
336
|
setLoadingOldMessages(false);
|
|
337
|
+
setRefreshing(false);
|
|
231
338
|
});
|
|
339
|
+
} else {
|
|
340
|
+
setRefreshing(false);
|
|
232
341
|
}
|
|
233
342
|
}, [parentId, channelId, totalCount, channelMessages]);
|
|
234
343
|
|
|
344
|
+
// Pull to refresh handler
|
|
345
|
+
const onRefresh = useCallback(() => {
|
|
346
|
+
setRefreshing(true);
|
|
347
|
+
onFetchOld();
|
|
348
|
+
}, [onFetchOld]);
|
|
349
|
+
|
|
235
350
|
// const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
|
|
236
351
|
// return contentOffset.y <= 100; // 100px from top
|
|
237
352
|
// };
|
|
@@ -253,118 +368,438 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
253
368
|
return new File([u8arr], filename, { type: mime });
|
|
254
369
|
};
|
|
255
370
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
base64: true,
|
|
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;
|
|
264
378
|
});
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
438
|
+
const onSelectImages = async () => {
|
|
439
|
+
setLoading(true);
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
let imageSource = await ImagePicker.launchImageLibraryAsync({
|
|
443
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
444
|
+
allowsEditing: false,
|
|
445
|
+
aspect: [4, 3],
|
|
446
|
+
quality: 0.8,
|
|
447
|
+
base64: true,
|
|
448
|
+
exif: false,
|
|
449
|
+
allowsMultipleSelection: true, // Enable multiple selection
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
if (!imageSource?.canceled) {
|
|
453
|
+
// Get all selected assets
|
|
454
|
+
const selectedAssets = imageSource?.assets || [];
|
|
455
|
+
if (selectedAssets.length === 0) {
|
|
456
|
+
setLoading(false);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
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
|
+
});
|
|
476
|
+
|
|
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
|
+
}
|
|
483
|
+
|
|
484
|
+
// Add new images to existing ones
|
|
485
|
+
setImages((currentImages) => [...currentImages, ...newImages]);
|
|
486
|
+
setLoading(false); // Set loading to false after successful selection
|
|
487
|
+
} else {
|
|
488
|
+
setLoading(false);
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
setLoading(false);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
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);
|
|
271
508
|
}
|
|
272
|
-
|
|
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);
|
|
273
520
|
};
|
|
274
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
|
+
|
|
275
533
|
const handleSend = useCallback(
|
|
276
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
|
+
|
|
277
538
|
if (!channelId) return;
|
|
278
|
-
|
|
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);
|
|
279
548
|
|
|
280
549
|
const postId = objectId();
|
|
550
|
+
const currentDate = new Date();
|
|
281
551
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
568
|
+
const optimisticMessage = {
|
|
569
|
+
id: postId,
|
|
570
|
+
message: currentMessageText || (currentImages.length > 0 ? ' ' : ''),
|
|
571
|
+
createdAt: currentDate.toISOString(),
|
|
572
|
+
author: {
|
|
573
|
+
id: auth?.profile?.id,
|
|
574
|
+
givenName: auth?.profile?.given_name,
|
|
575
|
+
familyName: auth?.profile?.family_name,
|
|
576
|
+
picture: auth?.profile?.picture,
|
|
577
|
+
},
|
|
578
|
+
isDelivered: false, // Mark as not delivered yet during upload
|
|
579
|
+
isRead: false,
|
|
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,
|
|
601
|
+
};
|
|
602
|
+
|
|
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
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
let fileIds: string[] | null = null;
|
|
614
|
+
|
|
615
|
+
// Handle file uploads if images exist
|
|
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,
|
|
315
643
|
},
|
|
316
|
-
}
|
|
317
|
-
update: (cache, { data, errors }: any) => {
|
|
318
|
-
if (!data || errors) {
|
|
319
|
-
setLoading(false);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
setPostThread(data?.createPostThread?.data);
|
|
323
|
-
const lastMessageId = data?.createPostThread?.lastMessage?.id;
|
|
324
|
-
if (!parentId || parentId == 0) {
|
|
325
|
-
setParentId(data?.createPostThread?.lastMessage?.id);
|
|
326
|
-
}
|
|
644
|
+
}));
|
|
327
645
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
646
|
+
const uploadResponse = await startUpload({
|
|
647
|
+
file: imagesToUpload as unknown as ImagePicker.ImagePickerAsset[],
|
|
648
|
+
saveUploadedFile: {
|
|
649
|
+
variables: {
|
|
650
|
+
postId,
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
createUploadLink: {
|
|
654
|
+
variables: {
|
|
655
|
+
postId,
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
if (uploadResponse?.error) {
|
|
661
|
+
throw new Error(uploadResponse.error.toString());
|
|
662
|
+
}
|
|
663
|
+
|
|
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
|
+
}
|
|
692
|
+
|
|
693
|
+
// Clear pending upload now that it's complete
|
|
694
|
+
setPendingUploads((prev) => {
|
|
695
|
+
const newPending = { ...prev };
|
|
696
|
+
delete newPending[postId];
|
|
697
|
+
return newPending;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
335
700
|
}
|
|
336
|
-
|
|
337
|
-
|
|
701
|
+
|
|
702
|
+
// Send message with Apollo mutation
|
|
338
703
|
await sendThreadMessage({
|
|
339
704
|
variables: {
|
|
705
|
+
postId,
|
|
340
706
|
channelId,
|
|
341
707
|
postThreadId: postThread && postThread?.id,
|
|
342
|
-
postParentId: !parentId || parentId
|
|
708
|
+
postParentId: !parentId || parentId === 0 ? null : parentId,
|
|
343
709
|
threadMessageInput: {
|
|
344
|
-
content:
|
|
710
|
+
content: currentMessageText,
|
|
711
|
+
files: fileIds,
|
|
345
712
|
role,
|
|
346
713
|
},
|
|
347
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
|
+
},
|
|
348
738
|
update: (cache, { data, errors }: any) => {
|
|
349
|
-
if (!data || errors)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
setPostThread(data?.createPostThread.data);
|
|
739
|
+
if (!data || errors) return;
|
|
740
|
+
|
|
741
|
+
setPostThread(data?.createPostThread?.data);
|
|
354
742
|
const lastMessageId = data?.createPostThread?.lastMessage?.id;
|
|
355
|
-
if (!parentId || parentId
|
|
743
|
+
if (!parentId || parentId === 0) {
|
|
356
744
|
setParentId(data?.createPostThread?.lastMessage?.id);
|
|
357
745
|
}
|
|
358
746
|
setChannelToTop(channelToTop + 1);
|
|
359
|
-
setLoading(false);
|
|
360
|
-
setMsg('');
|
|
361
747
|
|
|
748
|
+
// Send push notification with message info
|
|
362
749
|
sendPushNotification(lastMessageId, channelId, parentId, data?.createPostThread?.data?.id);
|
|
363
750
|
},
|
|
364
751
|
});
|
|
752
|
+
} catch (error) {
|
|
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
|
+
|
|
780
|
+
// Remove the optimistic message if there was an error
|
|
781
|
+
setChannelMessages((oldMessages: any) => oldMessages.filter((msg) => msg.id !== postId));
|
|
782
|
+
|
|
783
|
+
// Clean up any pending upload states
|
|
784
|
+
removeMessageFromUI(postId);
|
|
785
|
+
} finally {
|
|
786
|
+
setLoading(false);
|
|
787
|
+
setIsUploadingImage(false);
|
|
788
|
+
setUploadingMessageId(null);
|
|
365
789
|
}
|
|
366
790
|
},
|
|
367
|
-
[
|
|
791
|
+
[
|
|
792
|
+
auth,
|
|
793
|
+
channelId,
|
|
794
|
+
channelToTop,
|
|
795
|
+
images,
|
|
796
|
+
parentId,
|
|
797
|
+
postThread,
|
|
798
|
+
selectedImage,
|
|
799
|
+
setChannelMessages,
|
|
800
|
+
removeMessageFromUI,
|
|
801
|
+
role,
|
|
802
|
+
],
|
|
368
803
|
);
|
|
369
804
|
|
|
370
805
|
const sendPushNotification = async (messageId: string, channelId: string, parentId: any, threadId: string) => {
|
|
@@ -385,6 +820,7 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
385
820
|
}
|
|
386
821
|
};
|
|
387
822
|
|
|
823
|
+
// Improved messageList function to better handle images
|
|
388
824
|
const messageList = useMemo(() => {
|
|
389
825
|
let currentDate = '';
|
|
390
826
|
let res: any = [];
|
|
@@ -407,17 +843,56 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
407
843
|
message._id = msg.id;
|
|
408
844
|
message.text = msg.message;
|
|
409
845
|
message.createdAt = date;
|
|
410
|
-
|
|
846
|
+
message.user = {
|
|
411
847
|
_id: msg?.author?.id ?? auth?.profile?.id,
|
|
412
848
|
name:
|
|
413
849
|
msg?.author?.givenName ??
|
|
414
850
|
auth?.profile?.given_name + ' ' + msg?.author?.familyName ??
|
|
415
851
|
auth?.profile?.family_name,
|
|
416
852
|
avatar: msg?.author?.picture ?? auth?.profile?.picture,
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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;
|
|
421
896
|
message.type = msg?.type;
|
|
422
897
|
message.propsConfiguration = msg?.propsConfiguration;
|
|
423
898
|
res.push(message);
|
|
@@ -427,149 +902,494 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
427
902
|
//return res;
|
|
428
903
|
}, [channelMessages]);
|
|
429
904
|
|
|
430
|
-
const renderSend = (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
style={{ marginBottom: 5, marginRight: 5 }}
|
|
437
|
-
size={32}
|
|
438
|
-
color="#2e64e5"
|
|
439
|
-
/>
|
|
440
|
-
</Box>
|
|
441
|
-
</Send>
|
|
442
|
-
);
|
|
443
|
-
};
|
|
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
|
+
}
|
|
444
911
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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(
|
|
954
|
+
(props: any) => {
|
|
955
|
+
const { currentMessage } = props;
|
|
956
|
+
if (currentMessage.type === 'ALERT') {
|
|
957
|
+
const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
|
|
958
|
+
let action: string = '';
|
|
959
|
+
let actionId: any = '';
|
|
960
|
+
let params: any = {};
|
|
961
|
+
if (attachment?.callToAction?.extraParams) {
|
|
962
|
+
const extraParams: any = attachment?.callToAction?.extraParams;
|
|
963
|
+
const route: any = extraParams?.route ?? null;
|
|
964
|
+
let path: any = null;
|
|
965
|
+
let param: any = null;
|
|
966
|
+
if (role && role == PreDefinedRole.Guest) {
|
|
967
|
+
path = route?.guest?.name ? route?.guest?.name ?? null : null;
|
|
968
|
+
param = route?.guest?.params ? route?.guest?.params ?? null : null;
|
|
969
|
+
} else if (role && role == PreDefinedRole.Owner) {
|
|
970
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
971
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
972
|
+
} else {
|
|
973
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
974
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
action = path;
|
|
978
|
+
params = { ...param };
|
|
979
|
+
} else if (attachment?.callToAction?.link) {
|
|
980
|
+
action = CALL_TO_ACTION_PATH;
|
|
981
|
+
actionId = attachment?.callToAction?.link.split('/').pop();
|
|
982
|
+
params = { reservationId: actionId };
|
|
466
983
|
}
|
|
467
984
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
985
|
+
return (
|
|
986
|
+
<>
|
|
987
|
+
{attachment?.callToAction && action ? (
|
|
988
|
+
<Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
|
|
989
|
+
<Button
|
|
990
|
+
variant={'outline'}
|
|
991
|
+
size={'sm'}
|
|
992
|
+
className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}]`}
|
|
993
|
+
onPress={() => action && params && navigation.navigate(action, params)}
|
|
994
|
+
>
|
|
995
|
+
<ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
|
|
996
|
+
{attachment.callToAction.title}
|
|
997
|
+
</ButtonText>
|
|
998
|
+
</Button>
|
|
999
|
+
<MessageText
|
|
1000
|
+
{...props}
|
|
1001
|
+
textStyle={{
|
|
1002
|
+
left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 },
|
|
1003
|
+
}}
|
|
1004
|
+
/>
|
|
1005
|
+
</Box>
|
|
1006
|
+
) : (
|
|
1007
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
1008
|
+
)}
|
|
1009
|
+
</>
|
|
1010
|
+
);
|
|
1011
|
+
} else {
|
|
1012
|
+
return <MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />;
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
[navigation, role],
|
|
1016
|
+
);
|
|
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;
|
|
474
1027
|
}
|
|
475
1028
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1029
|
+
if (currentMessage.type === 'ALERT') {
|
|
1030
|
+
const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
|
|
1031
|
+
let action: string = '';
|
|
1032
|
+
let actionId: any = '';
|
|
1033
|
+
let params: any = {};
|
|
480
1034
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
+
});
|
|
500
1092
|
}}
|
|
501
|
-
|
|
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
|
+
|
|
1207
|
+
// const renderActions = useCallback((props) => {
|
|
1208
|
+
// return (
|
|
1209
|
+
// <Actions
|
|
1210
|
+
// {...props}
|
|
1211
|
+
// icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
|
|
1212
|
+
// />
|
|
1213
|
+
// );
|
|
1214
|
+
// }, [onSelectImages]);
|
|
1215
|
+
|
|
1216
|
+
// Render action buttons (including image upload)
|
|
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]} />
|
|
502
1235
|
</Box>
|
|
503
|
-
) : (
|
|
504
|
-
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
505
1236
|
)}
|
|
506
|
-
{
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1237
|
+
containerStyle={{
|
|
1238
|
+
alignItems: 'center',
|
|
1239
|
+
justifyContent: 'center',
|
|
1240
|
+
marginLeft: 8,
|
|
1241
|
+
marginBottom: 0,
|
|
1242
|
+
}}
|
|
1243
|
+
/>
|
|
511
1244
|
);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const renderActions = (props) => {
|
|
518
|
-
return (
|
|
519
|
-
<Actions
|
|
520
|
-
{...props}
|
|
521
|
-
icon={() => <Ionicons name={'image'} size={30} color={'black'} onPress={onSelectImages} />}
|
|
522
|
-
/>
|
|
523
|
-
);
|
|
524
|
-
};
|
|
1245
|
+
},
|
|
1246
|
+
[onSelectImages],
|
|
1247
|
+
);
|
|
525
1248
|
|
|
526
|
-
|
|
1249
|
+
// Create a more visible and reliable image preview with cancel button
|
|
1250
|
+
const renderAccessory = useCallback(() => {
|
|
1251
|
+
if (!images.length) return null;
|
|
527
1252
|
return (
|
|
528
|
-
<Box>
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1253
|
+
<Box style={{ position: 'relative', height: 70, backgroundColor: 'transparent', justifyContent: 'center' }}>
|
|
1254
|
+
<ScrollView
|
|
1255
|
+
horizontal
|
|
1256
|
+
showsHorizontalScrollIndicator={false}
|
|
1257
|
+
style={{
|
|
1258
|
+
flexDirection: 'row',
|
|
1259
|
+
paddingLeft: 15,
|
|
1260
|
+
paddingRight: 5,
|
|
1261
|
+
}}
|
|
1262
|
+
contentContainerStyle={{
|
|
1263
|
+
alignItems: 'center',
|
|
1264
|
+
height: '100%',
|
|
1265
|
+
}}
|
|
1266
|
+
>
|
|
1267
|
+
{images.map((img, index) => (
|
|
1268
|
+
<View
|
|
1269
|
+
key={`image-preview-${index}`}
|
|
1270
|
+
style={{
|
|
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,
|
|
546
1281
|
}}
|
|
547
1282
|
>
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
+
}}
|
|
1299
|
+
style={{
|
|
1300
|
+
position: 'absolute',
|
|
1301
|
+
top: -1,
|
|
1302
|
+
right: -1,
|
|
1303
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
1304
|
+
borderRadius: 12,
|
|
1305
|
+
width: 20,
|
|
1306
|
+
height: 20,
|
|
1307
|
+
alignItems: 'center',
|
|
1308
|
+
justifyContent: 'center',
|
|
1309
|
+
zIndex: 9999,
|
|
1310
|
+
}}
|
|
1311
|
+
>
|
|
1312
|
+
<Ionicons name="close" size={16} color="white" />
|
|
1313
|
+
</TouchableOpacity>
|
|
1314
|
+
</View>
|
|
1315
|
+
))}
|
|
1316
|
+
</ScrollView>
|
|
552
1317
|
</Box>
|
|
553
1318
|
);
|
|
554
|
-
};
|
|
1319
|
+
}, [images]);
|
|
555
1320
|
|
|
556
|
-
|
|
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
|
+
);
|
|
1377
|
+
|
|
1378
|
+
const setImageViewerObject = useCallback((obj: any, v: boolean) => {
|
|
557
1379
|
setImageObject(obj);
|
|
558
1380
|
setImageViewer(v);
|
|
559
|
-
};
|
|
1381
|
+
}, []);
|
|
560
1382
|
|
|
561
|
-
const modalContent =
|
|
1383
|
+
const modalContent = useMemo(() => {
|
|
562
1384
|
if (!imageObject) return <></>;
|
|
563
1385
|
const { image, _id } = imageObject;
|
|
564
1386
|
return (
|
|
565
1387
|
<CachedImage
|
|
566
1388
|
style={{ width: '100%', height: '100%' }}
|
|
567
1389
|
resizeMode={'cover'}
|
|
568
|
-
// cacheKey={`${_id}-conversation-modal-image-key`}
|
|
569
1390
|
cacheKey={`${_id}-slack-bubble-imageKey`}
|
|
570
1391
|
source={{
|
|
571
1392
|
uri: image,
|
|
572
|
-
//headers: `Authorization: Bearer ${token}`,
|
|
573
1393
|
expiresIn: 86400,
|
|
574
1394
|
}}
|
|
575
1395
|
alt={'image'}
|
|
@@ -577,39 +1397,170 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
577
1397
|
);
|
|
578
1398
|
}, [imageObject]);
|
|
579
1399
|
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
1400
|
+
const renderMessage1 = useCallback(
|
|
1401
|
+
(props: any) => {
|
|
1402
|
+
const { currentMessage } = props;
|
|
1403
|
+
|
|
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
|
+
};
|
|
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
|
|
1422
|
+
return (
|
|
1423
|
+
<SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
|
|
1424
|
+
);
|
|
1425
|
+
},
|
|
1426
|
+
[isShowImageViewer],
|
|
1427
|
+
);
|
|
1428
|
+
|
|
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
|
+
);
|
|
1462
|
+
|
|
1463
|
+
const renderLoadEarlier = useCallback(() => {
|
|
1464
|
+
return loadingOldMessages && !refreshing ? (
|
|
1465
|
+
<Box
|
|
1466
|
+
style={{
|
|
1467
|
+
padding: 10,
|
|
1468
|
+
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
1469
|
+
borderRadius: 10,
|
|
1470
|
+
marginTop: 10,
|
|
1471
|
+
alignItems: 'center',
|
|
1472
|
+
}}
|
|
1473
|
+
>
|
|
1474
|
+
<Spinner size="small" color={colors.blue[500]} />
|
|
1475
|
+
<Text style={{ fontSize: 12, color: colors.gray[600], marginTop: 4 }}>Loading earlier messages...</Text>
|
|
1476
|
+
</Box>
|
|
1477
|
+
) : null;
|
|
1478
|
+
}, [loadingOldMessages, refreshing]);
|
|
583
1479
|
|
|
584
1480
|
let onScroll = false;
|
|
585
1481
|
|
|
586
1482
|
const onMomentumScrollBegin = ({ nativeEvent }: any) => {
|
|
587
1483
|
onScroll = true;
|
|
588
|
-
console.log('scroll top');
|
|
589
1484
|
if (!loadingOldMessages && isCloseToTop(nativeEvent) && totalCount > channelMessages?.length) {
|
|
590
1485
|
onFetchOld();
|
|
591
1486
|
}
|
|
592
1487
|
};
|
|
593
1488
|
|
|
594
1489
|
const onEndReached = () => {
|
|
595
|
-
console.log('on end reached');
|
|
596
1490
|
if (!onScroll) return;
|
|
597
|
-
// load messages, show ActivityIndicator
|
|
598
1491
|
onScroll = false;
|
|
599
|
-
// setLoadingOldMessages(true);
|
|
600
1492
|
};
|
|
601
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
|
+
|
|
602
1553
|
return (
|
|
603
|
-
|
|
604
|
-
{
|
|
605
|
-
{
|
|
1554
|
+
<View style={{ flex: 1, marginTop: -40 }}>
|
|
1555
|
+
{errorMessage ? <ErrorNotification message={errorMessage} onClose={() => setErrorMessage('')} /> : null}
|
|
1556
|
+
{loading && <Spinner color={'#3b82f6'} />}
|
|
606
1557
|
{isPostParentIdThread && (
|
|
607
1558
|
<>
|
|
608
1559
|
{threadPost?.length > 0 && (
|
|
609
1560
|
<>
|
|
610
|
-
<VStack px
|
|
611
|
-
<HStack space={'sm'}
|
|
612
|
-
<Avatar bg
|
|
1561
|
+
<VStack className="px-2 pt-2 pb-0" space={'sm'}>
|
|
1562
|
+
<HStack space={'sm'} className="items-center">
|
|
1563
|
+
<Avatar className="bg-transparent" size={'md'}>
|
|
613
1564
|
<AvatarFallbackText>
|
|
614
1565
|
{startCase(threadPost[0]?.author?.username?.charAt(0))}
|
|
615
1566
|
</AvatarFallbackText>
|
|
@@ -626,30 +1577,24 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
626
1577
|
/>
|
|
627
1578
|
</Avatar>
|
|
628
1579
|
<Box>
|
|
629
|
-
<Text color
|
|
1580
|
+
<Text className="font-bold color-black">
|
|
630
1581
|
{threadPost[0]?.author?.givenName ?? ''}{' '}
|
|
631
1582
|
{threadPost[0]?.author?.familyName ?? ''}
|
|
632
1583
|
</Text>
|
|
633
|
-
<Text
|
|
1584
|
+
<Text className="pl-0 color-gray-500">
|
|
634
1585
|
{createdAtText(threadPost[0]?.createdAt)} at{' '}
|
|
635
1586
|
{format(new Date(threadPost[0]?.createdAt), 'hh:ss:a')}
|
|
636
1587
|
</Text>
|
|
637
1588
|
</Box>
|
|
638
1589
|
</HStack>
|
|
639
|
-
<HStack
|
|
1590
|
+
<HStack space={'sm'} className="px-2 items-center">
|
|
640
1591
|
<Text>{threadPost[0]?.message ?? ''}</Text>
|
|
641
1592
|
</HStack>
|
|
642
1593
|
</VStack>
|
|
643
1594
|
|
|
644
|
-
<Box py
|
|
645
|
-
<Box
|
|
646
|
-
|
|
647
|
-
borderTopWidth={'$1'}
|
|
648
|
-
borderBottomWidth={'$1'}
|
|
649
|
-
py={'$2'}
|
|
650
|
-
borderColor={'$trueGray200'}
|
|
651
|
-
>
|
|
652
|
-
<Text color={'$trueGray600'} fontWeight={'$bold'}>
|
|
1595
|
+
<Box className="py-4">
|
|
1596
|
+
<Box className="px-4 py-2 border-t border-b border-gray-200">
|
|
1597
|
+
<Text className="font-bold color-gray-600">
|
|
653
1598
|
{threadPost[0]?.replies?.totalCount}{' '}
|
|
654
1599
|
{threadPost[0]?.replies?.totalCount > 0 ? 'replies' : 'reply'}
|
|
655
1600
|
</Text>
|
|
@@ -659,98 +1604,156 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
659
1604
|
)}
|
|
660
1605
|
</>
|
|
661
1606
|
)}
|
|
662
|
-
<
|
|
1607
|
+
<GiftedChatInboxComponent
|
|
663
1608
|
ref={threadMessageListRef}
|
|
664
|
-
|
|
665
|
-
|
|
1609
|
+
onRemoveImage={handleRemoveImage}
|
|
1610
|
+
images={images}
|
|
1611
|
+
onSelectImages={onSelectImages}
|
|
1612
|
+
selectedImage={selectedImage}
|
|
1613
|
+
setSelectedImage={setSelectedImage}
|
|
1614
|
+
isUploadingImage={isUploadingImage}
|
|
1615
|
+
loading={loading}
|
|
1616
|
+
wrapInSafeArea={true}
|
|
1617
|
+
inverted={true}
|
|
1618
|
+
renderLoading={() => <Skeleton variant="rounded" style={{ flex: 1 }} />}
|
|
666
1619
|
messages={messageList}
|
|
667
1620
|
listViewProps={{
|
|
668
1621
|
onEndReached: onEndReached,
|
|
669
1622
|
onEndReachedThreshold: 0.5,
|
|
670
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
|
+
),
|
|
671
1639
|
}}
|
|
672
1640
|
onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
|
|
673
|
-
text={msg
|
|
1641
|
+
text={msg || ' '}
|
|
674
1642
|
onInputTextChanged={(text) => setMsg(text)}
|
|
675
|
-
renderFooter={() =>
|
|
676
|
-
loading ? <Spinner color={'$blue500'} /> : imageLoading ? <Spinner color={'$blue500'} /> : ''
|
|
677
|
-
}
|
|
1643
|
+
renderFooter={() => null}
|
|
678
1644
|
scrollToBottom
|
|
679
1645
|
user={{
|
|
680
1646
|
_id: auth?.id || '',
|
|
681
1647
|
}}
|
|
682
|
-
|
|
683
|
-
alwaysShowSend={loading ? false : true}
|
|
684
|
-
// onLoadEarlier={onFetchOld}
|
|
1648
|
+
placeholder="Jot something down"
|
|
685
1649
|
infiniteScroll={true}
|
|
686
1650
|
renderSend={renderSend}
|
|
687
|
-
// loadEarlier={data?.messages?.totalCount > channelMessages.length}
|
|
688
|
-
// isLoadingEarlier={loadEarlierMsg}
|
|
689
|
-
// renderLoadEarlier={() =>
|
|
690
|
-
// !loadEarlierMsg && (
|
|
691
|
-
// <Center py={2}>
|
|
692
|
-
// <Button
|
|
693
|
-
// onPress={() => onFetchOld(channelMessages?.length)}
|
|
694
|
-
// variant={'outline'}
|
|
695
|
-
// _text={{ color: 'black', fontSize: 15, fontWeight: 'bold' }}
|
|
696
|
-
// >
|
|
697
|
-
// Load earlier messages
|
|
698
|
-
// </Button>
|
|
699
|
-
// </Center>
|
|
700
|
-
// )
|
|
701
|
-
// }
|
|
702
1651
|
renderMessageText={renderMessageText}
|
|
703
|
-
minInputToolbarHeight={50}
|
|
704
|
-
renderActions={renderActions}
|
|
705
|
-
renderAccessory={renderAccessory}
|
|
706
1652
|
renderMessage={renderMessage}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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]}
|
|
745
1708
|
/>
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1709
|
+
),
|
|
1710
|
+
}}
|
|
1711
|
+
onSend={(messages) => handleSend(messages[0]?.text ?? ' ')}
|
|
1712
|
+
text={msg || ' '}
|
|
1713
|
+
onInputTextChanged={(text) => setMsg(text)}
|
|
1714
|
+
renderFooter={() => null}
|
|
1715
|
+
scrollToBottom
|
|
1716
|
+
user={{
|
|
1717
|
+
_id: auth?.id || '',
|
|
1718
|
+
}}
|
|
1719
|
+
isTyping={false}
|
|
1720
|
+
alwaysShowSend={true}
|
|
1721
|
+
infiniteScroll={true}
|
|
1722
|
+
renderSend={renderSend}
|
|
1723
|
+
renderMessageText={renderMessageText}
|
|
1724
|
+
renderInputToolbar={renderInputToolbar}
|
|
1725
|
+
renderComposer={renderComposer}
|
|
1726
|
+
minInputToolbarHeight={isActionSheetVisible ? 0 : 56}
|
|
1727
|
+
renderAccessory={images.length > 0 ? renderAccessory : null}
|
|
1728
|
+
renderMessage={renderMessage}
|
|
1729
|
+
renderLoadEarlier={renderLoadEarlier}
|
|
1730
|
+
loadEarlier={totalCount > channelMessages.length}
|
|
1731
|
+
isLoadingEarlier={loadingOldMessages}
|
|
1732
|
+
bottomOffset={Platform.OS === 'ios' ? (selectedImage ? 90 : 10) : 0}
|
|
1733
|
+
textInputProps={{
|
|
1734
|
+
multiline: true,
|
|
1735
|
+
returnKeyType: 'default',
|
|
1736
|
+
enablesReturnKeyAutomatically: true,
|
|
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),
|
|
1742
|
+
}}
|
|
1743
|
+
minComposerHeight={36}
|
|
1744
|
+
maxComposerHeight={100}
|
|
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
|
+
}}
|
|
749
1752
|
renderChatEmpty={() => (
|
|
750
1753
|
<>
|
|
751
1754
|
{!threadLoading && messageList && messageList?.length == 0 && (
|
|
752
|
-
<Box p
|
|
753
|
-
<Center mt
|
|
1755
|
+
<Box className="p-5">
|
|
1756
|
+
<Center className="mt-6">
|
|
754
1757
|
<Ionicons name="chatbubbles" size={30} />
|
|
755
1758
|
<Text>You don't have any message yet!</Text>
|
|
756
1759
|
</Center>
|
|
@@ -763,8 +1766,36 @@ const ThreadConversationViewComponent = ({ channelId, postParentId, isPostParent
|
|
|
763
1766
|
springConfig: { tension: 90000, friction: 90000 },
|
|
764
1767
|
disabled: true,
|
|
765
1768
|
}}
|
|
766
|
-
/>
|
|
767
|
-
|
|
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>
|
|
768
1799
|
);
|
|
769
1800
|
};
|
|
770
1801
|
|