@droppii-org/chat-mobile 0.2.23 → 0.2.24
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/lib/module/config/uploadLimits.js +13 -0
- package/lib/module/config/uploadLimits.js.map +1 -0
- package/lib/module/hooks/useChatCompose.js +142 -0
- package/lib/module/hooks/useChatCompose.js.map +1 -0
- package/lib/module/hooks/useImageAttachment.js +71 -151
- package/lib/module/hooks/useImageAttachment.js.map +1 -1
- package/lib/module/hooks/useSendAttachment.js +2 -2
- package/lib/module/hooks/useSendAttachment.js.map +1 -1
- package/lib/module/hooks/useVideoAttachment.js +4 -4
- package/lib/module/hooks/useVideoAttachment.js.map +1 -1
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +50 -185
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +16 -137
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/index.js +0 -1
- package/lib/module/screens/chat-detail/index.js.map +1 -1
- package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js +2 -2
- package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js.map +1 -1
- package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js +3 -3
- package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js.map +1 -1
- package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js +2 -1
- package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js.map +1 -1
- package/lib/module/services/attachmentOrchestrator.js +6 -5
- package/lib/module/services/attachmentOrchestrator.js.map +1 -1
- package/lib/module/utils/imageUtils.js +3 -3
- package/lib/module/utils/imageUtils.js.map +1 -1
- package/lib/module/utils/pLimit.js +19 -0
- package/lib/module/utils/pLimit.js.map +1 -0
- package/lib/module/utils/videoThumbnail.js +8 -3
- package/lib/module/utils/videoThumbnail.js.map +1 -1
- package/lib/module/utils/videoThumbnailUpload.js +76 -0
- package/lib/module/utils/videoThumbnailUpload.js.map +1 -0
- package/lib/typescript/src/config/uploadLimits.d.ts +7 -0
- package/lib/typescript/src/config/uploadLimits.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useChatCompose.d.ts +27 -0
- package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useImageAttachment.d.ts +2 -2
- package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useSendAttachment.d.ts +1 -1
- package/lib/typescript/src/hooks/useSendAttachment.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useVideoAttachment.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +0 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +5 -2
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/index.d.ts +0 -2
- package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts +1 -35
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/imageAttachmentHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/videoAttachmentHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentOrchestrator.d.ts +1 -1
- package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
- package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
- package/lib/typescript/src/utils/pLimit.d.ts +6 -0
- package/lib/typescript/src/utils/pLimit.d.ts.map +1 -0
- package/lib/typescript/src/utils/videoThumbnail.d.ts.map +1 -1
- package/lib/typescript/src/utils/videoThumbnailUpload.d.ts +11 -0
- package/lib/typescript/src/utils/videoThumbnailUpload.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/config/uploadLimits.ts +11 -0
- package/src/hooks/useChatCompose.ts +186 -0
- package/src/hooks/useImageAttachment.ts +110 -197
- package/src/hooks/useSendAttachment.ts +3 -6
- package/src/hooks/useVideoAttachment.ts +4 -4
- package/src/index.tsx +1 -5
- package/src/screens/chat-detail/ChatComposer.tsx +88 -240
- package/src/screens/chat-detail/ChatDetail.tsx +24 -195
- package/src/screens/chat-detail/index.ts +1 -5
- package/src/screens/chat-detail/types.ts +1 -38
- package/src/services/attachmentHandlers/fileAttachmentHandler.ts +3 -2
- package/src/services/attachmentHandlers/imageAttachmentHandler.ts +6 -3
- package/src/services/attachmentHandlers/videoAttachmentHandler.ts +2 -1
- package/src/services/attachmentOrchestrator.ts +6 -8
- package/src/utils/imageUtils.ts +3 -4
- package/src/utils/pLimit.ts +20 -0
- package/src/utils/videoThumbnail.ts +10 -3
- package/src/utils/videoThumbnailUpload.ts +103 -0
- package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js +0 -288
- package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +0 -1
- package/lib/module/utils/giftedChatMessage.js +0 -90
- package/lib/module/utils/giftedChatMessage.js.map +0 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts +0 -26
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +0 -1
- package/lib/typescript/src/utils/giftedChatMessage.d.ts +0 -10
- package/lib/typescript/src/utils/giftedChatMessage.d.ts.map +0 -1
- package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +0 -386
- package/src/utils/giftedChatMessage.ts +0 -137
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo,
|
|
1
|
+
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { Platform, StyleSheet } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
|
|
@@ -14,24 +14,13 @@ import { useConversation, useConversationStore } from '../../store';
|
|
|
14
14
|
import ChatDetailHeader from './ChatDetailHeader';
|
|
15
15
|
import { ChatListLegend } from './ChatListLegend';
|
|
16
16
|
import { ChatComposer } from './ChatComposer';
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_ATTACHMENT_ACTIONS,
|
|
19
|
-
DEFAULT_Chat_QUICK_ACTIONS,
|
|
20
|
-
} from './constants';
|
|
21
17
|
import {
|
|
22
18
|
getConversationAvatarUri,
|
|
23
19
|
getConversationSubtitle,
|
|
24
20
|
getConversationTitle,
|
|
25
21
|
shouldShowAddMember,
|
|
26
22
|
} from './conversationHeader.utils';
|
|
27
|
-
import type {
|
|
28
|
-
ChatDetailProps,
|
|
29
|
-
DChatQuickAction,
|
|
30
|
-
DChatAttachmentAction,
|
|
31
|
-
} from './types';
|
|
32
|
-
import { useSendMessage } from '../../hooks/message/useSendMessage';
|
|
33
|
-
import type { IUrlMetadata } from '../../types/common';
|
|
34
|
-
import { useMessageStore } from '../../store/message';
|
|
23
|
+
import type { ChatDetailProps } from './types';
|
|
35
24
|
import { ChatDetailProvider } from '../../context/ChatDetailContext';
|
|
36
25
|
import { normalizeUrl } from '../../utils/url';
|
|
37
26
|
|
|
@@ -50,58 +39,27 @@ const ChatDetail = memo(
|
|
|
50
39
|
onPressAddMember,
|
|
51
40
|
onPressMenu,
|
|
52
41
|
onPressAvatar,
|
|
53
|
-
showQuickActions = true,
|
|
54
|
-
quickActions,
|
|
55
|
-
inputPlaceholder,
|
|
56
|
-
onChangeInput,
|
|
57
|
-
onPressAttach,
|
|
58
|
-
onPressEmoji,
|
|
59
|
-
attachmentActions,
|
|
60
|
-
onQuickActionPress,
|
|
61
|
-
onAttachmentAction,
|
|
62
|
-
renderQuickAction,
|
|
63
|
-
renderQuickActions,
|
|
64
|
-
renderAttachmentAction,
|
|
65
|
-
attachmentColumns,
|
|
66
42
|
onPressUrl,
|
|
67
43
|
customMessageItemFactory,
|
|
68
44
|
}: ChatDetailProps) => {
|
|
69
45
|
const navigation = useNavigation();
|
|
70
|
-
const [internalInput, setInternalInput] = useState<string>('');
|
|
71
46
|
const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
|
|
72
47
|
const conversation = useConversation(conversationId);
|
|
73
|
-
// const user = conversation?.peer;
|
|
74
48
|
|
|
75
|
-
// Default handlers when props not provided
|
|
76
49
|
const handleBackDefault = useCallback(() => {
|
|
77
50
|
navigation.goBack();
|
|
78
51
|
}, [navigation]);
|
|
79
52
|
|
|
80
|
-
const
|
|
81
|
-
(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
})),
|
|
88
|
-
[]
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const attachmentActionsDefault = useMemo<DChatAttachmentAction[]>(
|
|
92
|
-
() =>
|
|
93
|
-
DEFAULT_ATTACHMENT_ACTIONS.map((action) => ({
|
|
94
|
-
...action,
|
|
95
|
-
onPress: () => {
|
|
96
|
-
console.log('[attachment-action]', action.id);
|
|
97
|
-
},
|
|
98
|
-
})),
|
|
53
|
+
const handleMediaPress = useCallback(
|
|
54
|
+
(
|
|
55
|
+
items: Parameters<MediaViewerModalHandle['show']>[0],
|
|
56
|
+
index?: number
|
|
57
|
+
) => {
|
|
58
|
+
mediaViewerRef.current?.show(items, index);
|
|
59
|
+
},
|
|
99
60
|
[]
|
|
100
61
|
);
|
|
101
62
|
|
|
102
|
-
const { sendMessage } = useSendMessage();
|
|
103
|
-
const { pushNewMessage } = useMessageStore();
|
|
104
|
-
|
|
105
63
|
const {
|
|
106
64
|
messages,
|
|
107
65
|
currentUserId,
|
|
@@ -109,25 +67,7 @@ const ChatDetail = memo(
|
|
|
109
67
|
isLoading,
|
|
110
68
|
isLoadingEarlier,
|
|
111
69
|
hasMoreEarlier,
|
|
112
|
-
} = useChatMessages({
|
|
113
|
-
conversationId,
|
|
114
|
-
enabled,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Callback for attachment messages - add them to the message list
|
|
118
|
-
const handleAttachmentMessageSent = useCallback(
|
|
119
|
-
(message: any) => {
|
|
120
|
-
if (__DEV__)
|
|
121
|
-
console.log('[ChatDetail] Attachment message received:', {
|
|
122
|
-
clientMsgID: message?.clientMsgID,
|
|
123
|
-
contentType: message?.contentType,
|
|
124
|
-
});
|
|
125
|
-
if (message) {
|
|
126
|
-
pushNewMessage(message);
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
[pushNewMessage]
|
|
130
|
-
);
|
|
70
|
+
} = useChatMessages({ conversationId, enabled });
|
|
131
71
|
|
|
132
72
|
useEffect(() => {
|
|
133
73
|
useConversationStore.getState().setCurrentConversation(conversationId);
|
|
@@ -140,84 +80,18 @@ const ChatDetail = memo(
|
|
|
140
80
|
const resolvedShowAddMember =
|
|
141
81
|
showAddMember ?? shouldShowAddMember(conversation);
|
|
142
82
|
const resolvedSubtitle = useMemo(() => {
|
|
143
|
-
if (subtitle !== undefined)
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (getSubtitle && conversation) {
|
|
148
|
-
return getSubtitle(conversation);
|
|
149
|
-
}
|
|
150
|
-
|
|
83
|
+
if (subtitle !== undefined) return subtitle;
|
|
84
|
+
if (getSubtitle && conversation) return getSubtitle(conversation);
|
|
151
85
|
return getConversationSubtitle(conversation);
|
|
152
86
|
}, [conversation, getSubtitle, subtitle]);
|
|
153
87
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
const handleChangeText = useCallback(
|
|
157
|
-
(text?: string) => {
|
|
158
|
-
const newText = text ?? '';
|
|
159
|
-
if (onChangeInput) {
|
|
160
|
-
onChangeInput(newText);
|
|
161
|
-
} else {
|
|
162
|
-
setInternalInput(newText);
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
[onChangeInput]
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
const handleSend = useCallback(
|
|
169
|
-
async (urlMetadata?: any) => {
|
|
170
|
-
// Support captured text passed from attachment handler
|
|
171
|
-
const capturedText = (urlMetadata as any)?._capturedText;
|
|
172
|
-
const cleanMetadata = capturedText
|
|
173
|
-
? (({ _capturedText, ...rest }) => rest)(urlMetadata)
|
|
174
|
-
: (urlMetadata as IUrlMetadata);
|
|
175
|
-
|
|
176
|
-
const text = (capturedText || currentInput).trim();
|
|
177
|
-
|
|
178
|
-
// Validate: must have text OR preview metadata (text is optional if preview exists)
|
|
179
|
-
// If metadata exists, trust it (useLinkPreview is responsible for validity)
|
|
180
|
-
if (!text && !cleanMetadata) {
|
|
181
|
-
if (__DEV__)
|
|
182
|
-
console.warn('[ChatDetail] No text or preview link to send');
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Log if metadata seems incomplete
|
|
187
|
-
if (cleanMetadata && !cleanMetadata.url) {
|
|
188
|
-
if (__DEV__)
|
|
189
|
-
console.warn(
|
|
190
|
-
'[ChatDetail] Preview metadata missing URL - check useLinkPreview'
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
await sendMessage({
|
|
196
|
-
files: [],
|
|
197
|
-
plainText: text,
|
|
198
|
-
urlMetadata: cleanMetadata || undefined,
|
|
199
|
-
});
|
|
88
|
+
const insets = useSafeAreaInsets();
|
|
200
89
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
} else {
|
|
205
|
-
setInternalInput('');
|
|
206
|
-
}
|
|
207
|
-
} catch (error) {
|
|
208
|
-
if (__DEV__)
|
|
209
|
-
console.error('[ChatDetail] Failed to send message:', {
|
|
210
|
-
error,
|
|
211
|
-
text: text.substring(0, 50),
|
|
212
|
-
hasPreview: !!cleanMetadata,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
[currentInput, onChangeInput, sendMessage]
|
|
90
|
+
const keyboardVerticalOffset = useMemo(
|
|
91
|
+
() => Platform.select({ android: insets.bottom, default: 0 }),
|
|
92
|
+
[insets.bottom]
|
|
217
93
|
);
|
|
218
94
|
|
|
219
|
-
const insets = useSafeAreaInsets();
|
|
220
|
-
|
|
221
95
|
const handlePressUrl = useCallback(
|
|
222
96
|
(url: string) => {
|
|
223
97
|
onPressUrl?.(normalizeUrl(url));
|
|
@@ -236,25 +110,16 @@ const ChatDetail = memo(
|
|
|
236
110
|
avatarFullName={resolvedAvatarFullName}
|
|
237
111
|
showAddMember={resolvedShowAddMember}
|
|
238
112
|
onBack={onBack ?? handleBackDefault}
|
|
239
|
-
onPressSearch={
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
onPressAddMember ?? (() => console.log('[onPressAddMember]'))
|
|
244
|
-
}
|
|
245
|
-
onPressMenu={onPressMenu ?? (() => console.log('[onPressMenu]'))}
|
|
246
|
-
onPressAvatar={
|
|
247
|
-
onPressAvatar ?? (() => console.log('[onPressAvatar]'))
|
|
248
|
-
}
|
|
113
|
+
onPressSearch={onPressSearch}
|
|
114
|
+
onPressAddMember={onPressAddMember}
|
|
115
|
+
onPressMenu={onPressMenu}
|
|
116
|
+
onPressAvatar={onPressAvatar}
|
|
249
117
|
/>
|
|
250
118
|
|
|
251
119
|
<KeyboardAvoidingView
|
|
252
120
|
style={styles.keyboardAvoiding}
|
|
253
121
|
behavior="padding"
|
|
254
|
-
keyboardVerticalOffset={
|
|
255
|
-
android: insets.bottom,
|
|
256
|
-
default: 0,
|
|
257
|
-
})}
|
|
122
|
+
keyboardVerticalOffset={keyboardVerticalOffset}
|
|
258
123
|
>
|
|
259
124
|
<ChatListLegend
|
|
260
125
|
messages={messages}
|
|
@@ -264,45 +129,11 @@ const ChatDetail = memo(
|
|
|
264
129
|
isLoading={isLoading}
|
|
265
130
|
isLoadingEarlier={isLoadingEarlier}
|
|
266
131
|
hasMoreEarlier={hasMoreEarlier}
|
|
267
|
-
onMediaPress={
|
|
268
|
-
mediaViewerRef.current?.show(items, index);
|
|
269
|
-
}}
|
|
132
|
+
onMediaPress={handleMediaPress}
|
|
270
133
|
onPressUrl={handlePressUrl}
|
|
271
134
|
/>
|
|
272
135
|
|
|
273
|
-
<ChatComposer
|
|
274
|
-
value={currentInput}
|
|
275
|
-
placeholder={inputPlaceholder}
|
|
276
|
-
onChangeText={handleChangeText}
|
|
277
|
-
onSend={handleSend}
|
|
278
|
-
onMessageSent={handleAttachmentMessageSent}
|
|
279
|
-
onPressAttach={
|
|
280
|
-
onPressAttach ?? (() => console.log('[onPressAttach]'))
|
|
281
|
-
}
|
|
282
|
-
onPressEmoji={
|
|
283
|
-
onPressEmoji ?? (() => console.log('[onPressEmoji]'))
|
|
284
|
-
}
|
|
285
|
-
showQuickActions={showQuickActions}
|
|
286
|
-
quickActions={quickActions ?? quickActionsDefault}
|
|
287
|
-
attachmentActions={attachmentActions ?? attachmentActionsDefault}
|
|
288
|
-
onQuickActionPress={
|
|
289
|
-
onQuickActionPress ??
|
|
290
|
-
((action: DChatQuickAction) => {
|
|
291
|
-
console.log('[onQuickActionPress]', action.id, action.meta);
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
onAttachmentAction={
|
|
295
|
-
onAttachmentAction ??
|
|
296
|
-
((action: DChatAttachmentAction) => {
|
|
297
|
-
console.log('[onAttachmentAction]', action.id, action.meta);
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
renderQuickAction={renderQuickAction}
|
|
301
|
-
renderQuickActions={renderQuickActions}
|
|
302
|
-
renderAttachmentAction={renderAttachmentAction}
|
|
303
|
-
attachmentColumns={attachmentColumns}
|
|
304
|
-
isEmptyMessage={messages?.length === 0}
|
|
305
|
-
/>
|
|
136
|
+
<ChatComposer isEmptyMessage={messages?.length === 0} />
|
|
306
137
|
</KeyboardAvoidingView>
|
|
307
138
|
|
|
308
139
|
<MediaViewerModal ref={mediaViewerRef} />
|
|
@@ -315,9 +146,7 @@ const ChatDetail = memo(
|
|
|
315
146
|
ChatDetail.displayName = 'ChatDetail';
|
|
316
147
|
|
|
317
148
|
const styles = StyleSheet.create({
|
|
318
|
-
keyboardAvoiding: {
|
|
319
|
-
flex: 1,
|
|
320
|
-
},
|
|
149
|
+
keyboardAvoiding: { flex: 1 },
|
|
321
150
|
});
|
|
322
151
|
|
|
323
152
|
export default ChatDetail;
|
|
@@ -27,12 +27,8 @@ export type {
|
|
|
27
27
|
ChatQuickActionsRenderParams,
|
|
28
28
|
ChatAttachmentPanelProps,
|
|
29
29
|
} from './types';
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
export {
|
|
32
32
|
DChatMessageType,
|
|
33
33
|
CUSTOM_MESSAGE_DATA_TYPE,
|
|
34
34
|
} from '../../types/message';
|
|
35
|
-
export {
|
|
36
|
-
mapOpenIMMessageToGiftedChat,
|
|
37
|
-
mapOpenIMMessagesToGiftedChat,
|
|
38
|
-
} from '../../utils/giftedChatMessage';
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
|
|
3
2
|
import type { MediaItem } from '../../hooks/useMediaViewer';
|
|
4
3
|
import type { DMessageItem } from '../../types/chat';
|
|
5
|
-
import type { IUrlMetadata } from '../../types/common';
|
|
6
4
|
import type { ConversationItem } from '@droppii/openim-rn-client-sdk';
|
|
7
5
|
|
|
8
6
|
export type DChatActionIconProvider = 'MaterialCommunityIcons' | 'DroppiiNew';
|
|
@@ -58,23 +56,7 @@ export interface ChatAttachmentPanelProps {
|
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
export interface ChatComposerProps {
|
|
61
|
-
|
|
62
|
-
placeholder?: string;
|
|
63
|
-
onChangeText?: (text?: string) => void;
|
|
64
|
-
onSend?: (urlMetadata?: IUrlMetadata) => void;
|
|
65
|
-
onPressAttach?: () => void;
|
|
66
|
-
onPressEmoji?: () => void;
|
|
67
|
-
onMessageSent?: (message: DMessageItem) => void;
|
|
68
|
-
showQuickActions?: boolean;
|
|
69
|
-
quickActions?: DChatQuickAction[];
|
|
70
|
-
attachmentActions?: DChatAttachmentAction[];
|
|
71
|
-
onQuickActionPress?: (action: DChatQuickAction) => void;
|
|
72
|
-
onAttachmentAction?: (action: DChatAttachmentAction) => void;
|
|
73
|
-
renderQuickAction?: ChatQuickActionsProps['renderAction'];
|
|
74
|
-
renderQuickActions?: ChatQuickActionsProps['renderQuickActions'];
|
|
75
|
-
renderAttachmentAction?: ChatAttachmentPanelProps['renderAction'];
|
|
76
|
-
attachmentColumns?: number;
|
|
77
|
-
isEmptyMessage: boolean;
|
|
59
|
+
isEmptyMessage?: boolean;
|
|
78
60
|
}
|
|
79
61
|
|
|
80
62
|
export interface ChatListProps {
|
|
@@ -110,25 +92,6 @@ export interface ChatDetailProps extends Omit<
|
|
|
110
92
|
avatarFullName?: string;
|
|
111
93
|
showAddMember?: boolean;
|
|
112
94
|
getSubtitle?: (conversation: ConversationItem) => string | undefined;
|
|
113
|
-
renderChat?: (item: DMessageItem) => ReactNode;
|
|
114
|
-
showQuickActions?: boolean;
|
|
115
|
-
showAttachmentActions?: boolean;
|
|
116
|
-
quickActions?: DChatQuickAction[];
|
|
117
|
-
getQuickActions?: (
|
|
118
|
-
conversation: ConversationItem
|
|
119
|
-
) => DChatQuickAction[] | undefined;
|
|
120
|
-
attachmentActions?: DChatAttachmentAction[];
|
|
121
|
-
inputPlaceholder?: string;
|
|
122
|
-
onChangeInput?: (text: string) => void;
|
|
123
|
-
onSend?: () => void;
|
|
124
|
-
onPressAttach?: () => void;
|
|
125
|
-
onPressEmoji?: () => void;
|
|
126
|
-
onQuickActionPress?: (action: DChatQuickAction) => void;
|
|
127
|
-
onAttachmentAction?: (action: DChatAttachmentAction) => void;
|
|
128
|
-
renderQuickAction?: ChatQuickActionsProps['renderAction'];
|
|
129
|
-
renderQuickActions?: ChatQuickActionsProps['renderQuickActions'];
|
|
130
|
-
renderAttachmentAction?: ChatAttachmentPanelProps['renderAction'];
|
|
131
|
-
attachmentColumns?: number;
|
|
132
95
|
onPressUrl?: (url: string) => void;
|
|
133
96
|
customMessageItemFactory?: (item: DMessageItem) => ReactNode;
|
|
134
97
|
}
|
|
@@ -9,8 +9,9 @@ interface FileElem {
|
|
|
9
9
|
sourceUrl: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import { UPLOAD_LIMITS } from '../../config/uploadLimits';
|
|
13
|
+
|
|
14
|
+
export const MAX_FILE_SIZE = UPLOAD_LIMITS.file;
|
|
14
15
|
|
|
15
16
|
export class FileAttachmentHandler implements IAttachmentHandler {
|
|
16
17
|
validate(
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { ImageAttachment } from '../../types/attachment';
|
|
2
2
|
import type { IAttachmentHandler } from '../../types/attachmentHandler';
|
|
3
3
|
import { generateUUID } from '../../utils/imageUtils';
|
|
4
|
+
import { UPLOAD_LIMITS } from '../../config/uploadLimits';
|
|
4
5
|
|
|
5
6
|
export const ImageAttachmentHandler: IAttachmentHandler = {
|
|
6
7
|
validate(attachment: ImageAttachment) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
if (attachment.sourceFile.size > UPLOAD_LIMITS.image) {
|
|
9
|
+
return {
|
|
10
|
+
valid: false,
|
|
11
|
+
reason: `Image exceeds ${UPLOAD_LIMITS.image / 1024 / 1024}MB limit`,
|
|
12
|
+
};
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
const validMimes = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Attachment, VideoAttachment } from '../../types/attachment';
|
|
2
2
|
import type { IAttachmentHandler } from '../../types/attachmentHandler';
|
|
3
|
+
import { UPLOAD_LIMITS } from '../../config/uploadLimits';
|
|
3
4
|
|
|
4
5
|
interface VideoPicInfo {
|
|
5
6
|
videoUUID: string;
|
|
@@ -18,7 +19,7 @@ interface VideoPicInfo {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export class VideoAttachmentHandler implements IAttachmentHandler {
|
|
21
|
-
private static readonly MAX_VIDEO_SIZE =
|
|
22
|
+
private static readonly MAX_VIDEO_SIZE = UPLOAD_LIMITS.video;
|
|
22
23
|
private static readonly SUPPORTED_TYPES = ['video/mp4', 'video/quicktime'];
|
|
23
24
|
|
|
24
25
|
validate(
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '../types/attachment';
|
|
7
7
|
import { getAttachmentHandler } from '../types/attachmentHandler';
|
|
8
8
|
import { useConversationStore } from '../store';
|
|
9
|
+
import { useMessageStore } from '../store/message';
|
|
9
10
|
import {
|
|
10
11
|
getAttachmentPriorityConfig,
|
|
11
12
|
type AttachmentType,
|
|
@@ -15,8 +16,7 @@ export namespace AttachmentOrchestratorService {
|
|
|
15
16
|
export async function sendPreparedAttachments(
|
|
16
17
|
attachments: Attachment[],
|
|
17
18
|
uploadedUrls: Record<string, string>,
|
|
18
|
-
options: AttachmentSendOptions = { strategy: 'individual' }
|
|
19
|
-
onMessageSent?: (message: any) => void
|
|
19
|
+
options: AttachmentSendOptions = { strategy: 'individual' }
|
|
20
20
|
): Promise<AttachmentSendResult> {
|
|
21
21
|
const result: AttachmentSendResult = {
|
|
22
22
|
success: false,
|
|
@@ -90,8 +90,7 @@ export namespace AttachmentOrchestratorService {
|
|
|
90
90
|
try {
|
|
91
91
|
const mergedMsgId = await sendMergedMessage(
|
|
92
92
|
payloads.map((p) => p.payload),
|
|
93
|
-
options.includeCaption
|
|
94
|
-
onMessageSent
|
|
93
|
+
options.includeCaption
|
|
95
94
|
);
|
|
96
95
|
console.log('[AttachmentOrchestrator] Messages sent via merger:', {
|
|
97
96
|
messageId: mergedMsgId,
|
|
@@ -116,8 +115,7 @@ export namespace AttachmentOrchestratorService {
|
|
|
116
115
|
|
|
117
116
|
async function sendMergedMessage(
|
|
118
117
|
payloads: unknown[],
|
|
119
|
-
_caption?: string
|
|
120
|
-
onMessageSent?: (message: any) => void
|
|
118
|
+
_caption?: string
|
|
121
119
|
): Promise<string> {
|
|
122
120
|
const currentConversation = useConversationStore
|
|
123
121
|
.getState()
|
|
@@ -290,8 +288,8 @@ export namespace AttachmentOrchestratorService {
|
|
|
290
288
|
index: i + 1,
|
|
291
289
|
});
|
|
292
290
|
|
|
293
|
-
if (
|
|
294
|
-
|
|
291
|
+
if (result) {
|
|
292
|
+
useMessageStore.getState().pushNewMessage(result);
|
|
295
293
|
}
|
|
296
294
|
}
|
|
297
295
|
|
package/src/utils/imageUtils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ImageFile } from '../types/imageUpload';
|
|
2
2
|
import Images from '../assets/images';
|
|
3
|
+
import { UPLOAD_LIMITS } from '../config/uploadLimits';
|
|
3
4
|
|
|
4
5
|
export const generateUUID = (): string => {
|
|
5
6
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
@@ -81,10 +82,8 @@ export const formatFileSize = (bytes: number): string => {
|
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
export const validateImageFile = (file: ImageFile): string | null => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (file.size > MAX_SIZE) {
|
|
87
|
-
return `Image too large. Max ${formatFileSize(MAX_SIZE)}`;
|
|
85
|
+
if (file.size > UPLOAD_LIMITS.image) {
|
|
86
|
+
return `Image too large. Max ${formatFileSize(UPLOAD_LIMITS.image)}`;
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
const validMimes = [
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `items` through `fn` with at most `concurrency` items in-flight at once.
|
|
3
|
+
* Errors per-item are swallowed — wrap `fn` if you need to capture them.
|
|
4
|
+
*/
|
|
5
|
+
export async function pLimit<T>(
|
|
6
|
+
items: T[],
|
|
7
|
+
fn: (item: T, index: number) => Promise<void>,
|
|
8
|
+
concurrency: number
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
let idx = 0;
|
|
11
|
+
const worker = async () => {
|
|
12
|
+
while (idx < items.length) {
|
|
13
|
+
const i = idx++;
|
|
14
|
+
await fn(items[i]!, i);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
await Promise.all(
|
|
18
|
+
Array.from({ length: Math.min(concurrency, items.length) }, worker)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -28,6 +28,9 @@ interface ThumbnailResult {
|
|
|
28
28
|
* The app strips "file://" before calling, so local videos fall into the broken
|
|
29
29
|
* branch. The fix is to KEEP the file:// scheme so the working overload is used.
|
|
30
30
|
*
|
|
31
|
+
* iOS is the opposite: AVAsset / react-native-create-thumbnail requires a bare
|
|
32
|
+
* path without any scheme prefix. Strip "file://" on iOS.
|
|
33
|
+
*
|
|
31
34
|
* For Android photo-picker synthetic paths we additionally copy the file into the
|
|
32
35
|
* app cache first (a clean, app-private file the retriever can always open) and
|
|
33
36
|
* still hand it back with a file:// prefix. Returns the path to pass to
|
|
@@ -36,9 +39,13 @@ interface ThumbnailResult {
|
|
|
36
39
|
const ensureRetrievableVideoPath = async (
|
|
37
40
|
videoPath: string
|
|
38
41
|
): Promise<{ path: string; cleanupPath: string | null }> => {
|
|
39
|
-
// iOS
|
|
42
|
+
// iOS AVAsset / react-native-create-thumbnail requires a bare file path
|
|
43
|
+
// without the file:// scheme. Strip it if present.
|
|
40
44
|
if (Platform.OS !== 'android') {
|
|
41
|
-
|
|
45
|
+
const barePath = videoPath.startsWith('file://')
|
|
46
|
+
? videoPath.slice('file://'.length)
|
|
47
|
+
: videoPath;
|
|
48
|
+
return { path: barePath, cleanupPath: null };
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
// content:// is handled by a dedicated native branch — pass through unchanged.
|
|
@@ -112,7 +119,6 @@ export const VideoThumbnailUtil = {
|
|
|
112
119
|
cacheName,
|
|
113
120
|
});
|
|
114
121
|
|
|
115
|
-
console.log('✅ [VideoThumbnail] THUMBNAIL PATH:', result.path);
|
|
116
122
|
console.log('[VideoThumbnail] Thumbnail generated', {
|
|
117
123
|
path: result.path,
|
|
118
124
|
width: result.width || 0,
|
|
@@ -147,6 +153,7 @@ export const VideoThumbnailUtil = {
|
|
|
147
153
|
}
|
|
148
154
|
},
|
|
149
155
|
|
|
156
|
+
// Serial by design — reduces memory pressure when processing multiple videos.
|
|
150
157
|
batchGenerateThumbnails: async (
|
|
151
158
|
videoPaths: string[],
|
|
152
159
|
options: ThumbnailOptions = {}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import RNFS from 'react-native-fs';
|
|
2
|
+
import VideoThumbnailUtil from './videoThumbnail';
|
|
3
|
+
import { ImageUploadAPI } from '../services/imageUpload';
|
|
4
|
+
import UIUtils from './ui';
|
|
5
|
+
import type { Video as PickedVideo } from 'react-native-image-crop-picker';
|
|
6
|
+
import type { ImageWithUploadStatus } from '../hooks/useImageAttachment';
|
|
7
|
+
|
|
8
|
+
export type VideoWithUploadStatus = Omit<ImageWithUploadStatus, 'image'> & {
|
|
9
|
+
image: PickedVideo;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface VideoThumbnailResult {
|
|
13
|
+
url: string;
|
|
14
|
+
size: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generateAndUploadVideoThumbnails(
|
|
18
|
+
videos: VideoWithUploadStatus[]
|
|
19
|
+
): Promise<Record<string, VideoThumbnailResult>> {
|
|
20
|
+
const thumbnails: Record<string, VideoThumbnailResult> = {};
|
|
21
|
+
const failed: string[] = [];
|
|
22
|
+
|
|
23
|
+
for (let idx = 0; idx < videos.length; idx++) {
|
|
24
|
+
const vidItem = videos[idx]!;
|
|
25
|
+
// Key by uploadedUrl so callers can look up by the same value.
|
|
26
|
+
const videoId = vidItem.uploadedUrl || `video-${idx}`;
|
|
27
|
+
let cachedVideoPath: string | null = null;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let videoDuration = vidItem.image.duration ?? 0;
|
|
31
|
+
|
|
32
|
+
if (videoDuration <= 0 || videoDuration > 3600000) {
|
|
33
|
+
videoDuration = 5000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Clamp upper bound to avoid negative timeMs on clips shorter than 1s.
|
|
37
|
+
const timeMs = Math.min(
|
|
38
|
+
Math.max(1000, Math.floor(videoDuration * 0.35)),
|
|
39
|
+
Math.max(0, videoDuration - 1000)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Copy the picker-returned path to CachesDirectory before generating a
|
|
43
|
+
// thumbnail. On iOS the picker can return Photos-library paths that
|
|
44
|
+
// react-native-create-thumbnail cannot open directly; a copy to a
|
|
45
|
+
// stable app-owned location is required.
|
|
46
|
+
const filename = vidItem.image.filename || 'video.mp4';
|
|
47
|
+
const ext = filename.split('.').pop() || 'mp4';
|
|
48
|
+
const cachedFileName = `thumb-src-${idx}-${Date.now()}.${ext}`;
|
|
49
|
+
cachedVideoPath = `${RNFS.CachesDirectoryPath}/${cachedFileName}`;
|
|
50
|
+
await RNFS.copyFile(vidItem.image.path, cachedVideoPath);
|
|
51
|
+
|
|
52
|
+
const thumbnail = await VideoThumbnailUtil.generateThumbnail(
|
|
53
|
+
cachedVideoPath,
|
|
54
|
+
{ time: timeMs, quality: 80, cacheId: `thumb-${idx}-${Date.now()}` }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!thumbnail) throw new Error('Failed to generate thumbnail');
|
|
58
|
+
|
|
59
|
+
const baseName = filename.split('.')[0] || `video-${idx}`;
|
|
60
|
+
const uploaded = await ImageUploadAPI.uploadImageFile(
|
|
61
|
+
{
|
|
62
|
+
path: thumbnail.path,
|
|
63
|
+
filename: `thumb-${baseName}.jpg`,
|
|
64
|
+
size: 0,
|
|
65
|
+
width: thumbnail.width || 320,
|
|
66
|
+
height: thumbnail.height || 180,
|
|
67
|
+
mime: 'image/jpeg',
|
|
68
|
+
},
|
|
69
|
+
() => {}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!uploaded?.url) throw new Error('No thumbnail URL returned');
|
|
73
|
+
|
|
74
|
+
thumbnails[videoId] = { url: uploaded.url, size: uploaded.size || 0 };
|
|
75
|
+
|
|
76
|
+
// Clean up the local thumbnail file after a successful upload.
|
|
77
|
+
VideoThumbnailUtil.cleanupThumbnail(thumbnail.path).catch((err) =>
|
|
78
|
+
console.warn('[videoThumbnailUpload] Thumbnail cleanup failed:', err)
|
|
79
|
+
);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
82
|
+
console.error('[videoThumbnailUpload] Failed for video:', idx, msg);
|
|
83
|
+
failed.push(vidItem.image.filename || `video-${idx}`);
|
|
84
|
+
// Do NOT insert a poison entry — callers treat a missing key as "no thumbnail".
|
|
85
|
+
} finally {
|
|
86
|
+
if (cachedVideoPath) {
|
|
87
|
+
RNFS.unlink(cachedVideoPath).catch((err) =>
|
|
88
|
+
console.warn('[videoThumbnailUpload] Temp copy cleanup failed:', err)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (failed.length > 0) {
|
|
95
|
+
UIUtils.toast.open({
|
|
96
|
+
title: 'Thumbnail Generation Failed',
|
|
97
|
+
message: `Failed for: ${failed.join(', ')}. Videos will be sent without thumbnails.`,
|
|
98
|
+
theme: 'warning',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return thumbnails;
|
|
103
|
+
}
|