@messenger-box/platform-mobile 10.0.3-alpha.16 → 10.0.3-alpha.162
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 +248 -0
- package/lib/compute.js +2 -3
- package/lib/compute.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/module.js.map +1 -1
- package/lib/queries/inboxQueries.js +65 -0
- package/lib/queries/inboxQueries.js.map +1 -0
- package/lib/routes.json +13 -1
- package/lib/screens/inbox/DialogMessages.js +5 -3
- package/lib/screens/inbox/DialogMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreadMessages.js +3 -7
- package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
- package/lib/screens/inbox/DialogThreads.js +57 -18
- package/lib/screens/inbox/DialogThreads.js.map +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/ImageViewerModal.js.map +1 -1
- package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js +197 -0
- package/lib/screens/inbox/components/SlackMessageContainer/PaymentMessage.js.map +1 -0
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +141 -31
- package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -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 -44
- 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 +1205 -432
- 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 +724 -207
- 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 +5 -5
- package/src/index.ts +2 -0
- 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 +2 -1
- package/src/screens/inbox/DialogThreadMessages.tsx +3 -11
- package/src/screens/inbox/DialogThreads.tsx +53 -60
- 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/DialogsListItem.tsx +230 -170
- 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 +330 -171
- package/src/screens/inbox/components/SlackInput.tsx +23 -0
- package/src/screens/inbox/components/SlackMessageContainer/PaymentMessage.tsx +199 -0
- package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
- 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 +41 -24
- package/src/screens/inbox/components/ThreadsViewItem.tsx +177 -265
- 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 +2004 -698
- package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
- package/src/screens/inbox/containers/Dialogs.tsx +402 -196
- package/src/screens/inbox/containers/SupportServiceDialogs.tsx +2 -2
- package/src/screens/inbox/containers/ThreadConversationView.tsx +1337 -301
- package/src/screens/inbox/containers/ThreadsView.tsx +105 -186
- 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/lib/screens/inbox/components/DialogsListItem.js +0 -175
- package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
- package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -165
- package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
|
@@ -11,25 +11,55 @@ import {
|
|
|
11
11
|
Image,
|
|
12
12
|
Spinner,
|
|
13
13
|
Text,
|
|
14
|
+
Skeleton,
|
|
15
|
+
ScrollView,
|
|
16
|
+
Toast,
|
|
17
|
+
ToastTitle,
|
|
18
|
+
ToastDescription,
|
|
19
|
+
useToast,
|
|
20
|
+
ToastAlert,
|
|
21
|
+
VStack,
|
|
22
|
+
Divider,
|
|
23
|
+
Center,
|
|
14
24
|
} from '@admin-layout/gluestack-ui-mobile';
|
|
15
|
-
import {
|
|
16
|
-
|
|
25
|
+
import {
|
|
26
|
+
Platform,
|
|
27
|
+
TouchableHighlight,
|
|
28
|
+
SafeAreaView,
|
|
29
|
+
View,
|
|
30
|
+
TouchableOpacity,
|
|
31
|
+
Animated,
|
|
32
|
+
Text as RNText,
|
|
33
|
+
TextInput,
|
|
34
|
+
KeyboardAvoidingView,
|
|
35
|
+
} from 'react-native';
|
|
36
|
+
import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
|
|
17
37
|
import { navigationRef } from '@common-stack/client-react';
|
|
18
|
-
import { useSelector } from 'react-redux';
|
|
38
|
+
import { useSelector, shallowEqual } from 'react-redux';
|
|
19
39
|
import { orderBy, startCase, uniqBy } from 'lodash-es';
|
|
20
40
|
import * as ImagePicker from 'expo-image-picker';
|
|
21
41
|
import { encode as atob } from 'base-64';
|
|
22
|
-
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
|
23
|
-
import {
|
|
24
|
-
|
|
42
|
+
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
|
|
43
|
+
import {
|
|
44
|
+
Actions,
|
|
45
|
+
GiftedChat,
|
|
46
|
+
IMessage,
|
|
47
|
+
MessageText,
|
|
48
|
+
Send,
|
|
49
|
+
Composer,
|
|
50
|
+
InputToolbar,
|
|
51
|
+
Message as GiftedMessage,
|
|
52
|
+
} from 'react-native-gifted-chat';
|
|
53
|
+
import { PreDefinedRole, RoomType, IExpoNotificationData, IFileInfo, FileRefType, PostTypeEnum } from 'common';
|
|
25
54
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
CHAT_MESSAGE_ADDED,
|
|
56
|
+
useChannelDetailQuery,
|
|
57
|
+
useChannelMessagesQuery,
|
|
58
|
+
useSendChannelMessage,
|
|
59
|
+
useAddDirectChannel,
|
|
60
|
+
MESSAGES_DOCUMENT,
|
|
61
|
+
useSendExpoNotification,
|
|
62
|
+
} from '../../../queries/inboxQueries';
|
|
33
63
|
import { useUploadFilesNative } from '@messenger-box/platform-client';
|
|
34
64
|
import { objectId } from '@messenger-box/core';
|
|
35
65
|
import { userSelector } from '@adminide-stack/user-auth0-client';
|
|
@@ -37,6 +67,23 @@ import { format, isToday, isYesterday } from 'date-fns';
|
|
|
37
67
|
import { ImageViewerModal, SlackMessage } from '../components/SlackMessageContainer';
|
|
38
68
|
import CachedImage from '../components/CachedImage';
|
|
39
69
|
import { config } from '../config';
|
|
70
|
+
import colors from 'tailwindcss/colors';
|
|
71
|
+
import ExpandableInputActionSheet from '../components/ExpandableInputActionSheet';
|
|
72
|
+
import ExpandableInput from '../components/ExpandableInput';
|
|
73
|
+
import { SubscriptionHandler } from '../components/SubscriptionHandler';
|
|
74
|
+
import { useInboxMessages } from '../hooks/useInboxMessages';
|
|
75
|
+
import Reanimated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
|
|
76
|
+
import Constants from 'expo-constants';
|
|
77
|
+
import { Keyboard } from 'react-native';
|
|
78
|
+
import GiftedChatInboxComponent from '../components/GiftedChatInboxComponent';
|
|
79
|
+
import PaymentMessage from '../components/SlackMessageContainer/PaymentMessage';
|
|
80
|
+
|
|
81
|
+
// Define an extended interface for ImagePickerAsset with url property
|
|
82
|
+
interface ExtendedImagePickerAsset extends ImagePicker.ImagePickerAsset {
|
|
83
|
+
url?: string;
|
|
84
|
+
fileName?: string;
|
|
85
|
+
mimeType?: string;
|
|
86
|
+
}
|
|
40
87
|
|
|
41
88
|
const {
|
|
42
89
|
MESSAGES_PER_PAGE,
|
|
@@ -64,6 +111,7 @@ interface IMessageProps extends IMessage {
|
|
|
64
111
|
propsConfiguration?: any;
|
|
65
112
|
replies?: any;
|
|
66
113
|
isShowThreadMessage?: boolean;
|
|
114
|
+
images?: string[]; // Add support for multiple images
|
|
67
115
|
}
|
|
68
116
|
|
|
69
117
|
export interface AlertMessageAttachmentsInterface {
|
|
@@ -76,257 +124,524 @@ export interface AlertMessageAttachmentsInterface {
|
|
|
76
124
|
};
|
|
77
125
|
}
|
|
78
126
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
127
|
+
// Fix for the optimistic response types
|
|
128
|
+
type OptimisticPropsConfig = {
|
|
129
|
+
__typename: 'MachineConfiguration';
|
|
130
|
+
resource: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Custom notification component
|
|
134
|
+
const ErrorNotification = ({ message, onClose, type = 'error' }) => {
|
|
135
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
136
|
+
|
|
137
|
+
// Choose colors based on type
|
|
138
|
+
const bgColor = type === 'error' ? '#f44336' : '#ff9800';
|
|
139
|
+
const title = type === 'error' ? 'Error' : 'Warning';
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
// Fade in
|
|
143
|
+
Animated.timing(opacity, {
|
|
144
|
+
toValue: 1,
|
|
145
|
+
duration: 300,
|
|
146
|
+
useNativeDriver: true,
|
|
147
|
+
}).start();
|
|
148
|
+
|
|
149
|
+
// Auto hide after 4 seconds
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
Animated.timing(opacity, {
|
|
152
|
+
toValue: 0,
|
|
153
|
+
duration: 300,
|
|
154
|
+
useNativeDriver: true,
|
|
155
|
+
}).start(() => onClose && onClose());
|
|
156
|
+
}, 4000);
|
|
157
|
+
|
|
158
|
+
return () => clearTimeout(timer);
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Animated.View
|
|
163
|
+
style={{
|
|
164
|
+
position: 'absolute',
|
|
165
|
+
top: 10,
|
|
166
|
+
left: 10,
|
|
167
|
+
right: 10,
|
|
168
|
+
backgroundColor: bgColor,
|
|
169
|
+
padding: 15,
|
|
170
|
+
borderRadius: 8,
|
|
171
|
+
shadowColor: '#000',
|
|
172
|
+
shadowOffset: { width: 0, height: 2 },
|
|
173
|
+
shadowOpacity: 0.25,
|
|
174
|
+
shadowRadius: 3.84,
|
|
175
|
+
elevation: 5,
|
|
176
|
+
zIndex: 1000,
|
|
177
|
+
opacity,
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
<HStack className="items-center justify-between">
|
|
181
|
+
<Text style={{ color: 'white', fontWeight: 'bold' }}>{title}</Text>
|
|
182
|
+
<TouchableOpacity onPress={onClose}>
|
|
183
|
+
<Ionicons name="close" size={20} color="white" />
|
|
184
|
+
</TouchableOpacity>
|
|
185
|
+
</HStack>
|
|
186
|
+
<Text style={{ color: 'white', marginTop: 5 }}>{message}</Text>
|
|
187
|
+
</Animated.View>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const PADDING_BOTTOM = Platform.OS === 'ios' ? 20 : 0;
|
|
192
|
+
|
|
193
|
+
function useGradualKeyboardAnimation() {
|
|
194
|
+
const height = useSharedValue(PADDING_BOTTOM);
|
|
195
|
+
const isExpoGo = Constants.executionEnvironment === 'storeClient';
|
|
196
|
+
let useKeyboardHandler;
|
|
197
|
+
if (!isExpoGo) {
|
|
198
|
+
useKeyboardHandler = require('react-native-keyboard-controller').useKeyboardHandler;
|
|
199
|
+
}
|
|
200
|
+
if (useKeyboardHandler) {
|
|
201
|
+
useKeyboardHandler(
|
|
202
|
+
{
|
|
203
|
+
onMove: (e) => {
|
|
204
|
+
'worklet';
|
|
205
|
+
height.value = Math.max(e.height, PADDING_BOTTOM);
|
|
206
|
+
},
|
|
207
|
+
onEnd: (e) => {
|
|
208
|
+
'worklet';
|
|
209
|
+
height.value = e.height;
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
[],
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return { height };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ConversationViewComponent = ({
|
|
219
|
+
channelId: initialChannelId,
|
|
220
|
+
role,
|
|
221
|
+
isShowThreadMessage,
|
|
222
|
+
isPaymentChatUI,
|
|
223
|
+
...rest
|
|
224
|
+
}: any) => {
|
|
225
|
+
// Core state management using React hooks instead of XState
|
|
226
|
+
const { params } = useRoute<any>();
|
|
227
|
+
const [channelId, setChannelId] = useState<string | null>(initialChannelId || null);
|
|
228
|
+
const [messageText, setMessageText] = useState('');
|
|
92
229
|
const [loading, setLoading] = useState(false);
|
|
93
|
-
const [
|
|
94
|
-
const [
|
|
95
|
-
const [
|
|
230
|
+
const [loadingOldMessages, setLoadingOldMessages] = useState(false);
|
|
231
|
+
const [error, setError] = useState<string | null>(null);
|
|
232
|
+
const [selectedImage, setSelectedImage] = useState<string>('');
|
|
233
|
+
const [images, setImages] = useState<any[]>([]);
|
|
96
234
|
const [isShowImageViewer, setImageViewer] = useState<boolean>(false);
|
|
97
235
|
const [imageObject, setImageObject] = useState<any>({});
|
|
98
|
-
const [
|
|
236
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
237
|
+
const [notificationType, setNotificationType] = useState('error');
|
|
238
|
+
|
|
239
|
+
// Add state for expandable action sheet
|
|
240
|
+
const [isActionSheetVisible, setActionSheetVisible] = useState(false);
|
|
241
|
+
// Add state for controlling bottom margin
|
|
242
|
+
const [bottomMargin, setBottomMargin] = useState(0);
|
|
243
|
+
|
|
244
|
+
// Create refs for various operations
|
|
99
245
|
const messageRootListRef = useRef<any>(null);
|
|
100
|
-
const
|
|
246
|
+
const textInputRef = useRef<any>(null); // Add new ref for the text input
|
|
247
|
+
const isMounted = useRef(true);
|
|
248
|
+
const fetchOldDebounceRef = useRef(false);
|
|
101
249
|
|
|
102
|
-
|
|
250
|
+
// Navigation and auth
|
|
251
|
+
const auth: any = useSelector(userSelector, shallowEqual);
|
|
252
|
+
const currentRoute = navigationRef.isReady() ? navigationRef?.getCurrentRoute() : null;
|
|
253
|
+
const navigation = useNavigation<any>();
|
|
254
|
+
const isFocused = useIsFocused();
|
|
103
255
|
|
|
256
|
+
// Apollo mutations
|
|
257
|
+
const [addDirectChannel] = useAddDirectChannel();
|
|
104
258
|
const { startUpload } = useUploadFilesNative();
|
|
259
|
+
const [sendMsg] = useSendChannelMessage();
|
|
260
|
+
const [sendExpoNotification] = useSendExpoNotification();
|
|
105
261
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const [sendExpoNotificationOnPostMutation] = useSendExpoNotificationOnPostMutation();
|
|
262
|
+
// Add skip state for pagination
|
|
263
|
+
const [skip, setSkip] = useState(0);
|
|
109
264
|
|
|
265
|
+
// Apollo query for messages
|
|
110
266
|
const {
|
|
111
267
|
data,
|
|
112
268
|
loading: messageLoading,
|
|
269
|
+
error: inboxError,
|
|
113
270
|
refetch,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
271
|
+
subscribe,
|
|
272
|
+
} = useInboxMessages({
|
|
273
|
+
useQueryHook: useChannelMessagesQuery,
|
|
274
|
+
queryVariables: {
|
|
118
275
|
channelId: channelId?.toString(),
|
|
119
276
|
parentId: null,
|
|
120
277
|
limit: MESSAGES_PER_PAGE,
|
|
121
|
-
skip: skip,
|
|
278
|
+
skip: skip, // Use skip state for pagination
|
|
279
|
+
orgName: params?.orgName,
|
|
122
280
|
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
281
|
+
subscriptionDocument: CHAT_MESSAGE_ADDED,
|
|
282
|
+
subscriptionVariables: { channelId: channelId?.toString() },
|
|
283
|
+
updateQuery: undefined, // Provide custom updateQuery if needed
|
|
284
|
+
onError: (err) => setError(String(err)),
|
|
127
285
|
});
|
|
128
286
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
287
|
+
// Extract messages from the query data
|
|
288
|
+
const channelMessages = useMemo(() => {
|
|
289
|
+
return (data?.messages?.data as any[]) || [];
|
|
290
|
+
}, [data?.messages?.data]);
|
|
291
|
+
|
|
292
|
+
// Get total message count
|
|
293
|
+
const totalCount = useMemo(() => {
|
|
294
|
+
return data?.messages?.totalCount || 0;
|
|
295
|
+
}, [data?.messages?.totalCount]);
|
|
296
|
+
|
|
297
|
+
// Clear messages when component unmounts
|
|
298
|
+
useEffect(() => {
|
|
141
299
|
return () => {
|
|
142
|
-
|
|
300
|
+
isMounted.current = false;
|
|
143
301
|
};
|
|
144
302
|
}, []);
|
|
145
303
|
|
|
304
|
+
// Update channelId from props or navigation params
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const currentChannelId = initialChannelId || currentRoute?.params?.channelId;
|
|
307
|
+
if (currentChannelId) {
|
|
308
|
+
setChannelId(currentChannelId);
|
|
309
|
+
}
|
|
310
|
+
}, [initialChannelId, currentRoute]);
|
|
311
|
+
|
|
312
|
+
// Focus/unfocus behavior
|
|
146
313
|
useFocusEffect(
|
|
147
314
|
React.useCallback(() => {
|
|
148
|
-
// Do something when the screen is focused
|
|
149
|
-
setSkip(0);
|
|
150
|
-
// refetchChannelDetail({ id: channelId?.toString() });
|
|
151
315
|
if (channelId) {
|
|
152
|
-
refetch(
|
|
153
|
-
channelId: channelId?.toString(),
|
|
154
|
-
parentId: null,
|
|
155
|
-
limit: MESSAGES_PER_PAGE,
|
|
156
|
-
skip: 0,
|
|
157
|
-
}).then(({ data }) => {
|
|
158
|
-
if (!data?.messages) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const { data: messages, totalCount }: any = data.messages;
|
|
162
|
-
setTotalCount(totalCount);
|
|
163
|
-
setChannelMessages(messages);
|
|
164
|
-
});
|
|
316
|
+
refetch();
|
|
165
317
|
}
|
|
166
|
-
|
|
167
|
-
// Do something when the screen is unfocused
|
|
168
|
-
// Useful for cleanup functions
|
|
169
|
-
setChannelId(null);
|
|
170
|
-
setTotalCount(0);
|
|
171
|
-
setSkip(0);
|
|
172
|
-
};
|
|
173
|
-
}, [channelId, isFocused]),
|
|
318
|
+
}, [isFocused, refetch]),
|
|
174
319
|
);
|
|
175
320
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
321
|
+
// When fetching more messages, update skip
|
|
322
|
+
const fetchMoreMessagesImpl = useCallback(async () => {
|
|
323
|
+
try {
|
|
324
|
+
setLoadingOldMessages(true);
|
|
325
|
+
const response = await refetch({
|
|
326
|
+
channelId: channelId?.toString(),
|
|
327
|
+
parentId: null,
|
|
328
|
+
limit: MESSAGES_PER_PAGE,
|
|
329
|
+
skip: channelMessages.length,
|
|
330
|
+
});
|
|
331
|
+
setSkip(channelMessages.length); // Update skip after fetching
|
|
332
|
+
setLoadingOldMessages(false);
|
|
333
|
+
if (!response?.data?.messages?.data) {
|
|
334
|
+
return { error: 'No messages returned' };
|
|
335
|
+
}
|
|
336
|
+
return { messages: response.data.messages.data };
|
|
337
|
+
} catch (error) {
|
|
338
|
+
setLoadingOldMessages(false);
|
|
339
|
+
setError(String(error));
|
|
340
|
+
return { error: String(error) };
|
|
341
|
+
}
|
|
342
|
+
}, [channelId, channelMessages.length, refetch]);
|
|
180
343
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
344
|
+
// Send message function
|
|
345
|
+
const sendMessageImpl = useCallback(async () => {
|
|
346
|
+
try {
|
|
347
|
+
// Store the current message text and clear input immediately for better UX
|
|
348
|
+
const currentMessageText = messageText;
|
|
349
|
+
setMessageText('');
|
|
184
350
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
351
|
+
const notificationData: IExpoNotificationData = {
|
|
352
|
+
url: config.INBOX_MESSEGE_PATH,
|
|
353
|
+
params: { channelId, hideTabBar: true },
|
|
354
|
+
screen: 'DialogMessages',
|
|
355
|
+
other: { sound: Platform.OS === 'android' ? undefined : 'default' },
|
|
356
|
+
};
|
|
189
357
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
358
|
+
// Create optimistic message with minimal structure required for UI rendering
|
|
359
|
+
const messageId = objectId();
|
|
360
|
+
const optimisticMessage = {
|
|
361
|
+
__typename: 'Post' as const,
|
|
362
|
+
id: messageId,
|
|
363
|
+
message: currentMessageText,
|
|
364
|
+
createdAt: new Date().toISOString(),
|
|
365
|
+
updatedAt: new Date().toISOString(),
|
|
366
|
+
author: {
|
|
367
|
+
__typename: 'UserAccount' as const,
|
|
368
|
+
id: auth?.id,
|
|
369
|
+
givenName: auth?.profile?.given_name || '',
|
|
370
|
+
familyName: auth?.profile?.family_name || '',
|
|
371
|
+
email: auth?.profile?.email || '',
|
|
372
|
+
username: auth?.profile?.nickname || '',
|
|
373
|
+
fullName: auth?.profile?.name || '',
|
|
374
|
+
picture: auth?.profile?.picture || '',
|
|
375
|
+
alias: [auth?.authUserId ?? ''] as string[],
|
|
376
|
+
tokens: [],
|
|
377
|
+
},
|
|
378
|
+
isDelivered: true,
|
|
379
|
+
isRead: false,
|
|
380
|
+
type: PostTypeEnum.Simple,
|
|
381
|
+
parentId: null,
|
|
382
|
+
fromServer: false,
|
|
383
|
+
channel: {
|
|
384
|
+
__typename: 'Channel' as const,
|
|
385
|
+
id: channelId,
|
|
386
|
+
},
|
|
387
|
+
// Required fields that Apollo expects in the cache
|
|
388
|
+
propsConfiguration: {
|
|
389
|
+
__typename: 'MachineConfiguration' as const,
|
|
390
|
+
id: null,
|
|
391
|
+
resource: '' as any,
|
|
392
|
+
contents: null,
|
|
393
|
+
keys: null,
|
|
394
|
+
target: null,
|
|
395
|
+
overrides: null,
|
|
396
|
+
},
|
|
397
|
+
props: {},
|
|
398
|
+
files: {
|
|
399
|
+
__typename: 'FilesInfo' as const,
|
|
400
|
+
data: [],
|
|
401
|
+
totalCount: 0,
|
|
402
|
+
},
|
|
403
|
+
replies: {
|
|
404
|
+
__typename: 'Messages' as const,
|
|
405
|
+
data: [],
|
|
406
|
+
totalCount: 0,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const response = await sendMsg({
|
|
411
|
+
variables: {
|
|
412
|
+
channelId,
|
|
413
|
+
content: currentMessageText,
|
|
414
|
+
notificationParams: notificationData,
|
|
415
|
+
},
|
|
416
|
+
optimisticResponse: {
|
|
417
|
+
__typename: 'Mutation',
|
|
418
|
+
sendMessage: optimisticMessage,
|
|
419
|
+
},
|
|
420
|
+
// Let the type policies handle the cache update automatically
|
|
421
|
+
update: (cache, { data }) => {
|
|
422
|
+
// Only perform cache update if we have valid data
|
|
423
|
+
if (!data?.sendMessage) return;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
// Let Apollo type policies handle merging by using writeQuery
|
|
427
|
+
// This will trigger the merge functions in the type policies
|
|
428
|
+
cache.writeQuery({
|
|
429
|
+
query: MESSAGES_DOCUMENT,
|
|
430
|
+
variables: {
|
|
431
|
+
channelId: channelId?.toString(),
|
|
432
|
+
parentId: null,
|
|
433
|
+
limit: MESSAGES_PER_PAGE,
|
|
434
|
+
skip: 0,
|
|
435
|
+
},
|
|
436
|
+
data: {
|
|
437
|
+
messages: {
|
|
438
|
+
__typename: 'Messages',
|
|
439
|
+
messagesRefId: channelId,
|
|
440
|
+
data: [data.sendMessage],
|
|
441
|
+
totalCount: 1, // Just send the count for this single message
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error('Error updating cache:', error);
|
|
447
|
+
|
|
448
|
+
// Format error for notification
|
|
449
|
+
let errorMsg = 'Failed to update message cache';
|
|
450
|
+
if (__DEV__ && error) {
|
|
451
|
+
// In development, show actual error
|
|
452
|
+
errorMsg = error.message
|
|
453
|
+
? error.message.replace('[ApolloError: ', '').replace(']', '')
|
|
454
|
+
: 'Cache update failed';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
setNotificationType('error');
|
|
458
|
+
setErrorMessage(errorMsg);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Ensure loader is removed after sending
|
|
464
|
+
setIsUploadingImage(false);
|
|
465
|
+
setLoading(false);
|
|
466
|
+
|
|
467
|
+
return { message: response.data?.sendMessage };
|
|
468
|
+
} catch (error) {
|
|
469
|
+
setLoading(false);
|
|
470
|
+
setIsUploadingImage(false);
|
|
471
|
+
|
|
472
|
+
// Format error for notification
|
|
473
|
+
let errorMsg = 'Failed to send message';
|
|
474
|
+
if (__DEV__ && error) {
|
|
475
|
+
// In development, show actual error
|
|
476
|
+
errorMsg = error.message
|
|
477
|
+
? error.message.replace('[ApolloError: ', '').replace(']', '')
|
|
478
|
+
: 'Message sending failed';
|
|
196
479
|
}
|
|
197
480
|
|
|
198
|
-
|
|
481
|
+
setNotificationType('error');
|
|
482
|
+
setErrorMessage(errorMsg);
|
|
483
|
+
setError(String(error));
|
|
484
|
+
return { error: String(error) };
|
|
199
485
|
}
|
|
200
|
-
}, [
|
|
486
|
+
}, [channelId, messageText, sendMsg, auth]);
|
|
201
487
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
488
|
+
// Image selection handler
|
|
489
|
+
const onSelectImages = async () => {
|
|
490
|
+
setLoading(true);
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
let imageSource = await ImagePicker.launchImageLibraryAsync({
|
|
494
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
495
|
+
allowsEditing: false,
|
|
496
|
+
aspect: [4, 3],
|
|
497
|
+
quality: 0.8,
|
|
498
|
+
base64: true,
|
|
499
|
+
exif: false,
|
|
500
|
+
allowsMultipleSelection: true, // Enable multiple selection
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
if (!imageSource?.canceled) {
|
|
504
|
+
// Get all selected assets
|
|
505
|
+
const selectedAssets = imageSource?.assets || [];
|
|
506
|
+
if (selectedAssets.length === 0) {
|
|
507
|
+
setLoading(false);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Process all selected images
|
|
512
|
+
const newImages = selectedAssets.map((selectedAsset) => {
|
|
513
|
+
// Create a base64 image string for preview
|
|
514
|
+
const base64Data = selectedAsset.base64;
|
|
515
|
+
const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : selectedAsset.uri;
|
|
516
|
+
|
|
517
|
+
// Format the asset for upload service requirements
|
|
518
|
+
const asset: ExtendedImagePickerAsset = {
|
|
519
|
+
...selectedAsset,
|
|
520
|
+
url: selectedAsset.uri,
|
|
521
|
+
fileName: selectedAsset.fileName || `image_${Date.now()}.jpg`,
|
|
522
|
+
mimeType: 'image/jpeg',
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
return asset;
|
|
212
526
|
});
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
527
|
+
|
|
528
|
+
// Set preview for the first image (for backward compatibility)
|
|
529
|
+
if (newImages.length > 0) {
|
|
530
|
+
const base64Data = newImages[0].base64;
|
|
531
|
+
const previewImage = base64Data ? `data:image/jpeg;base64,${base64Data}` : newImages[0].uri;
|
|
532
|
+
setSelectedImage(previewImage);
|
|
217
533
|
}
|
|
218
|
-
} catch (error: any) {
|
|
219
|
-
setLoadEarlierMsg(false);
|
|
220
|
-
}
|
|
221
534
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
535
|
+
// Add new images to existing ones
|
|
536
|
+
setImages((currentImages) => [...currentImages, ...newImages]);
|
|
537
|
+
|
|
538
|
+
// Show action sheet if it's not visible
|
|
539
|
+
if (!isActionSheetVisible) {
|
|
540
|
+
setActionSheetVisible(true);
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
setLoading(false);
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
setLoading(false);
|
|
225
547
|
}
|
|
226
|
-
}
|
|
548
|
+
};
|
|
227
549
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
//
|
|
550
|
+
// Add a state variable to track which message should show the skeleton
|
|
551
|
+
const [uploadingMessageId, setUploadingMessageId] = useState<string | null>(null);
|
|
552
|
+
// Add new state for tracking pending uploads
|
|
553
|
+
const [pendingUploads, setPendingUploads] = useState<Record<string, IMessageProps>>({});
|
|
554
|
+
const [uploadErrors, setUploadErrors] = useState<Record<string, string>>({});
|
|
231
555
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
|
|
235
|
-
};
|
|
556
|
+
// Add new state variable to track image upload status
|
|
557
|
+
const [isUploadingImage, setIsUploadingImage] = useState(false);
|
|
236
558
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
n = bstr.length,
|
|
242
|
-
u8arr = new Uint8Array(n);
|
|
243
|
-
while (n--) {
|
|
244
|
-
u8arr[n] = bstr.charCodeAt(n);
|
|
559
|
+
// Ensure loader is hidden when all images are removed
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
if (images.length === 0) {
|
|
562
|
+
setIsUploadingImage(false);
|
|
245
563
|
}
|
|
246
|
-
|
|
247
|
-
};
|
|
564
|
+
}, [images]);
|
|
248
565
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
566
|
+
// Add toast hook for notifications
|
|
567
|
+
const toast = useToast();
|
|
568
|
+
|
|
569
|
+
// Add a helper function for removing messages from the UI when uploads fail
|
|
570
|
+
const removeMessageFromUI = useCallback((messageId: string) => {
|
|
571
|
+
// Remove from pending uploads
|
|
572
|
+
setPendingUploads((prev) => {
|
|
573
|
+
const newPending = { ...prev };
|
|
574
|
+
delete newPending[messageId];
|
|
575
|
+
return newPending;
|
|
257
576
|
});
|
|
258
|
-
// if (!imageSource.cancelled) {
|
|
259
|
-
// const image = 'data:image/jpeg;base64,' + imageSource?.base64;
|
|
260
|
-
// setImage(image);
|
|
261
|
-
// const file = dataURLtoFile(image, 'inputImage.jpg');
|
|
262
|
-
// setFiles((files) => files.concat(file));
|
|
263
|
-
// setImages((images) => images.concat(imageSource as ImagePicker.ImageInfo));
|
|
264
|
-
// }
|
|
265
|
-
// if (imageSource.cancelled) setLoading(false);
|
|
266
|
-
|
|
267
|
-
if (!imageSource.canceled) {
|
|
268
|
-
const image = 'data:image/jpeg;base64,' + imageSource?.assets[0]?.base64;
|
|
269
|
-
setImage(image);
|
|
270
|
-
const file = dataURLtoFile(image, 'inputImage.jpg');
|
|
271
|
-
setFiles((files) => files.concat(file));
|
|
272
|
-
setImages((images) => images.concat(imageSource?.assets[0] as ImagePicker.ImagePickerAsset));
|
|
273
|
-
}
|
|
274
577
|
|
|
275
|
-
|
|
276
|
-
|
|
578
|
+
// Also remove any error state
|
|
579
|
+
setUploadErrors((prev) => {
|
|
580
|
+
const newErrors = { ...prev };
|
|
581
|
+
delete newErrors[messageId];
|
|
582
|
+
return newErrors;
|
|
583
|
+
});
|
|
277
584
|
|
|
278
|
-
|
|
279
|
-
(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
setLoading(true);
|
|
301
|
-
await sendMsg({
|
|
302
|
-
variables: {
|
|
303
|
-
channelId: res?.data?.createDirectChannel?.id,
|
|
304
|
-
content: msg,
|
|
305
|
-
notificationParams: notificationData,
|
|
306
|
-
},
|
|
307
|
-
update: (cache, { data, errors }: any) => {
|
|
308
|
-
if (!data || errors) {
|
|
309
|
-
setLoading(false);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
setChannelToTop(channelToTop + 1);
|
|
313
|
-
setLoading(false);
|
|
314
|
-
setMsg('');
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
})
|
|
319
|
-
?.catch((e: any) => console.log('error', JSON.stringify(e)));
|
|
585
|
+
// Reset upload state to ensure we don't get stuck with loading indicator
|
|
586
|
+
setIsUploadingImage(false);
|
|
587
|
+
}, []);
|
|
588
|
+
|
|
589
|
+
// Send message with file - fix to ensure images display without loading indicators
|
|
590
|
+
const sendMessageWithFileImpl = useCallback(async () => {
|
|
591
|
+
try {
|
|
592
|
+
// Generate a unique ID for the message
|
|
593
|
+
const postId = objectId();
|
|
594
|
+
|
|
595
|
+
// Set uploading state to true
|
|
596
|
+
setIsUploadingImage(true);
|
|
597
|
+
|
|
598
|
+
// Clear all loading states immediately
|
|
599
|
+
setLoading(false);
|
|
600
|
+
setUploadingMessageId(null);
|
|
601
|
+
|
|
602
|
+
// Safety check for images
|
|
603
|
+
if (!images || images.length === 0) {
|
|
604
|
+
setIsUploadingImage(false);
|
|
605
|
+
setLoading(false);
|
|
606
|
+
return { error: 'No images available to upload' };
|
|
320
607
|
}
|
|
321
|
-
},
|
|
322
|
-
[rest],
|
|
323
|
-
);
|
|
324
608
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
609
|
+
// Store current values before clearing
|
|
610
|
+
const currentMessageText = messageText;
|
|
611
|
+
const currentImages = [...images];
|
|
612
|
+
|
|
613
|
+
// Prepare image URIs for optimistic UI update
|
|
614
|
+
const imageUris = currentImages.map((img) => img.uri || img.url);
|
|
615
|
+
|
|
616
|
+
// Clear UI immediately for next message
|
|
617
|
+
setMessageText('');
|
|
618
|
+
setSelectedImage('');
|
|
619
|
+
setImages([]);
|
|
620
|
+
|
|
621
|
+
// Create a client message with all local image URIs
|
|
622
|
+
const clientMessage: IMessageProps = {
|
|
623
|
+
_id: postId,
|
|
624
|
+
text: currentMessageText || ' ',
|
|
625
|
+
createdAt: new Date(),
|
|
626
|
+
user: {
|
|
627
|
+
_id: auth?.id || '',
|
|
628
|
+
name: `${auth?.givenName || ''} ${auth?.familyName || ''}`,
|
|
629
|
+
avatar: auth?.picture || '',
|
|
630
|
+
},
|
|
631
|
+
image: imageUris[0], // First image for compatibility with GiftedChat
|
|
632
|
+
images: imageUris, // All images for our custom renderer
|
|
633
|
+
sent: true,
|
|
634
|
+
received: true,
|
|
635
|
+
pending: false,
|
|
636
|
+
type: 'TEXT',
|
|
637
|
+
replies: { data: [], totalCount: 0 },
|
|
638
|
+
isShowThreadMessage: false,
|
|
639
|
+
};
|
|
329
640
|
|
|
641
|
+
// Add to displayed messages immediately
|
|
642
|
+
setPendingUploads((prev) => ({ ...prev, [postId]: clientMessage }));
|
|
643
|
+
|
|
644
|
+
// Prepare notification data
|
|
330
645
|
const notificationData: IExpoNotificationData = {
|
|
331
646
|
url: config.INBOX_MESSEGE_PATH,
|
|
332
647
|
params: { channelId, hideTabBar: true },
|
|
@@ -334,366 +649,1017 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
|
|
|
334
649
|
other: { sound: Platform.OS === 'android' ? undefined : 'default' },
|
|
335
650
|
};
|
|
336
651
|
|
|
337
|
-
|
|
338
|
-
|
|
652
|
+
// Create optimistic message with minimal structure required for UI rendering
|
|
653
|
+
const optimisticMessage = {
|
|
654
|
+
__typename: 'Post' as const,
|
|
655
|
+
id: postId,
|
|
656
|
+
message: currentMessageText || ' ',
|
|
657
|
+
createdAt: new Date().toISOString(),
|
|
658
|
+
updatedAt: new Date().toISOString(),
|
|
659
|
+
author: {
|
|
660
|
+
__typename: 'UserAccount' as const,
|
|
661
|
+
id: auth?.id,
|
|
662
|
+
givenName: auth?.profile?.given_name || '',
|
|
663
|
+
familyName: auth?.profile?.family_name || '',
|
|
664
|
+
email: auth?.profile?.email || '',
|
|
665
|
+
username: auth?.profile?.nickname || '',
|
|
666
|
+
fullName: auth?.profile?.name || '',
|
|
667
|
+
picture: auth?.profile?.picture || '',
|
|
668
|
+
alias: [auth?.authUserId ?? ''] as string[],
|
|
669
|
+
tokens: [],
|
|
670
|
+
},
|
|
671
|
+
isDelivered: true,
|
|
672
|
+
isRead: false,
|
|
673
|
+
type: PostTypeEnum.Simple,
|
|
674
|
+
parentId: null,
|
|
675
|
+
fromServer: false,
|
|
676
|
+
channel: {
|
|
677
|
+
__typename: 'Channel' as const,
|
|
678
|
+
id: channelId,
|
|
679
|
+
},
|
|
680
|
+
// Required fields that Apollo expects in the cache
|
|
681
|
+
propsConfiguration: {
|
|
682
|
+
__typename: 'MachineConfiguration' as const,
|
|
683
|
+
id: null,
|
|
684
|
+
resource: '' as any,
|
|
685
|
+
contents: null,
|
|
686
|
+
keys: null,
|
|
687
|
+
target: null,
|
|
688
|
+
overrides: null,
|
|
689
|
+
},
|
|
690
|
+
props: {},
|
|
691
|
+
files: {
|
|
692
|
+
__typename: 'FilesInfo' as const,
|
|
693
|
+
data: imageUris.map((uri, index) => ({
|
|
694
|
+
__typename: 'FileInfo' as const,
|
|
695
|
+
id: `temp-file-${index}-${postId}`,
|
|
696
|
+
url: uri,
|
|
697
|
+
name: `image-${index}.jpg`,
|
|
698
|
+
extension: 'jpg',
|
|
699
|
+
mimeType: 'image/jpeg',
|
|
700
|
+
size: 0,
|
|
701
|
+
height: 300,
|
|
702
|
+
width: 300,
|
|
703
|
+
channel: null,
|
|
704
|
+
post: null,
|
|
705
|
+
refType: FileRefType.Post,
|
|
706
|
+
})),
|
|
707
|
+
totalCount: imageUris.length,
|
|
708
|
+
},
|
|
709
|
+
replies: {
|
|
710
|
+
__typename: 'Messages' as const,
|
|
711
|
+
data: [],
|
|
712
|
+
totalCount: 0,
|
|
713
|
+
},
|
|
714
|
+
};
|
|
339
715
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (uploadResponse.data) {
|
|
358
|
-
setImage('');
|
|
359
|
-
setFiles([]);
|
|
360
|
-
setImages([]);
|
|
361
|
-
//setLoading(false);
|
|
362
|
-
const files = uploadedFiles?.map((f: any) => f.id) ?? null;
|
|
363
|
-
await sendMsg({
|
|
364
|
-
variables: {
|
|
365
|
-
postId,
|
|
366
|
-
channelId,
|
|
367
|
-
content: message,
|
|
368
|
-
files,
|
|
369
|
-
notificationParams: notificationData,
|
|
716
|
+
// Start background processing without affecting UI
|
|
717
|
+
setTimeout(async () => {
|
|
718
|
+
try {
|
|
719
|
+
// Format images for upload
|
|
720
|
+
const imagesToUpload = currentImages.map((img) => ({
|
|
721
|
+
...img,
|
|
722
|
+
uri: img.uri || img.url,
|
|
723
|
+
type: img.mimeType || 'image/jpeg',
|
|
724
|
+
name: img.fileName || `image_${Date.now()}.jpg`,
|
|
725
|
+
}));
|
|
726
|
+
|
|
727
|
+
// Upload the files in background - pass the array of images
|
|
728
|
+
const uploadResponse = await startUpload({
|
|
729
|
+
file: imagesToUpload,
|
|
730
|
+
saveUploadedFile: {
|
|
731
|
+
variables: { postId },
|
|
370
732
|
},
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
setLoading(false);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
//Temporary fix.....//
|
|
377
|
-
const newMessage: any = data?.sendMessage;
|
|
378
|
-
setChannelMessages((oldMessages: any) =>
|
|
379
|
-
uniqBy([...oldMessages, newMessage], ({ id }) => id),
|
|
380
|
-
);
|
|
381
|
-
setTotalCount((t) => t + 1);
|
|
382
|
-
//Temporary fix.....//
|
|
383
|
-
|
|
384
|
-
setChannelToTop(channelToTop + 1);
|
|
385
|
-
setLoading(false);
|
|
386
|
-
setMsg('');
|
|
733
|
+
createUploadLink: {
|
|
734
|
+
variables: { postId },
|
|
387
735
|
},
|
|
388
736
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
737
|
+
|
|
738
|
+
// If upload fails, show error notification
|
|
739
|
+
if (uploadResponse?.error) {
|
|
740
|
+
console.error('Upload error:', uploadResponse.error);
|
|
741
|
+
|
|
742
|
+
// Format error message
|
|
743
|
+
let errorMsg = 'Failed to upload image. Please try again.';
|
|
744
|
+
if (__DEV__ && uploadResponse.error) {
|
|
745
|
+
// In development, show actual error
|
|
746
|
+
errorMsg =
|
|
747
|
+
typeof uploadResponse.error === 'string'
|
|
748
|
+
? uploadResponse.error
|
|
749
|
+
: uploadResponse.error.message || errorMsg;
|
|
402
750
|
}
|
|
403
|
-
//Temporary fix.....//
|
|
404
|
-
const newMessage: any = data?.sendMessage;
|
|
405
|
-
setChannelMessages((oldMessages: any) => uniqBy([...oldMessages, newMessage], ({ id }) => id));
|
|
406
|
-
setTotalCount((t) => t + 1);
|
|
407
|
-
//Temporary fix.....//
|
|
408
751
|
|
|
409
|
-
|
|
752
|
+
// Show error notification
|
|
753
|
+
setNotificationType('error');
|
|
754
|
+
setErrorMessage(errorMsg);
|
|
755
|
+
|
|
756
|
+
// Store error in state
|
|
757
|
+
setUploadErrors((prev) => ({ ...prev, [postId]: errorMsg }));
|
|
758
|
+
|
|
759
|
+
// Remove the message from UI
|
|
760
|
+
removeMessageFromUI(postId);
|
|
761
|
+
setIsUploadingImage(false);
|
|
410
762
|
setLoading(false);
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Get uploaded file info
|
|
767
|
+
const uploadedFiles = uploadResponse.data as unknown as IFileInfo[];
|
|
768
|
+
const fileIds = uploadedFiles?.map((f: any) => f.id) ?? null;
|
|
769
|
+
|
|
770
|
+
// Send the message with uploaded files
|
|
771
|
+
if (fileIds?.length > 0) {
|
|
772
|
+
await sendMsg({
|
|
773
|
+
variables: {
|
|
774
|
+
postId,
|
|
775
|
+
channelId,
|
|
776
|
+
content: currentMessageText || ' ',
|
|
777
|
+
files: fileIds,
|
|
778
|
+
notificationParams: notificationData,
|
|
779
|
+
},
|
|
780
|
+
optimisticResponse: {
|
|
781
|
+
__typename: 'Mutation',
|
|
782
|
+
sendMessage: optimisticMessage,
|
|
783
|
+
},
|
|
784
|
+
update: (cache, { data }) => {
|
|
785
|
+
if (!data?.sendMessage) {
|
|
786
|
+
setIsUploadingImage(false);
|
|
787
|
+
setLoading(false);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
// Let Apollo type policies handle the cache update
|
|
792
|
+
cache.writeQuery({
|
|
793
|
+
query: MESSAGES_DOCUMENT,
|
|
794
|
+
variables: {
|
|
795
|
+
channelId: channelId?.toString(),
|
|
796
|
+
parentId: null,
|
|
797
|
+
limit: MESSAGES_PER_PAGE,
|
|
798
|
+
skip: 0,
|
|
799
|
+
},
|
|
800
|
+
data: {
|
|
801
|
+
messages: {
|
|
802
|
+
__typename: 'Messages',
|
|
803
|
+
messagesRefId: channelId,
|
|
804
|
+
data: [data.sendMessage],
|
|
805
|
+
totalCount: 1, // Just one message
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// Check if the server response has the actual image
|
|
811
|
+
const serverMessage = data.sendMessage;
|
|
812
|
+
const hasServerImage = serverMessage?.files?.data?.some((file) => file.url);
|
|
813
|
+
|
|
814
|
+
if (hasServerImage) {
|
|
815
|
+
// Now that server has the image, we can remove client version
|
|
816
|
+
removeMessageFromUI(postId);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
setIsUploadingImage(false);
|
|
820
|
+
setLoading(false);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.error('Cache update error:', error);
|
|
823
|
+
|
|
824
|
+
// Format error for notification
|
|
825
|
+
let errorMsg = 'Failed to update message.';
|
|
826
|
+
if (__DEV__ && error) {
|
|
827
|
+
// In development, show actual error
|
|
828
|
+
errorMsg = error.message
|
|
829
|
+
? error.message.replace('[ApolloError: ', '').replace(']', '')
|
|
830
|
+
: 'Cache update failed';
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
setNotificationType('error');
|
|
834
|
+
setErrorMessage(errorMsg);
|
|
835
|
+
setIsUploadingImage(false);
|
|
836
|
+
setLoading(false);
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
} else {
|
|
841
|
+
setIsUploadingImage(false);
|
|
842
|
+
setLoading(false);
|
|
843
|
+
}
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.error('Background process error:', error);
|
|
846
|
+
|
|
847
|
+
// Format error for notification
|
|
848
|
+
let errorMsg = 'Failed to send image. Please try again.';
|
|
849
|
+
if (__DEV__ && error) {
|
|
850
|
+
// In development, show actual error
|
|
851
|
+
errorMsg = error.message
|
|
852
|
+
? error.message.replace('[ApolloError: ', '').replace(']', '')
|
|
853
|
+
: 'Background process failed';
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Show error notification
|
|
857
|
+
setNotificationType('error');
|
|
858
|
+
setErrorMessage(errorMsg);
|
|
859
|
+
removeMessageFromUI(postId);
|
|
860
|
+
setIsUploadingImage(false);
|
|
861
|
+
setLoading(false);
|
|
862
|
+
}
|
|
863
|
+
}, 0);
|
|
864
|
+
|
|
865
|
+
// Return success immediately - UI already updated
|
|
866
|
+
return { success: true };
|
|
867
|
+
} catch (error) {
|
|
868
|
+
console.error('Send message error:', error);
|
|
869
|
+
|
|
870
|
+
// Format error for notification
|
|
871
|
+
let errorMsg = 'Failed to process image. Please try again.';
|
|
872
|
+
if (__DEV__ && error) {
|
|
873
|
+
// In development, show actual error
|
|
874
|
+
errorMsg = error.message
|
|
875
|
+
? error.message.replace('[ApolloError: ', '').replace(']', '')
|
|
876
|
+
: 'Image processing failed';
|
|
414
877
|
}
|
|
415
|
-
},
|
|
416
|
-
[setChannelMessages, channelId, images, channelToTop, expoTokens],
|
|
417
|
-
);
|
|
418
878
|
|
|
879
|
+
// Show error notification
|
|
880
|
+
setNotificationType('error');
|
|
881
|
+
setErrorMessage(errorMsg);
|
|
882
|
+
setError(String(error));
|
|
883
|
+
setIsUploadingImage(false);
|
|
884
|
+
setLoading(false);
|
|
885
|
+
return { error: String(error) };
|
|
886
|
+
}
|
|
887
|
+
}, [channelId, messageText, images, selectedImage, startUpload, sendMsg, auth, removeMessageFromUI]);
|
|
888
|
+
|
|
889
|
+
// Create direct channel implementation
|
|
890
|
+
const createDirectChannelImpl = useCallback(async () => {
|
|
891
|
+
try {
|
|
892
|
+
setLoading(true);
|
|
893
|
+
if (
|
|
894
|
+
!rest?.isCreateNewChannel ||
|
|
895
|
+
rest?.newChannelData?.type !== RoomType?.Direct ||
|
|
896
|
+
!rest?.newChannelData?.userIds?.length
|
|
897
|
+
) {
|
|
898
|
+
setLoading(false);
|
|
899
|
+
setNotificationType('error');
|
|
900
|
+
setErrorMessage(__DEV__ ? 'Invalid channel data' : 'Unable to create conversation');
|
|
901
|
+
return { error: 'Invalid channel data' };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Store current message text
|
|
905
|
+
const currentMessageText = messageText;
|
|
906
|
+
// Clear message text immediately for better UX
|
|
907
|
+
setMessageText('');
|
|
908
|
+
|
|
909
|
+
const response = await addDirectChannel({
|
|
910
|
+
variables: {
|
|
911
|
+
receiver: [...(rest?.newChannelData?.userIds ?? [])],
|
|
912
|
+
displayName: 'DIRECT CHANNEL',
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
if (!response?.data?.createDirectChannel?.id) {
|
|
917
|
+
setLoading(false);
|
|
918
|
+
setNotificationType('error');
|
|
919
|
+
setErrorMessage(__DEV__ ? 'Failed to create channel' : 'Unable to create conversation');
|
|
920
|
+
return { error: 'Failed to create channel' };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const newChannelId = response.data.createDirectChannel.id;
|
|
924
|
+
setChannelId(newChannelId);
|
|
925
|
+
|
|
926
|
+
const notificationData: IExpoNotificationData = {
|
|
927
|
+
url: config.INBOX_MESSEGE_PATH,
|
|
928
|
+
params: { channelId: newChannelId, hideTabBar: true },
|
|
929
|
+
screen: 'DialogMessages',
|
|
930
|
+
other: { sound: Platform.OS === 'android' ? undefined : 'default' },
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// Create unique message ID for optimistic response
|
|
934
|
+
const messageId = objectId();
|
|
935
|
+
|
|
936
|
+
// Create minimal optimistic message
|
|
937
|
+
const optimisticMessage = {
|
|
938
|
+
__typename: 'Post' as const,
|
|
939
|
+
id: messageId,
|
|
940
|
+
message: currentMessageText,
|
|
941
|
+
createdAt: new Date().toISOString(),
|
|
942
|
+
updatedAt: new Date().toISOString(),
|
|
943
|
+
author: {
|
|
944
|
+
__typename: 'UserAccount' as const,
|
|
945
|
+
id: auth?.id,
|
|
946
|
+
picture: auth?.picture || '',
|
|
947
|
+
givenName: auth?.givenName || '',
|
|
948
|
+
familyName: auth?.familyName || '',
|
|
949
|
+
email: auth?.email || '',
|
|
950
|
+
username: auth?.username || '',
|
|
951
|
+
alias: [] as string[],
|
|
952
|
+
tokens: auth?.token ? [...auth?.token] : [],
|
|
953
|
+
},
|
|
954
|
+
isDelivered: true,
|
|
955
|
+
isRead: false,
|
|
956
|
+
type: PostTypeEnum.Simple,
|
|
957
|
+
parentId: null,
|
|
958
|
+
fromServer: false,
|
|
959
|
+
channel: {
|
|
960
|
+
__typename: 'Channel' as const,
|
|
961
|
+
id: newChannelId,
|
|
962
|
+
},
|
|
963
|
+
// Required fields that Apollo expects in the cache
|
|
964
|
+
propsConfiguration: {
|
|
965
|
+
__typename: 'MachineConfiguration' as const,
|
|
966
|
+
id: null,
|
|
967
|
+
resource: '' as any,
|
|
968
|
+
contents: null,
|
|
969
|
+
keys: null,
|
|
970
|
+
target: null,
|
|
971
|
+
overrides: null,
|
|
972
|
+
},
|
|
973
|
+
props: {},
|
|
974
|
+
files: {
|
|
975
|
+
__typename: 'FilesInfo' as const,
|
|
976
|
+
data: [],
|
|
977
|
+
totalCount: 0,
|
|
978
|
+
},
|
|
979
|
+
replies: {
|
|
980
|
+
__typename: 'Messages' as const,
|
|
981
|
+
data: [],
|
|
982
|
+
totalCount: 0,
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
// Send message in the new channel
|
|
987
|
+
await sendMsg({
|
|
988
|
+
variables: {
|
|
989
|
+
channelId: newChannelId,
|
|
990
|
+
content: currentMessageText,
|
|
991
|
+
notificationParams: notificationData,
|
|
992
|
+
},
|
|
993
|
+
optimisticResponse: {
|
|
994
|
+
__typename: 'Mutation',
|
|
995
|
+
sendMessage: optimisticMessage,
|
|
996
|
+
},
|
|
997
|
+
update: (cache, { data }) => {
|
|
998
|
+
if (!data?.sendMessage) return;
|
|
999
|
+
|
|
1000
|
+
try {
|
|
1001
|
+
// For a new channel, simply write the initial message to the cache
|
|
1002
|
+
// The type policies will handle it properly
|
|
1003
|
+
cache.writeQuery({
|
|
1004
|
+
query: MESSAGES_DOCUMENT,
|
|
1005
|
+
variables: {
|
|
1006
|
+
channelId: newChannelId,
|
|
1007
|
+
parentId: null,
|
|
1008
|
+
limit: MESSAGES_PER_PAGE,
|
|
1009
|
+
skip: 0,
|
|
1010
|
+
},
|
|
1011
|
+
data: {
|
|
1012
|
+
messages: {
|
|
1013
|
+
__typename: 'Messages',
|
|
1014
|
+
messagesRefId: newChannelId,
|
|
1015
|
+
data: [data.sendMessage],
|
|
1016
|
+
totalCount: 1,
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
console.error('Error updating cache:', error);
|
|
1022
|
+
|
|
1023
|
+
// Format error for notification
|
|
1024
|
+
let errorMsg = 'Failed to update message cache';
|
|
1025
|
+
if (__DEV__ && error) {
|
|
1026
|
+
// In development, show actual error
|
|
1027
|
+
errorMsg = error.message
|
|
1028
|
+
? error.message.replace('[ApolloError: ', '').replace(']', '')
|
|
1029
|
+
: 'Cache update failed';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
setNotificationType('error');
|
|
1033
|
+
setErrorMessage(errorMsg);
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
setLoading(false);
|
|
1039
|
+
return { channelId: newChannelId };
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
setLoading(false);
|
|
1042
|
+
|
|
1043
|
+
// Format error for notification
|
|
1044
|
+
let errorMsg = 'Failed to create conversation';
|
|
1045
|
+
if (__DEV__ && error) {
|
|
1046
|
+
// In development, show actual error
|
|
1047
|
+
errorMsg = error.message
|
|
1048
|
+
? error.message.replace('[ApolloError: ', '').replace(']', '')
|
|
1049
|
+
: 'Channel creation failed';
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
setNotificationType('error');
|
|
1053
|
+
setErrorMessage(errorMsg);
|
|
1054
|
+
setError(String(error));
|
|
1055
|
+
return { error: String(error) };
|
|
1056
|
+
}
|
|
1057
|
+
}, [rest, messageText, addDirectChannel, sendMsg, auth]);
|
|
1058
|
+
|
|
1059
|
+
// Optimize onFetchOld by adding debounce logic
|
|
1060
|
+
const onFetchOld = useCallback(() => {
|
|
1061
|
+
// Prevent multiple rapid calls
|
|
1062
|
+
if (fetchOldDebounceRef.current) return;
|
|
1063
|
+
|
|
1064
|
+
// Check if we need to fetch more messages
|
|
1065
|
+
if (totalCount > channelMessages.length && !loadingOldMessages) {
|
|
1066
|
+
// Set debounce
|
|
1067
|
+
fetchOldDebounceRef.current = true;
|
|
1068
|
+
|
|
1069
|
+
// Fetch more messages
|
|
1070
|
+
fetchMoreMessagesImpl();
|
|
1071
|
+
|
|
1072
|
+
// Clear debounce after a timeout
|
|
1073
|
+
setTimeout(() => {
|
|
1074
|
+
fetchOldDebounceRef.current = false;
|
|
1075
|
+
}, 1000);
|
|
1076
|
+
}
|
|
1077
|
+
}, [totalCount, channelMessages.length, loadingOldMessages, fetchMoreMessagesImpl]);
|
|
1078
|
+
|
|
1079
|
+
const isCloseToTop = ({ layoutMeasurement, contentOffset, contentSize }) => {
|
|
1080
|
+
const paddingToTop = 60;
|
|
1081
|
+
return contentSize.height - layoutMeasurement.height - paddingToTop <= contentOffset.y;
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
// Modify the messageList function to ensure local images take precedence
|
|
419
1085
|
const messageList = useMemo(() => {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1086
|
+
// Get pending upload messages as array
|
|
1087
|
+
const pendingMessages = Object.values(pendingUploads);
|
|
1088
|
+
|
|
1089
|
+
// If we have no server messages, just return pending messages
|
|
1090
|
+
if (!channelMessages || channelMessages.length === 0) {
|
|
1091
|
+
return pendingMessages;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Filter unique messages
|
|
1095
|
+
const filteredMessages = uniqBy(channelMessages, ({ id }) => id);
|
|
1096
|
+
|
|
1097
|
+
// Process server messages - skip any that have client versions
|
|
1098
|
+
const serverMessages = orderBy(filteredMessages, ['createdAt'], ['desc'])
|
|
1099
|
+
.map((msg) => {
|
|
1100
|
+
const date = new Date(msg.createdAt);
|
|
1101
|
+
|
|
1102
|
+
// Skip messages that are in pendingUploads - client version takes precedence
|
|
1103
|
+
if (pendingUploads[msg.id]) {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Extract image URLs from files data
|
|
1108
|
+
let imageUrls: string[] = [];
|
|
1109
|
+
let primaryImageUrl = null;
|
|
1110
|
+
|
|
1111
|
+
if (msg.files && typeof msg.files === 'object') {
|
|
1112
|
+
const filesData = msg.files.data || (Array.isArray(msg.files) ? msg.files : null);
|
|
1113
|
+
|
|
1114
|
+
if (filesData && filesData.length > 0) {
|
|
1115
|
+
// Collect all image URLs
|
|
1116
|
+
imageUrls = filesData
|
|
1117
|
+
.filter((fileData) => fileData && typeof fileData === 'object' && fileData.url)
|
|
1118
|
+
.map((fileData) => fileData.url);
|
|
1119
|
+
|
|
1120
|
+
// Set primary image for GiftedChat compatibility
|
|
1121
|
+
if (imageUrls.length > 0) {
|
|
1122
|
+
primaryImageUrl = imageUrls[0];
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Create formatted message
|
|
1128
|
+
return {
|
|
1129
|
+
_id: msg.id,
|
|
1130
|
+
text: msg.message,
|
|
1131
|
+
createdAt: date,
|
|
430
1132
|
user: {
|
|
431
|
-
_id: '',
|
|
432
|
-
name: ''
|
|
433
|
-
avatar: '',
|
|
1133
|
+
_id: msg.author?.id || '',
|
|
1134
|
+
name: `${msg.author?.givenName || ''} ${msg.author?.familyName || ''}`,
|
|
1135
|
+
avatar: msg.author?.picture || '',
|
|
434
1136
|
},
|
|
435
|
-
|
|
1137
|
+
image: primaryImageUrl,
|
|
1138
|
+
images: imageUrls, // Store all images for custom rendering
|
|
1139
|
+
sent: msg?.isDelivered,
|
|
1140
|
+
received: msg?.isRead,
|
|
1141
|
+
type: msg?.type,
|
|
1142
|
+
propsConfiguration: msg?.propsConfiguration,
|
|
1143
|
+
replies: msg?.replies ?? [],
|
|
1144
|
+
isShowThreadMessage,
|
|
436
1145
|
};
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1146
|
+
})
|
|
1147
|
+
.filter(Boolean); // Remove null entries
|
|
1148
|
+
|
|
1149
|
+
// Pending messages take precedence (they have local images)
|
|
1150
|
+
return [...pendingMessages, ...serverMessages];
|
|
1151
|
+
}, [channelMessages, pendingUploads, isShowThreadMessage]);
|
|
1152
|
+
|
|
1153
|
+
// Render the send button
|
|
1154
|
+
const renderSend = useCallback(
|
|
1155
|
+
(props) => {
|
|
1156
|
+
// If action sheet is visible, don't show the default send button
|
|
1157
|
+
// if (isActionSheetVisible) {
|
|
1158
|
+
// return null;
|
|
1159
|
+
// }
|
|
1160
|
+
|
|
1161
|
+
// Enable the send button if there's text OR we have images
|
|
1162
|
+
const hasContent = !!props.text || images?.length > 0;
|
|
1163
|
+
const canSend = (channelId || rest?.isCreateNewChannel) && hasContent;
|
|
1164
|
+
// const isDisabled = !canSend || isUploadingImage || loading;
|
|
1165
|
+
const isDisabled = !canSend;
|
|
1166
|
+
|
|
1167
|
+
return (
|
|
1168
|
+
<Send
|
|
1169
|
+
{...props}
|
|
1170
|
+
//disabled={isDisabled}
|
|
1171
|
+
containerStyle={{
|
|
1172
|
+
justifyContent: 'center',
|
|
1173
|
+
alignItems: 'center',
|
|
1174
|
+
height: 40,
|
|
1175
|
+
width: 44,
|
|
1176
|
+
marginRight: 4,
|
|
1177
|
+
marginBottom: 0,
|
|
1178
|
+
marginLeft: 4,
|
|
1179
|
+
}}
|
|
1180
|
+
>
|
|
1181
|
+
<View style={{ padding: 4 }}>
|
|
1182
|
+
<MaterialCommunityIcons
|
|
1183
|
+
name="send-circle"
|
|
1184
|
+
size={32}
|
|
1185
|
+
color={isDisabled ? colors.gray[400] : colors.blue[500]}
|
|
1186
|
+
/>
|
|
1187
|
+
</View>
|
|
1188
|
+
</Send>
|
|
1189
|
+
);
|
|
1190
|
+
},
|
|
1191
|
+
[channelId, images, rest?.isCreateNewChannel, isUploadingImage, loading, isActionSheetVisible],
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
// Add new handler to open the action sheet
|
|
1195
|
+
const openExpandableInput = useCallback(() => {
|
|
1196
|
+
console.log('Opening action sheet');
|
|
1197
|
+
setActionSheetVisible(true);
|
|
1198
|
+
}, []);
|
|
1199
|
+
|
|
1200
|
+
// Add a debug useEffect to log when visibility changes
|
|
1201
|
+
useEffect(() => {
|
|
1202
|
+
console.log('Action sheet visibility:', isActionSheetVisible);
|
|
1203
|
+
// Set appropriate bottom margin when action sheet visibility changes
|
|
1204
|
+
if (isActionSheetVisible) {
|
|
1205
|
+
setBottomMargin(0);
|
|
455
1206
|
}
|
|
456
|
-
|
|
457
|
-
//return res;
|
|
458
|
-
}, [channelMessages, channelId]);
|
|
1207
|
+
}, [isActionSheetVisible]);
|
|
459
1208
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
1209
|
+
// Handle removing image from action sheet
|
|
1210
|
+
const handleRemoveImage = useCallback(
|
|
1211
|
+
(index: number) => {
|
|
1212
|
+
const newImages = [...images];
|
|
1213
|
+
newImages.splice(index, 1);
|
|
1214
|
+
setImages(newImages);
|
|
1215
|
+
if (newImages.length === 0) {
|
|
1216
|
+
setSelectedImage('');
|
|
1217
|
+
if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
|
|
1218
|
+
textInputRef.current.focus();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
[images],
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
// Add a new state to track when the action sheet updates text
|
|
1226
|
+
const [textUpdatedInActionSheet, setTextUpdatedInActionSheet] = useState(false);
|
|
1227
|
+
|
|
1228
|
+
// Handle when the action sheet is closed
|
|
1229
|
+
const handleActionSheetClose = useCallback(() => {
|
|
1230
|
+
// Mark that we closed the sheet with potential text update
|
|
1231
|
+
setTextUpdatedInActionSheet(true);
|
|
1232
|
+
setActionSheetVisible(false);
|
|
1233
|
+
// Reset bottom margin to 0 when closing the expandable input
|
|
1234
|
+
setBottomMargin(0);
|
|
1235
|
+
}, []);
|
|
1236
|
+
|
|
1237
|
+
// Handle sending from action sheet
|
|
1238
|
+
const handleActionSheetSend = () => {
|
|
1239
|
+
if (messageText.trim() || images.length > 0) {
|
|
1240
|
+
// Set uploading state to show spinner
|
|
1241
|
+
setIsUploadingImage(true);
|
|
1242
|
+
|
|
1243
|
+
// Create a message object in the format GiftedChat expects
|
|
1244
|
+
const messages = [
|
|
1245
|
+
{
|
|
1246
|
+
text: messageText,
|
|
1247
|
+
user: {
|
|
1248
|
+
_id: auth?.id || '',
|
|
1249
|
+
},
|
|
1250
|
+
createdAt: new Date(),
|
|
1251
|
+
},
|
|
1252
|
+
];
|
|
1253
|
+
|
|
1254
|
+
// Use the existing handleSend function
|
|
1255
|
+
handleSend(messages);
|
|
1256
|
+
|
|
1257
|
+
// Close the action sheet
|
|
1258
|
+
setActionSheetVisible(false);
|
|
1259
|
+
}
|
|
473
1260
|
};
|
|
474
1261
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1262
|
+
// Update this useEffect to more reliably handle text syncing when action sheet closes
|
|
1263
|
+
useEffect(() => {
|
|
1264
|
+
// If action sheet just closed, ensure main input gets updated text
|
|
1265
|
+
if (!isActionSheetVisible && textUpdatedInActionSheet) {
|
|
1266
|
+
console.log('Action sheet closed with text:', messageText);
|
|
1267
|
+
// Reset the flag
|
|
1268
|
+
setTextUpdatedInActionSheet(false);
|
|
1269
|
+
|
|
1270
|
+
// Force GiftedChat to recognize the text change by creating a new state update
|
|
1271
|
+
const currentText = messageText;
|
|
1272
|
+
setMessageText('');
|
|
1273
|
+
setTimeout(() => {
|
|
1274
|
+
setMessageText(currentText);
|
|
1275
|
+
}, 50);
|
|
1276
|
+
}
|
|
1277
|
+
}, [isActionSheetVisible, textUpdatedInActionSheet]);
|
|
1278
|
+
|
|
1279
|
+
// Take a screenshot of the action sheet for debugging
|
|
1280
|
+
useEffect(() => {
|
|
1281
|
+
if (isActionSheetVisible && Platform.OS === 'ios') {
|
|
1282
|
+
console.log('Action sheet is visible, should show the input and options');
|
|
1283
|
+
}
|
|
1284
|
+
}, [isActionSheetVisible]);
|
|
1285
|
+
|
|
1286
|
+
// Handle send for messages
|
|
1287
|
+
const handleSend = useCallback(
|
|
1288
|
+
async (messages) => {
|
|
1289
|
+
// Extract message text from GiftedChat messages array
|
|
1290
|
+
const newMessageText = messages && messages.length > 0 ? messages[0]?.text || ' ' : ' ';
|
|
1291
|
+
|
|
1292
|
+
// Check if we can send a message (channel exists or we're creating one)
|
|
1293
|
+
if (!channelId && !rest?.isCreateNewChannel) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Allow sending if we have text OR images (image-only messages are valid)
|
|
1298
|
+
const hasText = !!newMessageText && newMessageText !== ' ';
|
|
1299
|
+
const hasImages = images && images.length > 0;
|
|
1300
|
+
|
|
1301
|
+
if (!hasText && !hasImages) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Update the message text state - now handled in send functions for better UX
|
|
1306
|
+
setMessageText(newMessageText);
|
|
1307
|
+
|
|
1308
|
+
// Set uploading state to show spinner
|
|
1309
|
+
// setIsUploadingImage(true);
|
|
1310
|
+
// setLoading(true);
|
|
1311
|
+
|
|
1312
|
+
// Handle direct channel creation if needed
|
|
1313
|
+
if (rest?.isCreateNewChannel && !channelId) {
|
|
1314
|
+
if (rest?.newChannelData?.type === RoomType?.Direct) {
|
|
1315
|
+
await createDirectChannelImpl();
|
|
499
1316
|
}
|
|
1317
|
+
setIsUploadingImage(false);
|
|
1318
|
+
setLoading(false);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
500
1321
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
params = { reservationId: actionId };
|
|
1322
|
+
// Send message with or without image based on state
|
|
1323
|
+
if (hasImages) {
|
|
1324
|
+
await sendMessageWithFileImpl();
|
|
1325
|
+
} else {
|
|
1326
|
+
await sendMessageImpl();
|
|
507
1327
|
}
|
|
508
1328
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
1329
|
+
setIsUploadingImage(false);
|
|
1330
|
+
setLoading(false);
|
|
1331
|
+
|
|
1332
|
+
// Focus the input field after sending
|
|
1333
|
+
setTimeout(() => {
|
|
1334
|
+
if (textInputRef.current) {
|
|
1335
|
+
textInputRef.current.focus();
|
|
1336
|
+
}
|
|
1337
|
+
}, 100);
|
|
1338
|
+
},
|
|
1339
|
+
[
|
|
1340
|
+
channelId,
|
|
1341
|
+
images,
|
|
1342
|
+
rest?.isCreateNewChannel,
|
|
1343
|
+
rest?.newChannelData?.type,
|
|
1344
|
+
createDirectChannelImpl,
|
|
1345
|
+
sendMessageWithFileImpl,
|
|
1346
|
+
sendMessageImpl,
|
|
1347
|
+
],
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
// Render message text with customizations for alerts and replies
|
|
1351
|
+
const renderMessageText = useCallback(
|
|
1352
|
+
(props: any) => {
|
|
1353
|
+
const { currentMessage } = props;
|
|
1354
|
+
const lastReply: any =
|
|
1355
|
+
currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
|
|
1356
|
+
|
|
1357
|
+
// Do not render anything if the message text is empty or only whitespace
|
|
1358
|
+
if (!currentMessage?.text || currentMessage.text.trim() === '') {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (currentMessage.type === 'ALERT') {
|
|
1363
|
+
const attachment = currentMessage?.propsConfiguration?.contents?.attachment;
|
|
1364
|
+
let action: string = '';
|
|
1365
|
+
let actionId: any = '';
|
|
1366
|
+
let params: any = {};
|
|
1367
|
+
|
|
1368
|
+
if (attachment?.callToAction?.extraParams) {
|
|
1369
|
+
const extraParams: any = attachment?.callToAction?.extraParams;
|
|
1370
|
+
const route: any = extraParams?.route ?? null;
|
|
1371
|
+
let path: any = null;
|
|
1372
|
+
let param: any = null;
|
|
1373
|
+
if (role && role == PreDefinedRole.Guest) {
|
|
1374
|
+
path = route?.guest?.name ? route?.guest?.name ?? null : null;
|
|
1375
|
+
param = route?.guest?.params ? route?.guest?.params ?? null : null;
|
|
1376
|
+
} else if (role && role == PreDefinedRole.Owner) {
|
|
1377
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
1378
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
1379
|
+
} else {
|
|
1380
|
+
path = route?.host?.name ? route?.host?.name ?? null : null;
|
|
1381
|
+
param = route?.host?.params ? route?.host?.params ?? null : null;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
action = path;
|
|
1385
|
+
params = { ...param };
|
|
1386
|
+
} else if (attachment?.callToAction?.link) {
|
|
1387
|
+
action = CALL_TO_ACTION_PATH;
|
|
1388
|
+
actionId = attachment?.callToAction?.link.split('/').pop();
|
|
1389
|
+
params = { reservationId: actionId };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
return (
|
|
1393
|
+
<>
|
|
1394
|
+
{attachment?.callToAction && action ? (
|
|
1395
|
+
<Box className={`bg-[${CALL_TO_ACTION_BOX_BGCOLOR}] rounded-[15] pb-2`}>
|
|
1396
|
+
<MessageText
|
|
1397
|
+
{...props}
|
|
1398
|
+
containerStyle={{
|
|
1399
|
+
left: { paddingLeft: 0, marginLeft: 0 },
|
|
1400
|
+
}}
|
|
1401
|
+
textStyle={{
|
|
1402
|
+
left: { marginLeft: 0 },
|
|
1403
|
+
}}
|
|
1404
|
+
/>
|
|
1405
|
+
<Button
|
|
1406
|
+
variant={'outline'}
|
|
1407
|
+
size={'sm'}
|
|
1408
|
+
className={`border-[${CALL_TO_ACTION_BUTTON_BORDERCOLOR}] my-2 rounded-full `}
|
|
1409
|
+
onPress={() => action && params && navigation.navigate(action, params)}
|
|
1410
|
+
>
|
|
1411
|
+
<ButtonText className={`color-[${CALL_TO_ACTION_TEXT_COLOR}]`}>
|
|
1412
|
+
{attachment.callToAction.title}
|
|
1413
|
+
</ButtonText>
|
|
1414
|
+
</Button>
|
|
1415
|
+
</Box>
|
|
1416
|
+
) : (
|
|
1417
|
+
<TouchableHighlight
|
|
1418
|
+
underlayColor={'#c0c0c0'}
|
|
1419
|
+
style={{ width: '100%' }}
|
|
1420
|
+
onPress={() => {
|
|
1421
|
+
if (currentMessage?.isShowThreadMessage)
|
|
1422
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
1423
|
+
channelId: channelId,
|
|
1424
|
+
title: 'Message',
|
|
1425
|
+
postParentId: currentMessage?._id,
|
|
1426
|
+
isPostParentIdThread: true,
|
|
1427
|
+
});
|
|
528
1428
|
}}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
source={{
|
|
574
|
-
uri: p?.author?.picture,
|
|
575
|
-
}}
|
|
576
|
-
/>
|
|
577
|
-
</Avatar>
|
|
578
|
-
))}
|
|
1429
|
+
>
|
|
1430
|
+
<>
|
|
1431
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
1432
|
+
{currentMessage?.replies?.data?.length > 0 && (
|
|
1433
|
+
<HStack space={'sm'} className="px-1 items-center">
|
|
1434
|
+
<HStack>
|
|
1435
|
+
{currentMessage?.replies?.data
|
|
1436
|
+
?.filter(
|
|
1437
|
+
(v: any, i: any, a: any) =>
|
|
1438
|
+
a.findIndex((t: any) => t?.author?.id === v?.author?.id) ===
|
|
1439
|
+
i,
|
|
1440
|
+
)
|
|
1441
|
+
?.slice(0, 2)
|
|
1442
|
+
?.reverse()
|
|
1443
|
+
?.map((p: any, i: Number) => (
|
|
1444
|
+
<Avatar
|
|
1445
|
+
key={'conversations-view-key-' + i}
|
|
1446
|
+
size={'sm'}
|
|
1447
|
+
className="bg-transparent"
|
|
1448
|
+
>
|
|
1449
|
+
<AvatarFallbackText>
|
|
1450
|
+
{startCase(p?.author?.username?.charAt(0))}
|
|
1451
|
+
</AvatarFallbackText>
|
|
1452
|
+
<AvatarImage
|
|
1453
|
+
alt="user image"
|
|
1454
|
+
style={{
|
|
1455
|
+
borderRadius: 6,
|
|
1456
|
+
borderWidth: 2,
|
|
1457
|
+
borderColor: '#fff',
|
|
1458
|
+
}}
|
|
1459
|
+
source={{
|
|
1460
|
+
uri: p?.author?.picture,
|
|
1461
|
+
}}
|
|
1462
|
+
/>
|
|
1463
|
+
</Avatar>
|
|
1464
|
+
))}
|
|
1465
|
+
</HStack>
|
|
1466
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
1467
|
+
{currentMessage?.replies?.totalCount}{' '}
|
|
1468
|
+
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
1469
|
+
</Text>
|
|
1470
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
1471
|
+
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
1472
|
+
</Text>
|
|
579
1473
|
</HStack>
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
</Text>
|
|
584
|
-
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
585
|
-
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
586
|
-
</Text>
|
|
587
|
-
</HStack>
|
|
588
|
-
)}
|
|
589
|
-
</>
|
|
590
|
-
</TouchableHighlight>
|
|
591
|
-
)}
|
|
592
|
-
{/* <MessageText
|
|
593
|
-
{...props}
|
|
594
|
-
textStyle={{ left: { marginLeft: 5, color: CALL_TO_ACTION_TEXT_COLOR, paddingHorizontal: 2 } }}
|
|
595
|
-
/> */}
|
|
596
|
-
</>
|
|
597
|
-
);
|
|
598
|
-
} else {
|
|
599
|
-
return (
|
|
600
|
-
<TouchableHighlight
|
|
601
|
-
underlayColor={'#c0c0c0'}
|
|
602
|
-
style={{ width: '100%' }}
|
|
603
|
-
onPress={() => {
|
|
604
|
-
if (currentMessage?.isShowThreadMessage)
|
|
605
|
-
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
606
|
-
channelId: channelId,
|
|
607
|
-
title: 'Message',
|
|
608
|
-
postParentId: currentMessage?._id,
|
|
609
|
-
isPostParentIdThread: true,
|
|
610
|
-
});
|
|
611
|
-
}}
|
|
612
|
-
>
|
|
613
|
-
<>
|
|
614
|
-
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
615
|
-
{currentMessage?.replies?.data?.length > 0 && (
|
|
616
|
-
<HStack space={'sm'} className="px-1 items-center">
|
|
617
|
-
<HStack>
|
|
618
|
-
{currentMessage?.replies?.data
|
|
619
|
-
?.filter(
|
|
620
|
-
(v: any, i: any, a: any) =>
|
|
621
|
-
a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
|
|
622
|
-
)
|
|
623
|
-
?.slice(0, 2)
|
|
624
|
-
?.reverse()
|
|
625
|
-
?.map((p: any, i: Number) => (
|
|
626
|
-
<Avatar
|
|
627
|
-
key={'conversation-replies-key-' + i}
|
|
628
|
-
className="bg-transparent"
|
|
629
|
-
size={'sm'}
|
|
630
|
-
>
|
|
631
|
-
<AvatarFallbackText>
|
|
632
|
-
{startCase(p?.author?.username?.charAt(0))}
|
|
633
|
-
</AvatarFallbackText>
|
|
634
|
-
<AvatarImage
|
|
635
|
-
alt="user image"
|
|
636
|
-
style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
|
|
637
|
-
source={{
|
|
638
|
-
uri: p?.author?.picture,
|
|
639
|
-
}}
|
|
640
|
-
/>
|
|
641
|
-
</Avatar>
|
|
642
|
-
))}
|
|
643
|
-
</HStack>
|
|
644
|
-
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
645
|
-
{currentMessage?.replies?.totalCount}{' '}
|
|
646
|
-
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
647
|
-
</Text>
|
|
648
|
-
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
649
|
-
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
650
|
-
</Text>
|
|
651
|
-
</HStack>
|
|
1474
|
+
)}
|
|
1475
|
+
</>
|
|
1476
|
+
</TouchableHighlight>
|
|
652
1477
|
)}
|
|
653
1478
|
</>
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1479
|
+
);
|
|
1480
|
+
} else {
|
|
1481
|
+
return (
|
|
1482
|
+
<TouchableHighlight
|
|
1483
|
+
underlayColor={'#c0c0c0'}
|
|
1484
|
+
style={{ width: '100%' }}
|
|
1485
|
+
onPress={() => {
|
|
1486
|
+
if (currentMessage?.isShowThreadMessage)
|
|
1487
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
1488
|
+
channelId: channelId,
|
|
1489
|
+
title: 'Message',
|
|
1490
|
+
postParentId: currentMessage?._id,
|
|
1491
|
+
isPostParentIdThread: true,
|
|
1492
|
+
});
|
|
1493
|
+
}}
|
|
1494
|
+
>
|
|
1495
|
+
<>
|
|
1496
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
1497
|
+
{currentMessage?.replies?.data?.length > 0 && (
|
|
1498
|
+
<HStack space={'sm'} className="px-1 items-center">
|
|
1499
|
+
<HStack>
|
|
1500
|
+
{currentMessage?.replies?.data
|
|
1501
|
+
?.filter(
|
|
1502
|
+
(v: any, i: any, a: any) =>
|
|
1503
|
+
a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
|
|
1504
|
+
)
|
|
1505
|
+
?.slice(0, 2)
|
|
1506
|
+
?.reverse()
|
|
1507
|
+
?.map((p: any, i: Number) => (
|
|
1508
|
+
<Avatar
|
|
1509
|
+
key={'conversation-replies-key-' + i}
|
|
1510
|
+
className="bg-transparent"
|
|
1511
|
+
size={'sm'}
|
|
1512
|
+
>
|
|
1513
|
+
<AvatarFallbackText>
|
|
1514
|
+
{startCase(p?.author?.username?.charAt(0))}
|
|
1515
|
+
</AvatarFallbackText>
|
|
1516
|
+
<AvatarImage
|
|
1517
|
+
alt="user image"
|
|
1518
|
+
style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
|
|
1519
|
+
source={{
|
|
1520
|
+
uri: p?.author?.picture,
|
|
1521
|
+
}}
|
|
1522
|
+
/>
|
|
1523
|
+
</Avatar>
|
|
1524
|
+
))}
|
|
1525
|
+
</HStack>
|
|
1526
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
1527
|
+
{currentMessage?.replies?.totalCount}{' '}
|
|
1528
|
+
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
1529
|
+
</Text>
|
|
1530
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
1531
|
+
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
1532
|
+
</Text>
|
|
1533
|
+
</HStack>
|
|
1534
|
+
)}
|
|
1535
|
+
</>
|
|
1536
|
+
</TouchableHighlight>
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
[navigation, channelId, role],
|
|
1541
|
+
);
|
|
658
1542
|
|
|
1543
|
+
// Render action buttons (including image upload)
|
|
659
1544
|
const renderActions = (props) => {
|
|
660
1545
|
return (
|
|
661
1546
|
<Actions
|
|
662
1547
|
{...props}
|
|
663
|
-
|
|
1548
|
+
// options={{
|
|
1549
|
+
// ['Choose from Library']: onSelectImages,
|
|
1550
|
+
// ['Cancel']: () => {}, // Add this option to make the sheet dismissible
|
|
1551
|
+
// }}
|
|
1552
|
+
optionTintColor="#000000"
|
|
1553
|
+
cancelButtonIndex={1} // Set the Cancel option as the cancel button
|
|
1554
|
+
icon={() => (
|
|
1555
|
+
<TouchableOpacity
|
|
1556
|
+
onPress={onSelectImages}
|
|
1557
|
+
style={{
|
|
1558
|
+
width: 25,
|
|
1559
|
+
height: 25,
|
|
1560
|
+
borderRadius: 20,
|
|
1561
|
+
backgroundColor: '#f5f5f5',
|
|
1562
|
+
alignItems: 'center',
|
|
1563
|
+
justifyContent: 'center',
|
|
1564
|
+
marginRight: 8,
|
|
1565
|
+
}}
|
|
1566
|
+
>
|
|
1567
|
+
<MaterialIcons name="add" size={20} color="#888" />
|
|
1568
|
+
</TouchableOpacity>
|
|
1569
|
+
// <Box
|
|
1570
|
+
// style={{
|
|
1571
|
+
// width: 32,
|
|
1572
|
+
// height: 32,
|
|
1573
|
+
// alignItems: 'center',
|
|
1574
|
+
// justifyContent: 'center',
|
|
1575
|
+
// }}
|
|
1576
|
+
// >
|
|
1577
|
+
// <Ionicons name="image" size={24} color={colors.blue[500]} />
|
|
1578
|
+
// </Box>
|
|
1579
|
+
)}
|
|
1580
|
+
containerStyle={{
|
|
1581
|
+
alignItems: 'center',
|
|
1582
|
+
justifyContent: 'center',
|
|
1583
|
+
marginLeft: 20,
|
|
1584
|
+
marginBottom: 0,
|
|
1585
|
+
}}
|
|
664
1586
|
/>
|
|
665
1587
|
);
|
|
666
1588
|
};
|
|
667
1589
|
|
|
668
|
-
|
|
1590
|
+
// Create a more visible and reliable image preview with cancel button
|
|
1591
|
+
const renderAccessory = useCallback(() => {
|
|
1592
|
+
if (!images.length) return null;
|
|
669
1593
|
return (
|
|
670
|
-
<Box>
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1594
|
+
<Box style={{ position: 'relative', height: 70, backgroundColor: 'transparent', justifyContent: 'center' }}>
|
|
1595
|
+
<ScrollView
|
|
1596
|
+
horizontal
|
|
1597
|
+
showsHorizontalScrollIndicator={false}
|
|
1598
|
+
style={{
|
|
1599
|
+
flexDirection: 'row',
|
|
1600
|
+
paddingLeft: 15,
|
|
1601
|
+
paddingRight: 5,
|
|
1602
|
+
}}
|
|
1603
|
+
contentContainerStyle={{
|
|
1604
|
+
alignItems: 'center',
|
|
1605
|
+
height: '100%',
|
|
1606
|
+
}}
|
|
1607
|
+
>
|
|
1608
|
+
{images.map((img, index) => (
|
|
1609
|
+
<View
|
|
1610
|
+
key={`image-preview-${index}`}
|
|
1611
|
+
style={{
|
|
1612
|
+
width: 40,
|
|
1613
|
+
height: 40,
|
|
1614
|
+
marginRight: 15,
|
|
1615
|
+
borderRadius: 4,
|
|
1616
|
+
backgroundColor: colors.gray[200],
|
|
1617
|
+
overflow: 'hidden',
|
|
1618
|
+
borderWidth: 1,
|
|
1619
|
+
borderColor: '#e0e0e0',
|
|
1620
|
+
position: 'relative',
|
|
1621
|
+
zIndex: 10,
|
|
688
1622
|
}}
|
|
689
1623
|
>
|
|
690
|
-
<
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1624
|
+
<Image
|
|
1625
|
+
source={{ uri: img.uri || img.url }}
|
|
1626
|
+
style={{ width: '100%', height: '100%' }}
|
|
1627
|
+
alt={`selected image ${index + 1}`}
|
|
1628
|
+
/>
|
|
1629
|
+
{/* Cross button at top right */}
|
|
1630
|
+
<TouchableOpacity
|
|
1631
|
+
onPress={() => {
|
|
1632
|
+
const newImages = [...images];
|
|
1633
|
+
newImages.splice(index, 1);
|
|
1634
|
+
setImages(newImages);
|
|
1635
|
+
if (newImages.length === 0) {
|
|
1636
|
+
setSelectedImage('');
|
|
1637
|
+
if (textInputRef.current && typeof textInputRef.current.focus === 'function') {
|
|
1638
|
+
textInputRef.current.focus();
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}}
|
|
1642
|
+
style={{
|
|
1643
|
+
position: 'absolute',
|
|
1644
|
+
top: -1,
|
|
1645
|
+
right: -1,
|
|
1646
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
1647
|
+
borderRadius: 12,
|
|
1648
|
+
width: 20,
|
|
1649
|
+
height: 20,
|
|
1650
|
+
alignItems: 'center',
|
|
1651
|
+
justifyContent: 'center',
|
|
1652
|
+
zIndex: 9999,
|
|
1653
|
+
}}
|
|
1654
|
+
>
|
|
1655
|
+
<Ionicons name="close" size={16} color="white" />
|
|
1656
|
+
</TouchableOpacity>
|
|
1657
|
+
</View>
|
|
1658
|
+
))}
|
|
1659
|
+
</ScrollView>
|
|
694
1660
|
</Box>
|
|
695
1661
|
);
|
|
696
|
-
};
|
|
1662
|
+
}, [images]);
|
|
697
1663
|
|
|
698
1664
|
const setImageViewerObject = (obj: any, v: boolean) => {
|
|
699
1665
|
setImageObject(obj);
|
|
@@ -701,17 +1667,16 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
|
|
|
701
1667
|
};
|
|
702
1668
|
|
|
703
1669
|
const modalContent = React.useMemo(() => {
|
|
704
|
-
if (!imageObject) return
|
|
1670
|
+
if (!imageObject || !imageObject.image) return null;
|
|
705
1671
|
const { image, _id } = imageObject;
|
|
1672
|
+
|
|
706
1673
|
return (
|
|
707
1674
|
<CachedImage
|
|
708
1675
|
style={{ width: '100%', height: '100%' }}
|
|
709
1676
|
resizeMode={'cover'}
|
|
710
|
-
|
|
711
|
-
cacheKey={`${_id}-slack-bubble-imageKey`}
|
|
1677
|
+
cacheKey={`${_id}-modal-imageKey`}
|
|
712
1678
|
source={{
|
|
713
1679
|
uri: image,
|
|
714
|
-
//headers: `Authorization: Bearer ${token}`,
|
|
715
1680
|
expiresIn: 86400,
|
|
716
1681
|
}}
|
|
717
1682
|
alt={'image'}
|
|
@@ -719,24 +1684,10 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
|
|
|
719
1684
|
);
|
|
720
1685
|
}, [imageObject]);
|
|
721
1686
|
|
|
1687
|
+
// Update the message rendering to show images instantly without loaders
|
|
722
1688
|
const renderMessage = useCallback(
|
|
723
1689
|
(props: any) => {
|
|
724
|
-
//
|
|
725
|
-
// currentMessage: { text: currText },
|
|
726
|
-
// } = props;
|
|
727
|
-
|
|
728
|
-
//let messageTextStyle: any;
|
|
729
|
-
|
|
730
|
-
// Make "pure emoji" messages much bigger than plain text.
|
|
731
|
-
// if (currText && emojiUtils.isPureEmojiString(currText)) {
|
|
732
|
-
// messageTextStyle = {
|
|
733
|
-
// fontSize: 28,
|
|
734
|
-
// // Emoji get clipped if lineHeight isn't increased; make it consistent across platforms.
|
|
735
|
-
// lineHeight: Platform.OS === 'android' ? 34 : 30,
|
|
736
|
-
// }
|
|
737
|
-
// }
|
|
738
|
-
|
|
739
|
-
// return <SlackMessage {...props} messageTextStyle={messageTextStyle} />;
|
|
1690
|
+
// For all messages, use the SlackMessage component directly
|
|
740
1691
|
return (
|
|
741
1692
|
<SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
|
|
742
1693
|
);
|
|
@@ -744,170 +1695,525 @@ const ConversationViewComponent = ({ channelId: ChannelId, role, isShowThreadMes
|
|
|
744
1695
|
[isShowImageViewer],
|
|
745
1696
|
);
|
|
746
1697
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1698
|
+
const renderPaymentMessageText = useCallback(
|
|
1699
|
+
(props: any) => {
|
|
1700
|
+
const { currentMessage } = props;
|
|
1701
|
+
const lastReply: any =
|
|
1702
|
+
currentMessage?.replies?.data?.length > 0 ? currentMessage?.replies?.data?.[0] : null;
|
|
751
1703
|
|
|
752
|
-
|
|
1704
|
+
// Do not render anything if the message text is empty or only whitespace
|
|
1705
|
+
if (!currentMessage?.text || currentMessage.text.trim() === '') {
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
753
1708
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1709
|
+
// PAYMENT UI
|
|
1710
|
+
if (currentMessage.type === 'PAYMENT') {
|
|
1711
|
+
// Extract payment details if available
|
|
1712
|
+
const paymentDetails = currentMessage?.propsConfiguration?.payment || {};
|
|
1713
|
+
const amount = paymentDetails.amount || currentMessage?.amount;
|
|
1714
|
+
const currency = paymentDetails.currency || currentMessage?.currency || '$';
|
|
1715
|
+
const status = paymentDetails.status || currentMessage?.status || 'Completed';
|
|
1716
|
+
const paymentNote = paymentDetails.note || currentMessage?.text;
|
|
762
1717
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1718
|
+
return (
|
|
1719
|
+
<TouchableHighlight
|
|
1720
|
+
underlayColor={'#e0f7fa'}
|
|
1721
|
+
style={{ width: '100%' }}
|
|
1722
|
+
onPress={() => {
|
|
1723
|
+
if (currentMessage?.isShowThreadMessage)
|
|
1724
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
1725
|
+
channelId: channelId,
|
|
1726
|
+
title: 'Message',
|
|
1727
|
+
postParentId: currentMessage?._id,
|
|
1728
|
+
isPostParentIdThread: true,
|
|
1729
|
+
});
|
|
1730
|
+
}}
|
|
1731
|
+
>
|
|
1732
|
+
<Box
|
|
1733
|
+
style={{
|
|
1734
|
+
backgroundColor: '#e0f7fa',
|
|
1735
|
+
borderRadius: 16,
|
|
1736
|
+
padding: 12,
|
|
1737
|
+
marginVertical: 2,
|
|
1738
|
+
borderWidth: 1,
|
|
1739
|
+
borderColor: '#26c6da',
|
|
1740
|
+
flexDirection: 'row',
|
|
1741
|
+
alignItems: 'center',
|
|
1742
|
+
}}
|
|
1743
|
+
>
|
|
1744
|
+
<MaterialCommunityIcons
|
|
1745
|
+
name="credit-card-check"
|
|
1746
|
+
size={28}
|
|
1747
|
+
color="#00838f"
|
|
1748
|
+
style={{ marginRight: 10 }}
|
|
1749
|
+
/>
|
|
1750
|
+
<VStack style={{ flex: 1 }}>
|
|
1751
|
+
<Text style={{ fontWeight: 'bold', color: '#00838f', fontSize: 16 }}>
|
|
1752
|
+
{amount ? `${currency}${amount}` : 'Payment'}
|
|
1753
|
+
</Text>
|
|
1754
|
+
<Text style={{ color: '#00838f', fontSize: 13, marginTop: 2 }}>{status}</Text>
|
|
1755
|
+
{paymentNote && (
|
|
1756
|
+
<Text style={{ color: '#444', fontSize: 13, marginTop: 4 }}>{paymentNote}</Text>
|
|
1757
|
+
)}
|
|
1758
|
+
</VStack>
|
|
1759
|
+
</Box>
|
|
1760
|
+
</TouchableHighlight>
|
|
1761
|
+
);
|
|
1762
|
+
} else {
|
|
1763
|
+
// Default UI (unchanged)
|
|
1764
|
+
return (
|
|
1765
|
+
<TouchableHighlight
|
|
1766
|
+
underlayColor={'#c0c0c0'}
|
|
1767
|
+
style={{ width: '100%' }}
|
|
1768
|
+
onPress={() => {
|
|
1769
|
+
if (currentMessage?.isShowThreadMessage)
|
|
1770
|
+
navigation.navigate(config.THREAD_MESSEGE_PATH, {
|
|
1771
|
+
channelId: channelId,
|
|
1772
|
+
title: 'Message',
|
|
1773
|
+
postParentId: currentMessage?._id,
|
|
1774
|
+
isPostParentIdThread: true,
|
|
1775
|
+
});
|
|
1776
|
+
}}
|
|
1777
|
+
>
|
|
1778
|
+
<>
|
|
1779
|
+
<MessageText {...props} textStyle={{ left: { marginLeft: 5 } }} />
|
|
1780
|
+
{currentMessage?.replies?.data?.length > 0 && (
|
|
1781
|
+
<HStack space={'sm'} className="px-1 items-center">
|
|
1782
|
+
<HStack>
|
|
1783
|
+
{currentMessage?.replies?.data
|
|
1784
|
+
?.filter(
|
|
1785
|
+
(v: any, i: any, a: any) =>
|
|
1786
|
+
a.findIndex((t: any) => t?.author?.id === v?.author?.id) === i,
|
|
1787
|
+
)
|
|
1788
|
+
?.slice(0, 2)
|
|
1789
|
+
?.reverse()
|
|
1790
|
+
?.map((p: any, i: Number) => (
|
|
1791
|
+
<Avatar
|
|
1792
|
+
key={'conversation-replies-key-' + i}
|
|
1793
|
+
className="bg-transparent"
|
|
1794
|
+
size={'sm'}
|
|
1795
|
+
>
|
|
1796
|
+
<AvatarFallbackText>
|
|
1797
|
+
{startCase(p?.author?.username?.charAt(0))}
|
|
1798
|
+
</AvatarFallbackText>
|
|
1799
|
+
<AvatarImage
|
|
1800
|
+
alt="user image"
|
|
1801
|
+
style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
|
|
1802
|
+
source={{
|
|
1803
|
+
uri: p?.author?.picture,
|
|
1804
|
+
}}
|
|
1805
|
+
/>
|
|
1806
|
+
</Avatar>
|
|
1807
|
+
))}
|
|
1808
|
+
</HStack>
|
|
1809
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-blue-800">
|
|
1810
|
+
{currentMessage?.replies?.totalCount}{' '}
|
|
1811
|
+
{currentMessage?.replies?.totalCount == 1 ? 'reply' : 'replies'}
|
|
1812
|
+
</Text>
|
|
1813
|
+
<Text style={{ fontSize: 12 }} className="font-bold color-gray-500">
|
|
1814
|
+
{lastReply ? createdAtText(lastReply?.createdAt) : ''}
|
|
1815
|
+
</Text>
|
|
1816
|
+
</HStack>
|
|
1817
|
+
)}
|
|
1818
|
+
</>
|
|
1819
|
+
</TouchableHighlight>
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
},
|
|
1823
|
+
[navigation, channelId, role],
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
const renderPaymentMessage = useCallback(
|
|
1827
|
+
(props: any) => {
|
|
1828
|
+
const { currentMessage } = props;
|
|
1829
|
+
if (currentMessage?.type === 'PAYMENT') {
|
|
1830
|
+
return <PaymentMessage {...props} navigation={navigation} channelId={channelId} config={config} />;
|
|
1831
|
+
}
|
|
1832
|
+
// return <PaymentMessage
|
|
1833
|
+
// {...props}
|
|
1834
|
+
// navigation={navigation}
|
|
1835
|
+
// channelId={channelId}
|
|
1836
|
+
// config={config}
|
|
1837
|
+
// />;
|
|
1838
|
+
// Fallback to SlackMessage for other message types
|
|
1839
|
+
return (
|
|
1840
|
+
<SlackMessage {...props} isShowImageViewer={isShowImageViewer} setImageViewer={setImageViewerObject} />
|
|
1841
|
+
);
|
|
1842
|
+
},
|
|
1843
|
+
[isShowImageViewer, channelId, navigation],
|
|
1844
|
+
);
|
|
766
1845
|
|
|
767
1846
|
let onScroll = false;
|
|
768
1847
|
|
|
1848
|
+
// Optimize onMomentumScrollBegin for better scroll performance
|
|
769
1849
|
const onMomentumScrollBegin = async ({ nativeEvent }: any) => {
|
|
1850
|
+
// Set scroll state
|
|
770
1851
|
onScroll = true;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1852
|
+
|
|
1853
|
+
// Use the debounced fetch function to prevent excessive calls
|
|
1854
|
+
if (isCloseToTop(nativeEvent)) {
|
|
1855
|
+
onFetchOld();
|
|
774
1856
|
}
|
|
775
1857
|
};
|
|
776
1858
|
|
|
777
1859
|
const onEndReached = () => {
|
|
778
|
-
console.log('on end reached');
|
|
779
1860
|
if (!onScroll) return;
|
|
780
|
-
// load messages, show ActivityIndicator
|
|
781
1861
|
onScroll = false;
|
|
782
|
-
// setLoadingOldMessages(true);
|
|
783
1862
|
};
|
|
784
1863
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
listViewProps={{
|
|
795
|
-
onEndReached: onEndReached,
|
|
796
|
-
onEndReachedThreshold: 0.5,
|
|
797
|
-
onMomentumScrollBegin: onMomentumScrollBegin,
|
|
1864
|
+
// Add a loader for when more messages are being loaded
|
|
1865
|
+
const renderLoadEarlier = useCallback(() => {
|
|
1866
|
+
return loadingOldMessages ? (
|
|
1867
|
+
<View
|
|
1868
|
+
style={{
|
|
1869
|
+
padding: 10,
|
|
1870
|
+
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
1871
|
+
borderRadius: 10,
|
|
1872
|
+
marginTop: 10,
|
|
798
1873
|
}}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
...prev,
|
|
879
|
-
messages: {
|
|
880
|
-
...prev?.messages,
|
|
881
|
-
data: [...(prev?.messages?.data ?? []), newMessage],
|
|
882
|
-
totalCount: totalMsgCount,
|
|
883
|
-
},
|
|
884
|
-
};
|
|
885
|
-
return merged;
|
|
886
|
-
// return Object.assign({}, prev, {
|
|
887
|
-
// messages: {
|
|
888
|
-
// data: [...prev.messages.data, newMessage],
|
|
889
|
-
// totalCount: prev.messages.totalCount + 1,
|
|
890
|
-
// },
|
|
891
|
-
// });
|
|
892
|
-
},
|
|
893
|
-
})
|
|
894
|
-
}
|
|
1874
|
+
>
|
|
1875
|
+
<Spinner size="small" color="#3b82f6" />
|
|
1876
|
+
</View>
|
|
1877
|
+
) : null;
|
|
1878
|
+
}, [loadingOldMessages]);
|
|
1879
|
+
|
|
1880
|
+
// Add state for tracking input toolbar height
|
|
1881
|
+
const [inputToolbarHeight, setInputToolbarHeight] = useState(30);
|
|
1882
|
+
|
|
1883
|
+
// 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.
|
|
1884
|
+
const renderInputToolbar = useCallback(
|
|
1885
|
+
(props) => (
|
|
1886
|
+
<View style={{ backgroundColor: '#fff', paddingBottom: 4, paddingTop: 4 }}>
|
|
1887
|
+
<View
|
|
1888
|
+
style={{
|
|
1889
|
+
flexDirection: 'row',
|
|
1890
|
+
alignItems: 'center',
|
|
1891
|
+
minHeight: 44,
|
|
1892
|
+
maxHeight: 56,
|
|
1893
|
+
backgroundColor: '#fff',
|
|
1894
|
+
borderRadius: 22,
|
|
1895
|
+
marginHorizontal: 8,
|
|
1896
|
+
paddingHorizontal: 8,
|
|
1897
|
+
borderTopWidth: 1,
|
|
1898
|
+
borderTopColor: '#e0e0e0',
|
|
1899
|
+
}}
|
|
1900
|
+
>
|
|
1901
|
+
<TouchableOpacity
|
|
1902
|
+
onPress={onSelectImages}
|
|
1903
|
+
style={{
|
|
1904
|
+
width: 32,
|
|
1905
|
+
height: 32,
|
|
1906
|
+
borderRadius: 16,
|
|
1907
|
+
backgroundColor: '#fff',
|
|
1908
|
+
alignItems: 'center',
|
|
1909
|
+
justifyContent: 'center',
|
|
1910
|
+
marginRight: 8,
|
|
1911
|
+
}}
|
|
1912
|
+
>
|
|
1913
|
+
<MaterialIcons name="add" size={24} color="#888" />
|
|
1914
|
+
</TouchableOpacity>
|
|
1915
|
+
<TextInput
|
|
1916
|
+
ref={textInputRef}
|
|
1917
|
+
style={{
|
|
1918
|
+
flex: 1,
|
|
1919
|
+
//minHeight: 36,
|
|
1920
|
+
maxHeight: 44,
|
|
1921
|
+
backgroundColor: 'transparent',
|
|
1922
|
+
color: '#444',
|
|
1923
|
+
paddingHorizontal: 8,
|
|
1924
|
+
paddingVertical: 0,
|
|
1925
|
+
alignSelf: 'center',
|
|
1926
|
+
textAlignVertical: 'center',
|
|
1927
|
+
}}
|
|
1928
|
+
placeholder="Jot something down"
|
|
1929
|
+
placeholderTextColor={colors.gray[400]}
|
|
1930
|
+
multiline
|
|
1931
|
+
value={messageText}
|
|
1932
|
+
onChangeText={setMessageText}
|
|
1933
|
+
/>
|
|
1934
|
+
<TouchableOpacity
|
|
1935
|
+
onPress={() => handleSend([{ text: messageText }])}
|
|
1936
|
+
// disabled={(!messageText.trim() && images.length === 0) || isUploadingImage || loading}
|
|
1937
|
+
disabled={false}
|
|
1938
|
+
style={{
|
|
1939
|
+
marginLeft: 8,
|
|
1940
|
+
// opacity: (!messageText.trim() && images.length === 0) || isUploadingImage || loading ? 0.5 : 1,
|
|
1941
|
+
opacity: !messageText.trim() && images.length === 0 ? 0.5 : 1,
|
|
1942
|
+
}}
|
|
1943
|
+
>
|
|
1944
|
+
<MaterialCommunityIcons
|
|
1945
|
+
name="send-circle"
|
|
1946
|
+
size={32}
|
|
1947
|
+
color={!messageText.trim() && images.length === 0 ? colors.gray[400] : colors.blue[500]}
|
|
1948
|
+
// color={
|
|
1949
|
+
// (!messageText.trim() && images.length === 0) || isUploadingImage || loading
|
|
1950
|
+
// ? colors.gray[400]
|
|
1951
|
+
// : colors.blue[500]
|
|
1952
|
+
// }
|
|
895
1953
|
/>
|
|
896
|
-
|
|
1954
|
+
</TouchableOpacity>
|
|
1955
|
+
</View>
|
|
1956
|
+
{/* Selected Images Row */}
|
|
1957
|
+
{images && images.length > 0 && (
|
|
1958
|
+
<ScrollView
|
|
1959
|
+
horizontal
|
|
1960
|
+
showsHorizontalScrollIndicator={false}
|
|
1961
|
+
style={{ marginTop: 4, marginLeft: 8 }}
|
|
1962
|
+
>
|
|
1963
|
+
{images.map((img, index) => (
|
|
1964
|
+
<View
|
|
1965
|
+
key={`image-preview-${index}`}
|
|
1966
|
+
style={{
|
|
1967
|
+
width: 48,
|
|
1968
|
+
height: 48,
|
|
1969
|
+
marginRight: 8,
|
|
1970
|
+
borderRadius: 6,
|
|
1971
|
+
overflow: 'hidden',
|
|
1972
|
+
position: 'relative',
|
|
1973
|
+
backgroundColor: colors.gray[200],
|
|
1974
|
+
}}
|
|
1975
|
+
>
|
|
1976
|
+
<Image
|
|
1977
|
+
source={{ uri: img.uri || img.url }}
|
|
1978
|
+
style={{ width: '100%', height: '100%' }}
|
|
1979
|
+
alt={`selected image ${index + 1}`}
|
|
1980
|
+
/>
|
|
1981
|
+
<TouchableOpacity
|
|
1982
|
+
onPress={() => {
|
|
1983
|
+
handleRemoveImage(index);
|
|
1984
|
+
// handleRemoveImage already focuses input if needed
|
|
1985
|
+
}}
|
|
1986
|
+
style={{
|
|
1987
|
+
position: 'absolute',
|
|
1988
|
+
top: 2,
|
|
1989
|
+
right: 2,
|
|
1990
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
1991
|
+
borderRadius: 10,
|
|
1992
|
+
width: 20,
|
|
1993
|
+
height: 20,
|
|
1994
|
+
alignItems: 'center',
|
|
1995
|
+
justifyContent: 'center',
|
|
1996
|
+
}}
|
|
1997
|
+
>
|
|
1998
|
+
<Ionicons name="close" size={14} color="white" />
|
|
1999
|
+
</TouchableOpacity>
|
|
2000
|
+
</View>
|
|
2001
|
+
))}
|
|
2002
|
+
</ScrollView>
|
|
897
2003
|
)}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
2004
|
+
</View>
|
|
2005
|
+
),
|
|
2006
|
+
[onSelectImages, messageText, images, isUploadingImage, loading, handleSend, handleRemoveImage],
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
// Create a memoized ImageViewerModal component
|
|
2010
|
+
const imageViewerModal = useMemo(
|
|
2011
|
+
() => (
|
|
2012
|
+
<ImageViewerModal isVisible={isShowImageViewer} setVisible={setImageViewer} modalContent={modalContent} />
|
|
2013
|
+
),
|
|
2014
|
+
[isShowImageViewer, modalContent],
|
|
2015
|
+
);
|
|
2016
|
+
|
|
2017
|
+
// Create a memoized renderChatFooter function
|
|
2018
|
+
const renderChatFooter = useCallback(() => {
|
|
2019
|
+
return (
|
|
2020
|
+
<>
|
|
2021
|
+
{imageViewerModal}
|
|
2022
|
+
<SubscriptionHandler
|
|
2023
|
+
subscribeToMore={subscribe}
|
|
2024
|
+
document={CHAT_MESSAGE_ADDED}
|
|
2025
|
+
variables={{ channelId: channelId?.toString() }}
|
|
2026
|
+
updateQuery={undefined}
|
|
2027
|
+
/>
|
|
2028
|
+
</>
|
|
2029
|
+
);
|
|
2030
|
+
}, [imageViewerModal, subscribe]);
|
|
2031
|
+
|
|
2032
|
+
// Add optimized listViewProps to reduce re-renders and improve list performance
|
|
2033
|
+
const listViewProps = useMemo(
|
|
2034
|
+
() => ({
|
|
2035
|
+
onEndReached: onEndReached,
|
|
2036
|
+
onEndReachedThreshold: 0.5,
|
|
2037
|
+
onMomentumScrollBegin: onMomentumScrollBegin,
|
|
2038
|
+
removeClippedSubviews: true, // Improve performance by unmounting components when not visible
|
|
2039
|
+
initialNumToRender: 10, // Reduce initial render amount
|
|
2040
|
+
maxToRenderPerBatch: 7, // Reduce number in each render batch
|
|
2041
|
+
windowSize: 7, // Reduce the window size
|
|
2042
|
+
updateCellsBatchingPeriod: 50, // Batch cell updates to improve scrolling
|
|
2043
|
+
keyExtractor: (item) => item._id, // Add explicit key extractor
|
|
2044
|
+
}),
|
|
2045
|
+
[onEndReached, onMomentumScrollBegin],
|
|
905
2046
|
);
|
|
906
|
-
};
|
|
907
2047
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
2048
|
+
// Debug helper function to inspect files in messages
|
|
2049
|
+
const debugFileData = useCallback((message: any, prefix: string = 'Message') => {
|
|
2050
|
+
if (__DEV__) {
|
|
2051
|
+
console.log(
|
|
2052
|
+
`${prefix} ID: ${message?.id}, ` +
|
|
2053
|
+
`Has files object: ${!!message?.files}, ` +
|
|
2054
|
+
`Files typename: ${message?.files?.__typename}, ` +
|
|
2055
|
+
`Files data exists: ${!!message?.files?.data}, ` +
|
|
2056
|
+
`Files count: ${message?.files?.data?.length || 0}`,
|
|
2057
|
+
);
|
|
2058
|
+
|
|
2059
|
+
if (message?.files?.data && message?.files?.data?.length > 0) {
|
|
2060
|
+
const file = message?.files?.data[0];
|
|
2061
|
+
console.log(
|
|
2062
|
+
`File[0] ID: ${file?.id}, ` +
|
|
2063
|
+
`URL: ${file?.url?.substring(0, 30)}..., ` +
|
|
2064
|
+
`Name: ${file?.name}, ` +
|
|
2065
|
+
`Type: ${file?.mimeType}`,
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}, []);
|
|
2070
|
+
|
|
2071
|
+
// Return optimized component with performance improvements
|
|
2072
|
+
return (
|
|
2073
|
+
<KeyboardAvoidingView
|
|
2074
|
+
style={{ flex: 1, justifyContent: 'flex-end' }}
|
|
2075
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
2076
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
|
|
2077
|
+
>
|
|
2078
|
+
<View
|
|
2079
|
+
style={{
|
|
2080
|
+
flex: 1,
|
|
2081
|
+
backgroundColor: 'white',
|
|
2082
|
+
position: 'relative',
|
|
2083
|
+
marginBottom: images.length > 0 ? 5 : bottomMargin,
|
|
2084
|
+
}}
|
|
2085
|
+
>
|
|
2086
|
+
{errorMessage ? (
|
|
2087
|
+
<ErrorNotification
|
|
2088
|
+
message={errorMessage}
|
|
2089
|
+
onClose={() => setErrorMessage('')}
|
|
2090
|
+
type={notificationType}
|
|
2091
|
+
/>
|
|
2092
|
+
) : null}
|
|
2093
|
+
|
|
2094
|
+
{messageLoading && <Spinner color={'#3b82f6'} />}
|
|
2095
|
+
<GiftedChatInboxComponent
|
|
2096
|
+
ref={messageRootListRef}
|
|
2097
|
+
errorMessage={errorMessage}
|
|
2098
|
+
images={images}
|
|
2099
|
+
onSelectImages={onSelectImages}
|
|
2100
|
+
onRemoveImage={handleRemoveImage}
|
|
2101
|
+
selectedImage={selectedImage}
|
|
2102
|
+
setSelectedImage={setSelectedImage}
|
|
2103
|
+
isUploadingImage={isUploadingImage}
|
|
2104
|
+
loading={loading}
|
|
2105
|
+
wrapInSafeArea={true}
|
|
2106
|
+
renderLoading={() => <Spinner color={'#3b82f6'} />}
|
|
2107
|
+
messages={messageList}
|
|
2108
|
+
renderAvatar={null}
|
|
2109
|
+
showUserAvatar={false}
|
|
2110
|
+
listViewProps={{
|
|
2111
|
+
...listViewProps,
|
|
2112
|
+
contentContainerStyle: {
|
|
2113
|
+
paddingBottom: inputToolbarHeight,
|
|
2114
|
+
},
|
|
2115
|
+
}}
|
|
2116
|
+
onSend={handleSend}
|
|
2117
|
+
text={messageText || ' '}
|
|
2118
|
+
onInputTextChanged={(text) => {
|
|
2119
|
+
setMessageText(text);
|
|
2120
|
+
}}
|
|
2121
|
+
renderFooter={() => (isUploadingImage ? <Spinner color={'#3b82f6'} /> : null)}
|
|
2122
|
+
scrollToBottom
|
|
2123
|
+
user={{
|
|
2124
|
+
_id: auth?.id || '',
|
|
2125
|
+
}}
|
|
2126
|
+
renderSend={renderSend}
|
|
2127
|
+
renderMessageText={isPaymentChatUI ? renderPaymentMessageText : renderMessageText}
|
|
2128
|
+
renderMessage={isPaymentChatUI ? renderPaymentMessage : renderMessage}
|
|
2129
|
+
renderChatFooter={renderChatFooter}
|
|
2130
|
+
renderLoadEarlier={renderLoadEarlier}
|
|
2131
|
+
loadEarlier={totalCount > channelMessages.length}
|
|
2132
|
+
isLoadingEarlier={loadingOldMessages}
|
|
2133
|
+
placeholder="Jot something down"
|
|
2134
|
+
infiniteScroll={true}
|
|
2135
|
+
// renderChatEmpty={() => (
|
|
2136
|
+
// <><Text>Empty</Text>
|
|
2137
|
+
// {!loading && messageList && messageList?.length == 0 && (
|
|
2138
|
+
// <Box className="p-5">
|
|
2139
|
+
// <Center className="mt-6">
|
|
2140
|
+
// <Ionicons name="chatbubbles" size={30} />
|
|
2141
|
+
// <Text>You don't have any message yet!</Text>
|
|
2142
|
+
// </Center>
|
|
2143
|
+
// </Box>
|
|
2144
|
+
// )}
|
|
2145
|
+
// </>
|
|
2146
|
+
// )}
|
|
2147
|
+
/>
|
|
2148
|
+
|
|
2149
|
+
{/* <GiftedChat
|
|
2150
|
+
ref={messageRootListRef}
|
|
2151
|
+
wrapInSafeArea={true}
|
|
2152
|
+
renderLoading={() => <Spinner color={'#3b82f6'} />}
|
|
2153
|
+
messages={messageList}
|
|
2154
|
+
renderAvatar={null}
|
|
2155
|
+
showUserAvatar={false}
|
|
2156
|
+
listViewProps={{
|
|
2157
|
+
...listViewProps,
|
|
2158
|
+
contentContainerStyle: {
|
|
2159
|
+
paddingBottom: inputToolbarHeight,
|
|
2160
|
+
},
|
|
2161
|
+
}}
|
|
2162
|
+
onSend={handleSend}
|
|
2163
|
+
text={messageText || ' '}
|
|
2164
|
+
onInputTextChanged={(text) => {
|
|
2165
|
+
setMessageText(text);
|
|
2166
|
+
}}
|
|
2167
|
+
renderFooter={() => (isUploadingImage ? <Spinner color={'#3b82f6'} /> : null)}
|
|
2168
|
+
scrollToBottom
|
|
2169
|
+
user={{
|
|
2170
|
+
_id: auth?.id || '',
|
|
2171
|
+
}}
|
|
2172
|
+
isTyping={false}
|
|
2173
|
+
alwaysShowSend={true}
|
|
2174
|
+
renderSend={renderSend}
|
|
2175
|
+
renderMessageText={renderMessageText}
|
|
2176
|
+
renderInputToolbar={renderInputToolbar}
|
|
2177
|
+
// renderComposer={renderComposer}
|
|
2178
|
+
// minInputToolbarHeight={isActionSheetVisible ? 0 : 56}
|
|
2179
|
+
minInputToolbarHeight={inputToolbarHeight}
|
|
2180
|
+
renderActions={null}
|
|
2181
|
+
renderMessage={renderMessage}
|
|
2182
|
+
renderChatFooter={renderChatFooter}
|
|
2183
|
+
renderLoadEarlier={renderLoadEarlier}
|
|
2184
|
+
loadEarlier={totalCount > channelMessages.length}
|
|
2185
|
+
isLoadingEarlier={loadingOldMessages}
|
|
2186
|
+
bottomOffset={0}
|
|
2187
|
+
isKeyboardInternallyHandled={false}
|
|
2188
|
+
textInputProps={{
|
|
2189
|
+
multiline: true,
|
|
2190
|
+
returnKeyType: 'default',
|
|
2191
|
+
enablesReturnKeyAutomatically: true,
|
|
2192
|
+
placeholderTextColor: colors.gray[400],
|
|
2193
|
+
}}
|
|
2194
|
+
minComposerHeight={36}
|
|
2195
|
+
maxComposerHeight={100}
|
|
2196
|
+
placeholder="Jot something down"
|
|
2197
|
+
lightboxProps={{
|
|
2198
|
+
underlayColor: 'transparent',
|
|
2199
|
+
springConfig: { tension: 90000, friction: 90000 },
|
|
2200
|
+
disabled: true,
|
|
2201
|
+
}}
|
|
2202
|
+
infiniteScroll={false}
|
|
2203
|
+
renderAccessory={selectedImage ? renderAccessory : null}
|
|
2204
|
+
/> */}
|
|
2205
|
+
</View>
|
|
2206
|
+
</KeyboardAvoidingView>
|
|
2207
|
+
);
|
|
911
2208
|
};
|
|
912
2209
|
|
|
913
|
-
|
|
2210
|
+
// Export with React.memo to prevent unnecessary re-renders
|
|
2211
|
+
export const ConversationView = React.memo(ConversationViewComponent, (prevProps, nextProps) => {
|
|
2212
|
+
// Only re-render if these critical props change
|
|
2213
|
+
return (
|
|
2214
|
+
prevProps.channelId === nextProps.channelId &&
|
|
2215
|
+
prevProps.role === nextProps.role &&
|
|
2216
|
+
prevProps.isShowThreadMessage === nextProps.isShowThreadMessage &&
|
|
2217
|
+
prevProps.isPaymentChatUI === nextProps.isPaymentChatUI
|
|
2218
|
+
);
|
|
2219
|
+
});
|