@droppii-org/chat-mobile 0.2.16 → 0.2.17
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/components/AttachmentPreview.js +9 -7
- package/lib/module/components/AttachmentPreview.js.map +1 -1
- package/lib/module/components/MergedImageGrid.js +28 -18
- package/lib/module/components/MergedImageGrid.js.map +1 -1
- package/lib/module/components/messages/mergedMessage/index.js +4 -7
- package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
- package/lib/module/hooks/useImageAttachment.js +16 -8
- package/lib/module/hooks/useImageAttachment.js.map +1 -1
- package/lib/module/hooks/useVideoAttachment.js +1 -1
- package/lib/module/screens/MediaView/VideoPlayer.js +1 -0
- package/lib/module/screens/MediaView/VideoPlayer.js.map +1 -1
- package/lib/module/screens/MediaView/index.js +7 -9
- package/lib/module/screens/MediaView/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +11 -2
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js +1 -1
- package/lib/module/translation/resources/i18n.js +1 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/utils/videoThumbnail.js +76 -3
- package/lib/module/utils/videoThumbnail.js.map +1 -1
- package/lib/typescript/src/components/AttachmentPreview.d.ts.map +1 -1
- package/lib/typescript/src/components/MergedImageGrid.d.ts +2 -2
- package/lib/typescript/src/components/MergedImageGrid.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
- package/lib/typescript/src/screens/MediaView/VideoPlayer.d.ts.map +1 -1
- package/lib/typescript/src/screens/MediaView/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/utils/videoThumbnail.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AttachmentPreview.tsx +11 -5
- package/src/components/MergedImageGrid.tsx +36 -23
- package/src/components/messages/mergedMessage/index.tsx +5 -7
- package/src/hooks/useImageAttachment.ts +22 -12
- package/src/hooks/useVideoAttachment.ts +1 -1
- package/src/screens/MediaView/VideoPlayer.tsx +1 -0
- package/src/screens/MediaView/index.tsx +7 -6
- package/src/screens/chat-detail/ChatListLegend.tsx +9 -2
- package/src/services/attachmentHandlers/videoAttachmentHandler.ts +1 -1
- package/src/translation/resources/i18n.ts +1 -0
- package/src/utils/videoThumbnail.ts +77 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
export class VideoAttachmentHandler {
|
|
4
|
-
static MAX_VIDEO_SIZE =
|
|
4
|
+
static MAX_VIDEO_SIZE = 200 * 1024 * 1024; // 200MB
|
|
5
5
|
static SUPPORTED_TYPES = ['video/mp4', 'video/quicktime'];
|
|
6
6
|
validate(attachment) {
|
|
7
7
|
if (attachment.type !== 'video') {
|
|
@@ -5,6 +5,7 @@ const i18nKey = {
|
|
|
5
5
|
'thread_card_fallback_name': 'Người dùng',
|
|
6
6
|
'chat:image_preview': 'Xem trước ảnh',
|
|
7
7
|
'chat:image_too_large': 'Ảnh quá lớn. Tối đa 5MB',
|
|
8
|
+
'chat:video_too_large': 'Video quá lớn. Tối đa 200MB',
|
|
8
9
|
'chat:image_sent': 'Đã gửi ảnh',
|
|
9
10
|
'chat:image_upload_failed': 'Lỗi tải ảnh',
|
|
10
11
|
'chat:failed_to_send_message': 'Không thể gửi tin nhắn. Thử lại?',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["i18nKey","vi"],"sourceRoot":"../../../../src","sources":["translation/resources/i18n.ts"],"mappings":";;AAAA,MAAMA,OAA+B,GAAG;EACtC,0BAA0B,EAAE,gBAAgB;EAC5C,2BAA2B,EAAE,YAAY;EACzC,oBAAoB,EAAE,eAAe;EACrC,sBAAsB,EAAE,yBAAyB;EACjD,iBAAiB,EAAE,YAAY;EAC/B,0BAA0B,EAAE,aAAa;EACzC,6BAA6B,EAAE,kCAAkC;EACjE,eAAe,EAAE,SAAS;EAC1B,gBAAgB,EAAE,YAAY;EAC9B,iBAAiB,EAAE,SAAS;EAC5B,WAAW,EAAE,KAAK;EAClB,YAAY,EAAE,SAAS;EACvB,2BAA2B,EAAE,6BAA6B;EAC1D,mBAAmB,EAAE,kBAAkB;EACvC,yBAAyB,EAAE,kBAAkB;EAC7C,qBAAqB,EAAE,0BAA0B;EACjD,gBAAgB,EAAE,YAAY;EAC9B,gBAAgB,EAAE,kBAAkB;EACpC,gBAAgB,EAAE,SAAS;EAC3B,eAAe,EAAE,iBAAiB;EAClC,eAAe,EAAE,YAAY;EAC7B,iBAAiB,EAAE,qBAAqB;EACxC,OAAO,EAAE,SAAS;EAClB,SAAS,EAAE;AACb,CAAC;AAED,eAAe;EACbC,EAAE,EAAED;AACN,CAAC","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["i18nKey","vi"],"sourceRoot":"../../../../src","sources":["translation/resources/i18n.ts"],"mappings":";;AAAA,MAAMA,OAA+B,GAAG;EACtC,0BAA0B,EAAE,gBAAgB;EAC5C,2BAA2B,EAAE,YAAY;EACzC,oBAAoB,EAAE,eAAe;EACrC,sBAAsB,EAAE,yBAAyB;EACjD,sBAAsB,EAAE,6BAA6B;EACrD,iBAAiB,EAAE,YAAY;EAC/B,0BAA0B,EAAE,aAAa;EACzC,6BAA6B,EAAE,kCAAkC;EACjE,eAAe,EAAE,SAAS;EAC1B,gBAAgB,EAAE,YAAY;EAC9B,iBAAiB,EAAE,SAAS;EAC5B,WAAW,EAAE,KAAK;EAClB,YAAY,EAAE,SAAS;EACvB,2BAA2B,EAAE,6BAA6B;EAC1D,mBAAmB,EAAE,kBAAkB;EACvC,yBAAyB,EAAE,kBAAkB;EAC7C,qBAAqB,EAAE,0BAA0B;EACjD,gBAAgB,EAAE,YAAY;EAC9B,gBAAgB,EAAE,kBAAkB;EACpC,gBAAgB,EAAE,SAAS;EAC3B,eAAe,EAAE,iBAAiB;EAClC,eAAe,EAAE,YAAY;EAC7B,iBAAiB,EAAE,qBAAqB;EACxC,OAAO,EAAE,SAAS;EAClB,SAAS,EAAE;AACb,CAAC;AAED,eAAe;EACbC,EAAE,EAAED;AACN,CAAC","ignoreList":[]}
|
|
@@ -1,7 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { Platform } from 'react-native';
|
|
3
4
|
import RNFS from 'react-native-fs';
|
|
4
5
|
import { createThumbnail } from 'react-native-create-thumbnail';
|
|
6
|
+
/**
|
|
7
|
+
* Routes the video source so react-native-create-thumbnail picks the correct
|
|
8
|
+
* native MediaMetadataRetriever.setDataSource overload.
|
|
9
|
+
*
|
|
10
|
+
* Root cause of the Android crash: CreateThumbnailModule.getBitmapAtTime branches
|
|
11
|
+
* on the URL scheme —
|
|
12
|
+
* - "file://..." -> setDataSource(String) ✅ works for local files
|
|
13
|
+
* - "content://" -> setDataSource(Context, Uri)
|
|
14
|
+
* - otherwise -> setDataSource(String, Map headers) ❌ this overload is for
|
|
15
|
+
* REMOTE URIs; a bare local path makes it throw an uncaught
|
|
16
|
+
* RuntimeException (status 0xFFFFFFEA / EINVAL) → app crash.
|
|
17
|
+
* The app strips "file://" before calling, so local videos fall into the broken
|
|
18
|
+
* branch. The fix is to KEEP the file:// scheme so the working overload is used.
|
|
19
|
+
*
|
|
20
|
+
* For Android photo-picker synthetic paths we additionally copy the file into the
|
|
21
|
+
* app cache first (a clean, app-private file the retriever can always open) and
|
|
22
|
+
* still hand it back with a file:// prefix. Returns the path to pass to
|
|
23
|
+
* createThumbnail plus a raw cleanup path for any temp copy.
|
|
24
|
+
*/
|
|
25
|
+
const ensureRetrievableVideoPath = async videoPath => {
|
|
26
|
+
// iOS (and any non-Android platform) is left completely untouched.
|
|
27
|
+
if (Platform.OS !== 'android') {
|
|
28
|
+
return {
|
|
29
|
+
path: videoPath,
|
|
30
|
+
cleanupPath: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// content:// is handled by a dedicated native branch — pass through unchanged.
|
|
35
|
+
if (videoPath.startsWith('content://')) {
|
|
36
|
+
return {
|
|
37
|
+
path: videoPath,
|
|
38
|
+
cleanupPath: null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const rawPath = videoPath.startsWith('file://') ? videoPath.replace('file://', '') : videoPath;
|
|
42
|
+
const isSynthetic = rawPath.includes('/.transforms/') || rawPath.includes('/synthetic/');
|
|
43
|
+
if (isSynthetic) {
|
|
44
|
+
const ext = (rawPath.split('.').pop() || 'mp4').split('?')[0];
|
|
45
|
+
const dest = `${RNFS.CachesDirectoryPath}/video-thumb-src-${Date.now()}-${Math.floor(Math.random() * 1e6)}.${ext}`;
|
|
46
|
+
console.log('[VideoThumbnail] Copying picker video to cache before thumbnail', {
|
|
47
|
+
from: rawPath,
|
|
48
|
+
to: dest
|
|
49
|
+
});
|
|
50
|
+
await RNFS.copyFile(rawPath, dest);
|
|
51
|
+
return {
|
|
52
|
+
path: `file://${dest}`,
|
|
53
|
+
cleanupPath: dest
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Any other local path: ensure the file:// scheme so the native module routes
|
|
58
|
+
// to setDataSource(String) instead of the broken (String, Map) overload.
|
|
59
|
+
return {
|
|
60
|
+
path: `file://${rawPath}`,
|
|
61
|
+
cleanupPath: null
|
|
62
|
+
};
|
|
63
|
+
};
|
|
5
64
|
export const VideoThumbnailUtil = {
|
|
6
65
|
generateThumbnail: async (videoPath, options = {}) => {
|
|
7
66
|
const {
|
|
@@ -10,19 +69,28 @@ export const VideoThumbnailUtil = {
|
|
|
10
69
|
cacheId
|
|
11
70
|
} = options; // time in milliseconds
|
|
12
71
|
|
|
72
|
+
let tempSourcePath = null;
|
|
13
73
|
try {
|
|
74
|
+
// Normalize the source so the native MediaMetadataRetriever can open it.
|
|
75
|
+
// Android picker paths crash the app otherwise (see ensureRetrievableVideoPath).
|
|
76
|
+
const {
|
|
77
|
+
path: retrievablePath,
|
|
78
|
+
cleanupPath
|
|
79
|
+
} = await ensureRetrievableVideoPath(videoPath);
|
|
80
|
+
tempSourcePath = cleanupPath;
|
|
81
|
+
|
|
14
82
|
// Generate unique cache name per video to avoid conflicts with concurrent generations
|
|
15
83
|
// Uses provided cacheId or generates one from videoPath filename
|
|
16
|
-
const fileName =
|
|
84
|
+
const fileName = retrievablePath.split('/').pop() || String(Date.now());
|
|
17
85
|
const cacheName = cacheId || `thumb-${fileName.replace(/[^a-zA-Z0-9-_]/g, '')}-${Math.abs(fileName.charCodeAt(0) + fileName.charCodeAt(fileName.length - 1))}`;
|
|
18
86
|
console.log('[VideoThumbnail] Generating thumbnail', {
|
|
19
|
-
videoPath,
|
|
87
|
+
videoPath: retrievablePath,
|
|
20
88
|
timeMs: time,
|
|
21
89
|
quality,
|
|
22
90
|
cacheName
|
|
23
91
|
});
|
|
24
92
|
const result = await createThumbnail({
|
|
25
|
-
url:
|
|
93
|
+
url: retrievablePath,
|
|
26
94
|
timeStamp: time,
|
|
27
95
|
format: 'png',
|
|
28
96
|
dirSize: 100,
|
|
@@ -42,6 +110,11 @@ export const VideoThumbnailUtil = {
|
|
|
42
110
|
} catch (error) {
|
|
43
111
|
console.error('[VideoThumbnail] Failed to generate thumbnail', error);
|
|
44
112
|
return null;
|
|
113
|
+
} finally {
|
|
114
|
+
// Remove the temporary video copy made for synthetic/content sources.
|
|
115
|
+
if (tempSourcePath) {
|
|
116
|
+
RNFS.unlink(tempSourcePath).catch(err => console.warn('[VideoThumbnail] Temp source cleanup failed:', err));
|
|
117
|
+
}
|
|
45
118
|
}
|
|
46
119
|
},
|
|
47
120
|
cleanupThumbnail: async thumbnailPath => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["RNFS","createThumbnail","
|
|
1
|
+
{"version":3,"names":["Platform","RNFS","createThumbnail","ensureRetrievableVideoPath","videoPath","OS","path","cleanupPath","startsWith","rawPath","replace","isSynthetic","includes","ext","split","pop","dest","CachesDirectoryPath","Date","now","Math","floor","random","console","log","from","to","copyFile","VideoThumbnailUtil","generateThumbnail","options","time","quality","cacheId","tempSourcePath","retrievablePath","fileName","String","cacheName","abs","charCodeAt","length","timeMs","result","url","timeStamp","format","dirSize","width","height","error","unlink","catch","err","warn","cleanupThumbnail","thumbnailPath","batchGenerateThumbnails","videoPaths","results","thumbnail","push"],"sourceRoot":"../../../src","sources":["utils/videoThumbnail.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachmentPreview.d.ts","sourceRoot":"","sources":["../../../../src/components/AttachmentPreview.tsx"],"names":[],"mappings":"AAIA,KAAK,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAExE,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,cAAc;IACtB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,sBAAsB;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,KAAK,CAAC,cAAc,GAAG;QAAE,IAAI,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC/D,QAAQ,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD;
|
|
1
|
+
{"version":3,"file":"AttachmentPreview.d.ts","sourceRoot":"","sources":["../../../../src/components/AttachmentPreview.tsx"],"names":[],"mappings":"AAIA,KAAK,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAExE,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,cAAc;IACtB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,sBAAsB;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,KAAK,CAAC,cAAc,GAAG;QAAE,IAAI,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC/D,QAAQ,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD;AA2BD,eAAO,MAAM,iBAAiB,2EACS,sBAAsB,iCA4J5D,CAAC"}
|
|
@@ -8,8 +8,8 @@ interface MergedImageGridProps {
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Generic image grid for merged messages
|
|
11
|
-
* Layout:
|
|
12
|
-
* Shows "+N more" overlay on last cell if images exceed limit
|
|
11
|
+
* Layout: always a single horizontal row (1xN), N capped at maxDisplay (default 3)
|
|
12
|
+
* Shows "+N more" overlay on last cell if images exceed limit (e.g. 4 images → 3 + "+1")
|
|
13
13
|
*/
|
|
14
14
|
export declare const MergedImageGrid: import("react").MemoExoticComponent<({ images, maxDisplay, onMediaPress }: MergedImageGridProps) => import("react").JSX.Element>;
|
|
15
15
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MergedImageGrid.d.ts","sourceRoot":"","sources":["../../../../src/components/MergedImageGrid.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MergedImageGrid.d.ts","sourceRoot":"","sources":["../../../../src/components/MergedImageGrid.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,UAAU,oBAAoB;IAC5B,MAAM,EAAE,KAAK,CAAC;QACZ,WAAW,CAAC,EAAE,GAAG,CAAC;KACnB,CAAC,CAAC;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7D;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,6EACiB,oBAAoB,iCAyLhE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/components/messages/mergedMessage/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,eAAO,MAAM,aAAa,8EACgB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/components/messages/mergedMessage/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,eAAO,MAAM,aAAa,8EACgB,sBAAsB,iCAiC/D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useImageAttachment.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useImageAttachment.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAQ3E,UAAU,qBAAqB;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,qBAAqB,EAAE,CAAC;IACxC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,eAAO,MAAM,kBAAkB,QAAO,
|
|
1
|
+
{"version":3,"file":"useImageAttachment.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useImageAttachment.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAQ3E,UAAU,qBAAqB;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,qBAAqB,EAAE,CAAC;IACxC,qBAAqB,EAAE,OAAO,CAAC;IAC/B,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,eAAO,MAAM,kBAAkB,QAAO,wBAiTrC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../../../src/screens/MediaView/VideoPlayer.tsx"],"names":[],"mappings":"AAUA,OAAc,EAA0B,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAK/E,UAAU,KAAK;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAUD,QAAA,MAAM,WAAW,8CAAgB,KAAK,
|
|
1
|
+
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../../../src/screens/MediaView/VideoPlayer.tsx"],"names":[],"mappings":"AAUA,OAAc,EAA0B,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAK/E,UAAU,KAAK;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAUD,QAAA,MAAM,WAAW,8CAAgB,KAAK,iCA+IpC,CAAC;AAGH,eAAe,WAAW,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/screens/MediaView/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/screens/MediaView/index.tsx"],"names":[],"mappings":"AAgCA,UAAU,iBAAiB;IACzB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,QAAA,MAAM,iBAAiB,GAAI,OAAO,iBAAiB,gCA0LlD,CAAC;AAIF,eAAe,iBAAiB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatListLegend.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/ChatListLegend.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAW7C,eAAO,MAAM,cAAc,kNActB,aAAa,
|
|
1
|
+
{"version":3,"file":"ChatListLegend.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/ChatListLegend.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAW7C,eAAO,MAAM,cAAc,kNActB,aAAa,iCA0WjB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../../../../src/translation/resources/i18n.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../../../../src/translation/resources/i18n.ts"],"names":[],"mappings":";;;AA4BA,wBAEE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"videoThumbnail.d.ts","sourceRoot":"","sources":["../../../../src/utils/videoThumbnail.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"videoThumbnail.d.ts","sourceRoot":"","sources":["../../../../src/utils/videoThumbnail.ts"],"names":[],"mappings":"AAIA,UAAU,gBAAgB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AA4DD,eAAO,MAAM,kBAAkB;mCAEhB,MAAM,YACR,gBAAgB,KACxB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;sCA6DM,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;0CAWhD,MAAM,EAAE,YACX,gBAAgB,KACxB,OAAO,CAAC,eAAe,EAAE,CAAC;CAe9B,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
|
package/package.json
CHANGED
|
@@ -30,9 +30,12 @@ interface AttachmentPreviewProps {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const formatDuration = (seconds: number): string => {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
33
|
+
const totalSecs = Math.floor(seconds);
|
|
34
|
+
const mins = Math.floor(totalSecs / 60);
|
|
35
|
+
const secs = totalSecs % 60;
|
|
36
|
+
return `${mins.toString().padStart(2, '0')}:${secs
|
|
37
|
+
.toString()
|
|
38
|
+
.padStart(2, '0')}`;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
const getAttachmentIcon = (type: AttachmentType): string => {
|
|
@@ -72,7 +75,7 @@ export const AttachmentPreview = memo(
|
|
|
72
75
|
horizontal
|
|
73
76
|
showsHorizontalScrollIndicator={false}
|
|
74
77
|
scrollEventThrottle={16}
|
|
75
|
-
contentContainerStyle={
|
|
78
|
+
contentContainerStyle={styles.scrollContent}
|
|
76
79
|
>
|
|
77
80
|
{attachments.map((item, index) => {
|
|
78
81
|
const {
|
|
@@ -134,7 +137,7 @@ export const AttachmentPreview = memo(
|
|
|
134
137
|
)}
|
|
135
138
|
|
|
136
139
|
{/* Video duration badge */}
|
|
137
|
-
{isVideo && metadata.duration && (
|
|
140
|
+
{isVideo && !!metadata.duration && (
|
|
138
141
|
<KContainer.View style={styles.durationBadge} center>
|
|
139
142
|
<KLabel.Text
|
|
140
143
|
typo="TextXsNormal"
|
|
@@ -216,6 +219,9 @@ const styles = StyleSheet.create({
|
|
|
216
219
|
container: {
|
|
217
220
|
// minHeight: 80,
|
|
218
221
|
},
|
|
222
|
+
scrollContent: {
|
|
223
|
+
alignItems: 'center',
|
|
224
|
+
},
|
|
219
225
|
thumbnail: {
|
|
220
226
|
position: 'relative',
|
|
221
227
|
width: 48,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { memo, useMemo, useState, useCallback } from 'react';
|
|
2
2
|
import { StyleSheet, useWindowDimensions } from 'react-native';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
KContainer,
|
|
5
|
+
KImage,
|
|
6
|
+
KLabel,
|
|
7
|
+
KColors,
|
|
8
|
+
KSpacingValue,
|
|
9
|
+
} from '@droppii/libs';
|
|
4
10
|
import ChatImageDimens from '../utils/chatImageDimens';
|
|
5
11
|
import type { MediaItem } from '../hooks/useMediaViewer';
|
|
6
12
|
|
|
@@ -14,11 +20,11 @@ interface MergedImageGridProps {
|
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* Generic image grid for merged messages
|
|
17
|
-
* Layout:
|
|
18
|
-
* Shows "+N more" overlay on last cell if images exceed limit
|
|
23
|
+
* Layout: always a single horizontal row (1xN), N capped at maxDisplay (default 3)
|
|
24
|
+
* Shows "+N more" overlay on last cell if images exceed limit (e.g. 4 images → 3 + "+1")
|
|
19
25
|
*/
|
|
20
26
|
export const MergedImageGrid = memo(
|
|
21
|
-
({ images, maxDisplay =
|
|
27
|
+
({ images, maxDisplay = 3, onMediaPress }: MergedImageGridProps) => {
|
|
22
28
|
const windowWidth = useWindowDimensions().width;
|
|
23
29
|
const [loadingState, setLoadingState] = useState<Record<number, boolean>>(
|
|
24
30
|
{}
|
|
@@ -106,26 +112,33 @@ export const MergedImageGrid = memo(
|
|
|
106
112
|
return null;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
if (displayedImages.length === 2) {
|
|
112
|
-
columns = 2; // 2 images: horizontal layout (1x2)
|
|
113
|
-
} else if (displayedImages.length === 3) {
|
|
114
|
-
columns = 3; // 3 images: horizontal layout (1x3)
|
|
115
|
-
} else if (displayedImages.length >= 4) {
|
|
116
|
-
columns = 2; // 4+ images: grid layout (2x2)
|
|
117
|
-
}
|
|
115
|
+
// Always lay out images in a single horizontal row (1xN)
|
|
116
|
+
const columns = displayedImages.length;
|
|
118
117
|
|
|
119
|
-
// Use calculated aspect ratio for single image, 100x100 for multiple images
|
|
120
|
-
const cellSize =
|
|
121
|
-
displayedImages.length === 1 && singleImageSize
|
|
122
|
-
? singleImageSize.width
|
|
123
|
-
: 100;
|
|
124
|
-
const cellHeight =
|
|
125
|
-
displayedImages.length === 1 && singleImageSize
|
|
126
|
-
? singleImageSize.height
|
|
127
|
-
: cellSize;
|
|
128
118
|
const gap = 4;
|
|
119
|
+
|
|
120
|
+
// Mirror the parent layout chain so the row never exceeds its container:
|
|
121
|
+
// messageRow: width 100% with 0.75rem horizontal padding (both sides)
|
|
122
|
+
// bubble wrapper: maxWidth 80% of that
|
|
123
|
+
// bubble: another 0.75rem horizontal padding (both sides)
|
|
124
|
+
const rowPaddingH = KSpacingValue['0.75rem'] * 2;
|
|
125
|
+
const bubblePaddingH = KSpacingValue['0.75rem'] * 2;
|
|
126
|
+
const availableWidth = (windowWidth - rowPaddingH) * 0.8 - bubblePaddingH;
|
|
127
|
+
|
|
128
|
+
// Single image keeps its aspect-ratio size; multiple images share the row
|
|
129
|
+
// width so the whole row fits inside the bubble (cell capped at 100px).
|
|
130
|
+
let cellSize: number;
|
|
131
|
+
let cellHeight: number;
|
|
132
|
+
if (displayedImages.length === 1 && singleImageSize) {
|
|
133
|
+
cellSize = singleImageSize.width;
|
|
134
|
+
cellHeight = singleImageSize.height;
|
|
135
|
+
} else {
|
|
136
|
+
const fitted = Math.floor(
|
|
137
|
+
(availableWidth - gap * (columns - 1)) / columns
|
|
138
|
+
);
|
|
139
|
+
cellSize = Math.min(100, fitted);
|
|
140
|
+
cellHeight = cellSize;
|
|
141
|
+
}
|
|
129
142
|
const totalWidth = cellSize * columns + gap * (columns - 1);
|
|
130
143
|
|
|
131
144
|
return (
|
|
@@ -203,7 +216,7 @@ MergedImageGrid.displayName = 'MergedImageGrid';
|
|
|
203
216
|
const styles = StyleSheet.create({
|
|
204
217
|
grid: {
|
|
205
218
|
flexDirection: 'row',
|
|
206
|
-
flexWrap: '
|
|
219
|
+
flexWrap: 'nowrap',
|
|
207
220
|
gap: 4,
|
|
208
221
|
justifyContent: 'flex-start',
|
|
209
222
|
alignItems: 'flex-start',
|
|
@@ -28,13 +28,11 @@ export const MergedMessage = memo(
|
|
|
28
28
|
]}
|
|
29
29
|
>
|
|
30
30
|
{imageMessages.length > 0 && (
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/>
|
|
37
|
-
</KContainer.View>
|
|
31
|
+
<MergedImageGrid
|
|
32
|
+
images={imageMessages}
|
|
33
|
+
maxDisplay={3}
|
|
34
|
+
onMediaPress={onMediaPress}
|
|
35
|
+
/>
|
|
38
36
|
)}
|
|
39
37
|
</KContainer.View>
|
|
40
38
|
</KContainer.View>
|
|
@@ -237,23 +237,33 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
|
|
|
237
237
|
count: images.length,
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
// The gallery picker can return both images and videos. Apply the
|
|
241
|
+
// correct size limit per type: 5MB for images, 200MB for videos.
|
|
242
|
+
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
243
|
+
const MAX_VIDEO_SIZE = 200 * 1024 * 1024;
|
|
244
|
+
const isWithinLimit = (item: PickedImage) => {
|
|
245
|
+
const maxSize = isVideoFromPicker(item)
|
|
246
|
+
? MAX_VIDEO_SIZE
|
|
247
|
+
: MAX_IMAGE_SIZE;
|
|
248
|
+
return (item.size || 0) <= maxSize;
|
|
249
|
+
};
|
|
244
250
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
251
|
+
const oversizedItems = images.filter((img) => !isWithinLimit(img));
|
|
252
|
+
|
|
253
|
+
if (oversizedItems.length > 0) {
|
|
254
|
+
const hasOversizedVideo = oversizedItems.some(isVideoFromPicker);
|
|
255
|
+
console.warn('ImageAttach', 'Step 5: Some files too large', {
|
|
256
|
+
count: oversizedItems.length,
|
|
249
257
|
});
|
|
250
258
|
UIUtils.toast.open({
|
|
251
|
-
title: trans(
|
|
259
|
+
title: trans(
|
|
260
|
+
hasOversizedVideo
|
|
261
|
+
? 'chat:video_too_large'
|
|
262
|
+
: 'chat:image_too_large'
|
|
263
|
+
),
|
|
252
264
|
theme: 'danger',
|
|
253
265
|
});
|
|
254
|
-
const validImages = images.filter(
|
|
255
|
-
(img) => (img.size || 0) <= MAX_FILE_SIZE
|
|
256
|
-
);
|
|
266
|
+
const validImages = images.filter(isWithinLimit);
|
|
257
267
|
if (validImages.length === 0) return;
|
|
258
268
|
images.splice(0, images.length, ...validImages);
|
|
259
269
|
}
|
|
@@ -260,7 +260,7 @@ export const useVideoAttachment = (): UseVideoAttachmentReturn => {
|
|
|
260
260
|
count: videos.length,
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
-
const MAX_FILE_SIZE =
|
|
263
|
+
const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB for videos
|
|
264
264
|
const oversizedVideos = videos.filter(
|
|
265
265
|
(vid) => (vid.size || 0) > MAX_FILE_SIZE
|
|
266
266
|
);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
FlatList,
|
|
8
8
|
type ListRenderItemInfo,
|
|
9
9
|
type ViewToken,
|
|
10
|
+
StatusBar,
|
|
10
11
|
} from 'react-native';
|
|
11
12
|
|
|
12
13
|
import LinearGradient from 'react-native-linear-gradient';
|
|
@@ -58,12 +59,12 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
58
59
|
|
|
59
60
|
const onDownload = useCallback(() => {}, []);
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
StatusBar.setHidden(true);
|
|
64
|
+
return () => {
|
|
65
|
+
StatusBar.setHidden(false);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
67
68
|
|
|
68
69
|
useEffect(() => {
|
|
69
70
|
if (Platform.OS === 'android') {
|
|
@@ -191,12 +191,19 @@ export const ChatListLegend = memo(
|
|
|
191
191
|
on: {
|
|
192
192
|
dataChange: !isLoadingEarlier,
|
|
193
193
|
layout: !isLoadingEarlier,
|
|
194
|
-
|
|
194
|
+
// Must be disabled while prepending older history; otherwise re-measuring
|
|
195
|
+
// the freshly prepended items snaps the list back to the bottom.
|
|
196
|
+
itemLayout: !isLoadingEarlier,
|
|
195
197
|
},
|
|
196
198
|
}),
|
|
197
199
|
[isLoadingEarlier]
|
|
198
200
|
);
|
|
199
201
|
|
|
202
|
+
// Stable config so the native maintainVisibleContentPosition prop isn't toggled
|
|
203
|
+
// mid-prepend (toggling forces an extra layout recompute). `data` anchoring keeps
|
|
204
|
+
// the viewport pinned to the item being read when older messages are added on top.
|
|
205
|
+
const maintainVisiblePositionConfig = useMemo(() => ({ data: true }), []);
|
|
206
|
+
|
|
200
207
|
const handleStartReached = useCallback(() => {
|
|
201
208
|
if (__DEV__) {
|
|
202
209
|
console.log('[LegendList] onStartReached triggered', {
|
|
@@ -349,7 +356,7 @@ export const ChatListLegend = memo(
|
|
|
349
356
|
: undefined
|
|
350
357
|
}
|
|
351
358
|
alignItemsAtEnd
|
|
352
|
-
maintainVisibleContentPosition={
|
|
359
|
+
maintainVisibleContentPosition={maintainVisiblePositionConfig}
|
|
353
360
|
maintainScrollAtEnd={maintainScrollConfig}
|
|
354
361
|
scrollEventThrottle={16}
|
|
355
362
|
onScroll={handleScroll}
|
|
@@ -18,7 +18,7 @@ interface VideoPicInfo {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export class VideoAttachmentHandler implements IAttachmentHandler {
|
|
21
|
-
private static readonly MAX_VIDEO_SIZE =
|
|
21
|
+
private static readonly MAX_VIDEO_SIZE = 200 * 1024 * 1024; // 200MB
|
|
22
22
|
private static readonly SUPPORTED_TYPES = ['video/mp4', 'video/quicktime'];
|
|
23
23
|
|
|
24
24
|
validate(
|
|
@@ -3,6 +3,7 @@ const i18nKey: Record<string, string> = {
|
|
|
3
3
|
'thread_card_fallback_name': 'Người dùng',
|
|
4
4
|
'chat:image_preview': 'Xem trước ảnh',
|
|
5
5
|
'chat:image_too_large': 'Ảnh quá lớn. Tối đa 5MB',
|
|
6
|
+
'chat:video_too_large': 'Video quá lớn. Tối đa 200MB',
|
|
6
7
|
'chat:image_sent': 'Đã gửi ảnh',
|
|
7
8
|
'chat:image_upload_failed': 'Lỗi tải ảnh',
|
|
8
9
|
'chat:failed_to_send_message': 'Không thể gửi tin nhắn. Thử lại?',
|