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