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