@droppii-org/chat-mobile 0.2.17 → 0.2.18
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/assets/images/icon_excel.png +0 -0
- package/lib/module/assets/images/icon_img.png +0 -0
- package/lib/module/assets/images/icon_pdf.png +0 -0
- package/lib/module/assets/images/icon_txt.png +0 -0
- package/lib/module/assets/images/icon_word.png +0 -0
- package/lib/module/assets/images/index.js +5 -0
- package/lib/module/assets/images/index.js.map +1 -1
- package/lib/module/components/AttachmentPreview/FilePreview.js +107 -0
- package/lib/module/components/AttachmentPreview/FilePreview.js.map +1 -0
- package/lib/module/components/AttachmentPreview/MediaPreview.js +212 -0
- package/lib/module/components/AttachmentPreview/MediaPreview.js.map +1 -0
- package/lib/module/components/AttachmentPreview/index.js +49 -0
- package/lib/module/components/AttachmentPreview/index.js.map +1 -0
- package/lib/module/components/messages/fileMessage/index.js +122 -12
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/hooks/useFileAttachment.js +116 -0
- package/lib/module/hooks/useFileAttachment.js.map +1 -0
- package/lib/module/hooks/useFileDownload.js +175 -0
- package/lib/module/hooks/useFileDownload.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +44 -36
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useAttachmentMapping.js +51 -0
- package/lib/module/screens/chat-detail/hooks/useAttachmentMapping.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js +39 -13
- package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +1 -1
- package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js +6 -6
- package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js.map +1 -1
- package/lib/module/services/attachmentHandlers/index.js +1 -1
- package/lib/module/services/attachmentHandlers/index.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +9 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/utils/device.js +32 -0
- package/lib/module/utils/device.js.map +1 -1
- package/lib/module/utils/imageUtils.js +44 -28
- package/lib/module/utils/imageUtils.js.map +1 -1
- package/lib/typescript/src/assets/images/index.d.ts +5 -0
- package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
- package/lib/typescript/src/components/AttachmentPreview/FilePreview.d.ts +6 -0
- package/lib/typescript/src/components/AttachmentPreview/FilePreview.d.ts.map +1 -0
- package/lib/typescript/src/components/AttachmentPreview/MediaPreview.d.ts +9 -0
- package/lib/typescript/src/components/AttachmentPreview/MediaPreview.d.ts.map +1 -0
- package/lib/typescript/src/components/AttachmentPreview/index.d.ts +9 -0
- package/lib/typescript/src/components/AttachmentPreview/index.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useFileAttachment.d.ts +20 -0
- package/lib/typescript/src/hooks/useFileAttachment.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useFileDownload.d.ts +15 -0
- package/lib/typescript/src/hooks/useFileDownload.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useImageAttachment.d.ts +1 -2
- package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentMapping.d.ts +10 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentMapping.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts +4 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts +2 -2
- package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/index.d.ts +1 -1
- package/lib/typescript/src/services/attachmentHandlers/index.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/attachment.d.ts +22 -1
- package/lib/typescript/src/types/attachment.d.ts.map +1 -1
- package/lib/typescript/src/utils/device.d.ts +7 -0
- package/lib/typescript/src/utils/device.d.ts.map +1 -1
- package/lib/typescript/src/utils/imageUtils.d.ts +1 -0
- package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/assets/images/icon_excel.png +0 -0
- package/src/assets/images/icon_img.png +0 -0
- package/src/assets/images/icon_pdf.png +0 -0
- package/src/assets/images/icon_txt.png +0 -0
- package/src/assets/images/icon_word.png +0 -0
- package/src/assets/images/index.ts +5 -0
- package/src/components/AttachmentPreview/FilePreview.tsx +137 -0
- package/src/components/AttachmentPreview/MediaPreview.tsx +238 -0
- package/src/components/AttachmentPreview/index.tsx +68 -0
- package/src/components/messages/fileMessage/index.tsx +156 -13
- package/src/hooks/useFileAttachment.ts +163 -0
- package/src/hooks/useFileDownload.ts +200 -0
- package/src/hooks/useImageAttachment.ts +1 -1
- package/src/screens/chat-detail/ChatComposer.tsx +61 -30
- package/src/screens/chat-detail/hooks/useAttachmentMapping.ts +58 -0
- package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +50 -11
- package/src/services/attachmentHandlers/fileAttachmentHandler.ts +8 -7
- package/src/services/attachmentHandlers/index.ts +4 -1
- package/src/translation/resources/i18n.ts +9 -0
- package/src/types/attachment.ts +23 -1
- package/src/utils/device.ts +58 -0
- package/src/utils/imageUtils.ts +50 -28
- package/lib/module/components/AttachmentPreview.js +0 -252
- package/lib/module/components/AttachmentPreview.js.map +0 -1
- package/lib/typescript/src/components/AttachmentPreview.d.ts +0 -28
- package/lib/typescript/src/components/AttachmentPreview.d.ts.map +0 -1
- package/src/components/AttachmentPreview.tsx +0 -310
|
@@ -21,15 +21,17 @@ import { getAttachmentPanelHeight } from './constants';
|
|
|
21
21
|
import type { ChatComposerProps } from './types';
|
|
22
22
|
import { useLinkPreview } from '../../hooks/useLinkPreview/useLinkPreview';
|
|
23
23
|
import { useImageAttachment } from '../../hooks/useImageAttachment';
|
|
24
|
+
import { useFileAttachment } from '../../hooks/useFileAttachment';
|
|
24
25
|
import { useSendAttachment } from '../../hooks/useSendAttachment';
|
|
25
26
|
import { useChatComposerState } from './hooks/useChatComposerState';
|
|
26
27
|
import { useChatComposerAnimation } from './hooks/useChatComposerAnimation';
|
|
27
28
|
import { useAttachmentSendHandler } from './hooks/useAttachmentSendHandler';
|
|
28
|
-
import { isVideoFromPicker } from '../../utils/imageUtils';
|
|
29
29
|
import { useChatDetailContext } from '../../context/ChatDetailContext';
|
|
30
30
|
import { trans } from '../../translation';
|
|
31
31
|
import { useRunFlow } from '../../hooks/flows/useFlow';
|
|
32
32
|
import { ReactFlowsNodeTypes } from '../../types/flows';
|
|
33
|
+
import { useAttachmentMapping } from './hooks/useAttachmentMapping';
|
|
34
|
+
import type { AttachmentType } from '../../types/attachment';
|
|
33
35
|
|
|
34
36
|
export const ChatComposer = memo(
|
|
35
37
|
({
|
|
@@ -73,6 +75,15 @@ export const ChatComposer = memo(
|
|
|
73
75
|
clearAllImages,
|
|
74
76
|
} = useImageAttachment();
|
|
75
77
|
|
|
78
|
+
// File attachment
|
|
79
|
+
const {
|
|
80
|
+
selectedFiles,
|
|
81
|
+
isFilePreviewVisible,
|
|
82
|
+
handleFileAttach,
|
|
83
|
+
handleFileRemove,
|
|
84
|
+
clearAllFiles,
|
|
85
|
+
} = useFileAttachment();
|
|
86
|
+
|
|
76
87
|
// Send attachment
|
|
77
88
|
const { send: sendAttachments } = useSendAttachment();
|
|
78
89
|
|
|
@@ -95,8 +106,10 @@ export const ChatComposer = memo(
|
|
|
95
106
|
// Attachment send handler
|
|
96
107
|
const { handleSend: handleAttachmentSend } = useAttachmentSendHandler({
|
|
97
108
|
selectedImages,
|
|
109
|
+
selectedFiles,
|
|
98
110
|
sendAttachments,
|
|
99
111
|
clearAllImages,
|
|
112
|
+
clearAllFiles,
|
|
100
113
|
onSend,
|
|
101
114
|
onMessageSent,
|
|
102
115
|
textMessage: (value ?? '').trim(),
|
|
@@ -135,15 +148,18 @@ export const ChatComposer = memo(
|
|
|
135
148
|
onAttachmentOpenChange: setIsAttachmentOpen,
|
|
136
149
|
});
|
|
137
150
|
|
|
138
|
-
// Check if any attachments are currently uploading
|
|
151
|
+
// Check if any attachments are currently uploading or have errors
|
|
139
152
|
const isUploadingAttachments = useMemo(
|
|
140
|
-
() =>
|
|
141
|
-
|
|
153
|
+
() =>
|
|
154
|
+
selectedImages.some((img: any) => img.isUploading) ||
|
|
155
|
+
selectedFiles.some((f) => f.isUploading || !!f.error),
|
|
156
|
+
[selectedImages, selectedFiles]
|
|
142
157
|
);
|
|
143
158
|
|
|
144
159
|
// Send handler - includes attachment and preview link features
|
|
145
160
|
const handleSend = useCallback(() => {
|
|
146
|
-
const hasAttachments =
|
|
161
|
+
const hasAttachments =
|
|
162
|
+
selectedImages.length > 0 || selectedFiles.length > 0;
|
|
147
163
|
const hasText = (value ?? '').trim().length > 0;
|
|
148
164
|
// Only send metadata if preview is CURRENTLY visible (user didn't dismiss it)
|
|
149
165
|
const previewMetadata =
|
|
@@ -184,6 +200,7 @@ export const ChatComposer = memo(
|
|
|
184
200
|
urlMetadata,
|
|
185
201
|
showLinkPreview,
|
|
186
202
|
selectedImages,
|
|
203
|
+
selectedFiles,
|
|
187
204
|
handleAttachmentSend,
|
|
188
205
|
value,
|
|
189
206
|
isUploadingAttachments,
|
|
@@ -232,9 +249,22 @@ export const ChatComposer = memo(
|
|
|
232
249
|
}
|
|
233
250
|
}, [value, setIsInputWrapped]);
|
|
234
251
|
|
|
235
|
-
|
|
252
|
+
const handleImageAttachRef = useRef(handleImageAttach);
|
|
253
|
+
handleImageAttachRef.current = handleImageAttach;
|
|
236
254
|
const debouncedImagePickerRef = useRef(
|
|
237
|
-
debounce(
|
|
255
|
+
debounce(() => handleImageAttachRef.current(), 300, {
|
|
256
|
+
leading: true,
|
|
257
|
+
trailing: false,
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const handleFileAttachRef = useRef(handleFileAttach);
|
|
262
|
+
handleFileAttachRef.current = handleFileAttach;
|
|
263
|
+
const debouncedFilePickerRef = useRef(
|
|
264
|
+
debounce(() => handleFileAttachRef.current(), 300, {
|
|
265
|
+
leading: true,
|
|
266
|
+
trailing: false,
|
|
267
|
+
})
|
|
238
268
|
);
|
|
239
269
|
|
|
240
270
|
// Attachment action handler
|
|
@@ -242,6 +272,8 @@ export const ChatComposer = memo(
|
|
|
242
272
|
async (action: any) => {
|
|
243
273
|
if (action.id === 'gallery') {
|
|
244
274
|
debouncedImagePickerRef.current();
|
|
275
|
+
} else if (action.id === 'file') {
|
|
276
|
+
debouncedFilePickerRef.current();
|
|
245
277
|
} else {
|
|
246
278
|
onAttachmentAction?.(action);
|
|
247
279
|
}
|
|
@@ -257,6 +289,24 @@ export const ChatComposer = memo(
|
|
|
257
289
|
[insets.bottom, isAttachmentOpen]
|
|
258
290
|
);
|
|
259
291
|
|
|
292
|
+
const attachments = useAttachmentMapping({
|
|
293
|
+
selectedImages,
|
|
294
|
+
selectedFiles,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const onRemove = useCallback(
|
|
298
|
+
(type: AttachmentType, data: unknown) => {
|
|
299
|
+
switch (type) {
|
|
300
|
+
case 'file':
|
|
301
|
+
return handleFileRemove(data as string);
|
|
302
|
+
|
|
303
|
+
default:
|
|
304
|
+
return handleImageRemove(data as number);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
[handleImageRemove, handleFileRemove]
|
|
308
|
+
);
|
|
309
|
+
|
|
260
310
|
if (
|
|
261
311
|
isBotConversation &&
|
|
262
312
|
conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
|
|
@@ -278,6 +328,7 @@ export const ChatComposer = memo(
|
|
|
278
328
|
</KContainer.View>
|
|
279
329
|
);
|
|
280
330
|
}
|
|
331
|
+
|
|
281
332
|
return (
|
|
282
333
|
<KContainer.View background={'rgba(131, 137, 157, 0.05)'}>
|
|
283
334
|
<ChatQuickActions
|
|
@@ -297,29 +348,9 @@ export const ChatComposer = memo(
|
|
|
297
348
|
)}
|
|
298
349
|
|
|
299
350
|
<AttachmentPreview
|
|
300
|
-
visible={isImagePreviewVisible}
|
|
301
|
-
attachments={
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
type: isVideo ? ('video' as const) : ('image' as const),
|
|
305
|
-
metadata: {
|
|
306
|
-
path: img.image.path,
|
|
307
|
-
filename: img.image.filename,
|
|
308
|
-
size: img.image.size,
|
|
309
|
-
width: img.image.width,
|
|
310
|
-
height: img.image.height,
|
|
311
|
-
...(isVideo && { duration: (img.image as any).duration }),
|
|
312
|
-
},
|
|
313
|
-
uploadProgress: img.uploadProgress,
|
|
314
|
-
error: img.error,
|
|
315
|
-
isUploading: img.isUploading,
|
|
316
|
-
uploadedUrl: img.uploadedUrl,
|
|
317
|
-
...(isVideo && { thumbnailUrl: img.thumbnailUrl }),
|
|
318
|
-
};
|
|
319
|
-
})}
|
|
320
|
-
onRemove={(type, index) => {
|
|
321
|
-
handleImageRemove(index);
|
|
322
|
-
}}
|
|
351
|
+
visible={isImagePreviewVisible || isFilePreviewVisible}
|
|
352
|
+
attachments={attachments}
|
|
353
|
+
onRemove={onRemove}
|
|
323
354
|
/>
|
|
324
355
|
|
|
325
356
|
<KContainer.View
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isVideoFromPicker } from '../../../utils/imageUtils';
|
|
2
|
+
import type { ImageWithUploadStatus } from '../../../hooks/useImageAttachment';
|
|
3
|
+
import type { FileWithUploadStatus } from '../../../hooks/useFileAttachment';
|
|
4
|
+
import type { AttachmentItem } from '../../../types/attachment';
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
selectedImages: ImageWithUploadStatus[];
|
|
9
|
+
selectedFiles: FileWithUploadStatus[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const mapMedia = (img: ImageWithUploadStatus): AttachmentItem => {
|
|
13
|
+
const isVideo = isVideoFromPicker(img.image);
|
|
14
|
+
return {
|
|
15
|
+
type: isVideo ? ('video' as const) : ('image' as const),
|
|
16
|
+
metadata: {
|
|
17
|
+
path: img.image.path,
|
|
18
|
+
filename: img.image.filename,
|
|
19
|
+
size: img.image.size,
|
|
20
|
+
width: img.image.width,
|
|
21
|
+
height: img.image.height,
|
|
22
|
+
...(isVideo && { duration: (img.image as any).duration }),
|
|
23
|
+
},
|
|
24
|
+
uploadProgress: img.uploadProgress,
|
|
25
|
+
error: img.error,
|
|
26
|
+
isUploading: img.isUploading,
|
|
27
|
+
uploadedUrl: img.uploadedUrl,
|
|
28
|
+
...(isVideo && { thumbnailUrl: img.thumbnailUrl }),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mapFile = (file: FileWithUploadStatus): AttachmentItem => {
|
|
33
|
+
return {
|
|
34
|
+
type: 'file',
|
|
35
|
+
metadata: {
|
|
36
|
+
id: file.id,
|
|
37
|
+
path: file.uri,
|
|
38
|
+
filename: file.name,
|
|
39
|
+
size: file.size,
|
|
40
|
+
mimeType: file.mimeType,
|
|
41
|
+
uri: file.uri,
|
|
42
|
+
},
|
|
43
|
+
uploadProgress: file.uploadProgress,
|
|
44
|
+
error: file.error,
|
|
45
|
+
isUploading: file.isUploading,
|
|
46
|
+
uploadedUrl: file.uploadedUrl,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const useAttachmentMapping = ({
|
|
51
|
+
selectedImages,
|
|
52
|
+
selectedFiles,
|
|
53
|
+
}: Props) => {
|
|
54
|
+
return useMemo(
|
|
55
|
+
() => [...selectedImages.map(mapMedia), ...selectedFiles.map(mapFile)],
|
|
56
|
+
[selectedImages, selectedFiles]
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -5,6 +5,7 @@ import VideoThumbnailUtil from '../../../utils/videoThumbnail';
|
|
|
5
5
|
import { ImageUploadAPI } from '../../../services/imageUpload';
|
|
6
6
|
import UIUtils from '../../../utils/ui';
|
|
7
7
|
import type { DMessageItem } from '../../../types/chat';
|
|
8
|
+
import type { FileWithUploadStatus } from '../../../hooks/useFileAttachment';
|
|
8
9
|
|
|
9
10
|
interface AttachmentItem {
|
|
10
11
|
image: any;
|
|
@@ -16,12 +17,14 @@ interface AttachmentItem {
|
|
|
16
17
|
|
|
17
18
|
interface UseAttachmentSendHandlerProps {
|
|
18
19
|
selectedImages: AttachmentItem[];
|
|
20
|
+
selectedFiles: FileWithUploadStatus[];
|
|
19
21
|
sendAttachments: (
|
|
20
22
|
items: any[],
|
|
21
23
|
_: any,
|
|
22
24
|
onMessageSent?: (message: DMessageItem) => void
|
|
23
25
|
) => Promise<boolean>;
|
|
24
26
|
clearAllImages: () => void;
|
|
27
|
+
clearAllFiles?: () => void;
|
|
25
28
|
onSend?: () => void;
|
|
26
29
|
onMessageSent?: (message: DMessageItem) => void;
|
|
27
30
|
textMessage?: string;
|
|
@@ -31,8 +34,10 @@ interface UseAttachmentSendHandlerProps {
|
|
|
31
34
|
|
|
32
35
|
export const useAttachmentSendHandler = ({
|
|
33
36
|
selectedImages,
|
|
37
|
+
selectedFiles,
|
|
34
38
|
sendAttachments,
|
|
35
39
|
clearAllImages,
|
|
40
|
+
clearAllFiles,
|
|
36
41
|
onSend,
|
|
37
42
|
onMessageSent,
|
|
38
43
|
textMessage,
|
|
@@ -316,26 +321,60 @@ export const useAttachmentSendHandler = ({
|
|
|
316
321
|
if (success) {
|
|
317
322
|
clearAllImages();
|
|
318
323
|
console.log('[ChatComposer] Images cleared after successful send');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
319
326
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
// Send each file as its own individual message
|
|
328
|
+
const uploadedFiles = selectedFiles.filter(
|
|
329
|
+
(f) => f.uploadedUrl && !f.error
|
|
330
|
+
);
|
|
331
|
+
if (uploadedFiles.length > 0) {
|
|
332
|
+
console.log('[ChatComposer] Sending files individually:', {
|
|
333
|
+
count: uploadedFiles.length,
|
|
334
|
+
});
|
|
335
|
+
for (const file of uploadedFiles) {
|
|
336
|
+
await sendAttachments(
|
|
337
|
+
[
|
|
338
|
+
{
|
|
339
|
+
type: 'file' as const,
|
|
340
|
+
file: { path: file.uri, filename: file.name, size: file.size },
|
|
341
|
+
uploadProgress: 100,
|
|
342
|
+
error: null,
|
|
343
|
+
isUploading: false,
|
|
344
|
+
uploadedUrl: file.uploadedUrl,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
undefined,
|
|
348
|
+
onMessageSent
|
|
349
|
+
);
|
|
329
350
|
}
|
|
351
|
+
clearAllFiles?.();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Send text last
|
|
355
|
+
if (textMessage && onSendText) {
|
|
356
|
+
console.log('[ChatComposer] Sending text message after attachments', {
|
|
357
|
+
hasPreview: !!urlMetadata,
|
|
358
|
+
previewUrl: urlMetadata?.url,
|
|
359
|
+
});
|
|
360
|
+
onSendText(textMessage, urlMetadata);
|
|
330
361
|
return;
|
|
331
362
|
}
|
|
332
363
|
|
|
333
|
-
|
|
334
|
-
|
|
364
|
+
const hasAnySent =
|
|
365
|
+
uploadedImages.length > 0 ||
|
|
366
|
+
allUploadedVideos.length > 0 ||
|
|
367
|
+
uploadedFiles.length > 0;
|
|
368
|
+
if (!hasAnySent) {
|
|
369
|
+
onSend?.();
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
335
372
|
}, [
|
|
336
373
|
selectedImages,
|
|
374
|
+
selectedFiles,
|
|
337
375
|
sendAttachments,
|
|
338
376
|
clearAllImages,
|
|
377
|
+
clearAllFiles,
|
|
339
378
|
onSend,
|
|
340
379
|
onMessageSent,
|
|
341
380
|
textMessage,
|
|
@@ -6,12 +6,13 @@ interface FileElem {
|
|
|
6
6
|
fileName: string;
|
|
7
7
|
fileSize: number;
|
|
8
8
|
fileType: string;
|
|
9
|
-
|
|
9
|
+
sourceUrl: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const MB = 1024 * 1024;
|
|
13
|
+
export const MAX_FILE_SIZE = 50 * MB;
|
|
14
14
|
|
|
15
|
+
export class FileAttachmentHandler implements IAttachmentHandler {
|
|
15
16
|
validate(
|
|
16
17
|
attachment: Attachment
|
|
17
18
|
): { valid: true } | { valid: false; reason: string } {
|
|
@@ -38,10 +39,10 @@ export class FileAttachmentHandler implements IAttachmentHandler {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
if (sourceFile.size >
|
|
42
|
+
if (sourceFile.size > MAX_FILE_SIZE) {
|
|
42
43
|
return {
|
|
43
44
|
valid: false,
|
|
44
|
-
reason: `File size exceeds maximum (${
|
|
45
|
+
reason: `File size exceeds maximum (${MAX_FILE_SIZE / 1024 / 1024}MB)`,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -60,7 +61,7 @@ export class FileAttachmentHandler implements IAttachmentHandler {
|
|
|
60
61
|
fileName: sourceFile.filename,
|
|
61
62
|
fileSize: sourceFile.size,
|
|
62
63
|
fileType: sourceFile.mimeType,
|
|
63
|
-
|
|
64
|
+
sourceUrl: uploadedUrl,
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
console.warn('[FileAttachmentHandler] Built payload', {
|
|
@@ -68,7 +69,7 @@ export class FileAttachmentHandler implements IAttachmentHandler {
|
|
|
68
69
|
fileName: fileElem.fileName,
|
|
69
70
|
fileSize: fileElem.fileSize,
|
|
70
71
|
fileType: fileElem.fileType,
|
|
71
|
-
|
|
72
|
+
sourceUrl: uploadedUrl.substring(0, 50) + '...',
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
return fileElem;
|
|
@@ -7,4 +7,7 @@ export function initializeAttachmentHandlers(): void {
|
|
|
7
7
|
|
|
8
8
|
export { ImageAttachmentHandler } from './imageAttachmentHandler';
|
|
9
9
|
export { default as VideoAttachmentHandler } from './videoAttachmentHandler';
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
default as FileAttachmentHandler,
|
|
12
|
+
MAX_FILE_SIZE,
|
|
13
|
+
} from './fileAttachmentHandler';
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
const i18nKey: Record<string, string> = {
|
|
2
2
|
'thread_card_bot_crm_name': 'Droppii Hỗ Trợ',
|
|
3
3
|
'thread_card_fallback_name': 'Người dùng',
|
|
4
|
+
'chat:file_too_large': 'Tệp quá lớn. Tối đa 10MB',
|
|
5
|
+
'chat:file_max_count': 'Chỉ có thể đính kèm tối đa 3 tệp',
|
|
6
|
+
'chat:file_pick_failed': 'Không thể chọn tệp',
|
|
7
|
+
'chat:file_upload_failed': 'Lỗi tải tệp lên',
|
|
8
|
+
'chat:file_preview_unsupported': 'Không thể xem trước tệp này',
|
|
9
|
+
'chat:file_downloading': 'Đang tải {{received}}/{{total}} MB',
|
|
10
|
+
'chat:file_downloaded_success': 'Đã tải thành công {{fileName}}',
|
|
11
|
+
'chat:file_download_failed': 'Tải tệp thất bại',
|
|
12
|
+
'chat:file_open_failed': 'Không thể mở tệp này',
|
|
4
13
|
'chat:image_preview': 'Xem trước ảnh',
|
|
5
14
|
'chat:image_too_large': 'Ảnh quá lớn. Tối đa 5MB',
|
|
6
15
|
'chat:video_too_large': 'Video quá lớn. Tối đa 200MB',
|
package/src/types/attachment.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AttachmentType = 'image' | 'video' | 'file' | 'audio';
|
|
1
|
+
export type AttachmentType = 'image' | 'video' | 'file' | 'audio' | 'document';
|
|
2
2
|
|
|
3
3
|
export interface BaseAttachment {
|
|
4
4
|
id: string;
|
|
@@ -83,3 +83,25 @@ export interface AttachmentSendResult {
|
|
|
83
83
|
mergedMessageId?: string;
|
|
84
84
|
errors: Record<string, string>;
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
interface AttachmentMetadata {
|
|
88
|
+
id?: string;
|
|
89
|
+
path: string;
|
|
90
|
+
filename?: string;
|
|
91
|
+
size?: number;
|
|
92
|
+
duration?: number;
|
|
93
|
+
mimeType?: string;
|
|
94
|
+
width?: number;
|
|
95
|
+
height?: number;
|
|
96
|
+
uri?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AttachmentItem {
|
|
100
|
+
metadata: AttachmentMetadata;
|
|
101
|
+
uploadProgress: number;
|
|
102
|
+
error: string | null;
|
|
103
|
+
isUploading: boolean;
|
|
104
|
+
uploadedUrl?: string;
|
|
105
|
+
thumbnailUrl?: string;
|
|
106
|
+
type: AttachmentType;
|
|
107
|
+
}
|
package/src/utils/device.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import ImagePicker from 'react-native-image-crop-picker';
|
|
2
|
+
import {
|
|
3
|
+
pick as pickDocument,
|
|
4
|
+
types as documentTypes,
|
|
5
|
+
isErrorWithCode,
|
|
6
|
+
errorCodes,
|
|
7
|
+
keepLocalCopy,
|
|
8
|
+
} from '@react-native-documents/picker';
|
|
9
|
+
|
|
10
|
+
export interface PickedFile {
|
|
11
|
+
uri: string;
|
|
12
|
+
name: string;
|
|
13
|
+
size: number;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
const DeviceUtils = {
|
|
4
18
|
openImagePicker: async (multiple: boolean = false) => {
|
|
@@ -68,6 +82,50 @@ const DeviceUtils = {
|
|
|
68
82
|
throw error;
|
|
69
83
|
}
|
|
70
84
|
},
|
|
85
|
+
|
|
86
|
+
openFilePicker: async (
|
|
87
|
+
multiple: boolean = false
|
|
88
|
+
): Promise<PickedFile[] | null> => {
|
|
89
|
+
try {
|
|
90
|
+
const results = await pickDocument({
|
|
91
|
+
allowMultiSelection: multiple,
|
|
92
|
+
type: [documentTypes.allFiles],
|
|
93
|
+
mode: 'import',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Copy picked files to app's document directory so uploads can access them
|
|
97
|
+
const copies = await keepLocalCopy({
|
|
98
|
+
files: results.map((r) => ({
|
|
99
|
+
uri: r.uri,
|
|
100
|
+
fileName: r.name ?? 'file',
|
|
101
|
+
})) as [
|
|
102
|
+
{ uri: string; fileName: string },
|
|
103
|
+
...{ uri: string; fileName: string }[],
|
|
104
|
+
],
|
|
105
|
+
destination: 'documentDirectory',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const files = results.map((r, i) => ({
|
|
109
|
+
uri: decodeURIComponent(
|
|
110
|
+
copies[i]?.status === 'success' ? copies[i].localUri : r.uri
|
|
111
|
+
),
|
|
112
|
+
name: r.name ?? 'file',
|
|
113
|
+
size: r.size ?? 0,
|
|
114
|
+
mimeType: r.type ?? 'application/octet-stream',
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
return files;
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
if (
|
|
120
|
+
isErrorWithCode(error) &&
|
|
121
|
+
error.code === errorCodes.OPERATION_CANCELED
|
|
122
|
+
) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
console.error('[File Picker] Error:', error);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
71
129
|
};
|
|
72
130
|
|
|
73
131
|
export default DeviceUtils;
|
package/src/utils/imageUtils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageFile } from '../types/imageUpload';
|
|
2
|
+
import Images from '../assets/images';
|
|
2
3
|
|
|
3
4
|
export const generateUUID = (): string => {
|
|
4
5
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
@@ -17,39 +18,60 @@ export const isVideoFromPicker = (pickedItem: any): boolean => {
|
|
|
17
18
|
return mime.startsWith('video/');
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
const mimeMap: Record<string, string> = {
|
|
22
|
+
jpg: 'image/jpeg',
|
|
23
|
+
jpeg: 'image/jpeg',
|
|
24
|
+
png: 'image/png',
|
|
25
|
+
webp: 'image/webp',
|
|
26
|
+
heic: 'image/heic',
|
|
27
|
+
heif: 'image/heif',
|
|
28
|
+
mp4: 'video/mp4',
|
|
29
|
+
mov: 'video/quicktime',
|
|
30
|
+
avi: 'video/x-msvideo',
|
|
31
|
+
mkv: 'video/x-matroska',
|
|
32
|
+
webm: 'video/webm',
|
|
33
|
+
m4v: 'video/mp4',
|
|
34
|
+
flv: 'video/x-flv',
|
|
35
|
+
wmv: 'video/x-ms-wmv',
|
|
36
|
+
mp3: 'audio/mpeg',
|
|
37
|
+
wav: 'audio/wav',
|
|
38
|
+
m4a: 'audio/mp4',
|
|
39
|
+
aac: 'audio/aac',
|
|
40
|
+
flac: 'audio/flac',
|
|
41
|
+
ogg: 'audio/ogg',
|
|
42
|
+
pdf: 'application/pdf',
|
|
43
|
+
doc: 'application/msword',
|
|
44
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
45
|
+
xls: 'application/vnd.ms-excel',
|
|
46
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
47
|
+
zip: 'application/zip',
|
|
48
|
+
};
|
|
49
|
+
|
|
20
50
|
export const getMimeType = (fileName: string): string => {
|
|
21
51
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
22
|
-
const mimeMap: Record<string, string> = {
|
|
23
|
-
jpg: 'image/jpeg',
|
|
24
|
-
jpeg: 'image/jpeg',
|
|
25
|
-
png: 'image/png',
|
|
26
|
-
webp: 'image/webp',
|
|
27
|
-
heic: 'image/heic',
|
|
28
|
-
heif: 'image/heif',
|
|
29
|
-
mp4: 'video/mp4',
|
|
30
|
-
mov: 'video/quicktime',
|
|
31
|
-
avi: 'video/x-msvideo',
|
|
32
|
-
mkv: 'video/x-matroska',
|
|
33
|
-
webm: 'video/webm',
|
|
34
|
-
m4v: 'video/mp4',
|
|
35
|
-
flv: 'video/x-flv',
|
|
36
|
-
wmv: 'video/x-ms-wmv',
|
|
37
|
-
mp3: 'audio/mpeg',
|
|
38
|
-
wav: 'audio/wav',
|
|
39
|
-
m4a: 'audio/mp4',
|
|
40
|
-
aac: 'audio/aac',
|
|
41
|
-
flac: 'audio/flac',
|
|
42
|
-
ogg: 'audio/ogg',
|
|
43
|
-
pdf: 'application/pdf',
|
|
44
|
-
doc: 'application/msword',
|
|
45
|
-
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
46
|
-
xls: 'application/vnd.ms-excel',
|
|
47
|
-
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
48
|
-
zip: 'application/zip',
|
|
49
|
-
};
|
|
50
52
|
return mimeMap[ext || ''] || 'application/octet-stream';
|
|
51
53
|
};
|
|
52
54
|
|
|
55
|
+
export const getFileIcon = (mimeType?: string): number => {
|
|
56
|
+
if (mimeType?.startsWith('image/')) return Images.ICON_IMG;
|
|
57
|
+
|
|
58
|
+
switch (mimeType) {
|
|
59
|
+
case mimeMap.pdf:
|
|
60
|
+
return Images.ICON_PDF;
|
|
61
|
+
|
|
62
|
+
case mimeMap.doc:
|
|
63
|
+
case mimeMap.docx:
|
|
64
|
+
return Images.ICON_WORD;
|
|
65
|
+
|
|
66
|
+
case mimeMap.xls:
|
|
67
|
+
case mimeMap.xlsx:
|
|
68
|
+
return Images.ICON_EXCEL;
|
|
69
|
+
|
|
70
|
+
default:
|
|
71
|
+
return Images.ICON_TXT;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
53
75
|
export const formatFileSize = (bytes: number): string => {
|
|
54
76
|
if (bytes === 0) return '0 Bytes';
|
|
55
77
|
const k = 1024;
|