@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.
Files changed (42) hide show
  1. package/lib/module/components/AttachmentPreview.js +9 -7
  2. package/lib/module/components/AttachmentPreview.js.map +1 -1
  3. package/lib/module/components/MergedImageGrid.js +28 -18
  4. package/lib/module/components/MergedImageGrid.js.map +1 -1
  5. package/lib/module/components/messages/mergedMessage/index.js +4 -7
  6. package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
  7. package/lib/module/hooks/useImageAttachment.js +16 -8
  8. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  9. package/lib/module/hooks/useVideoAttachment.js +1 -1
  10. package/lib/module/screens/MediaView/VideoPlayer.js +1 -0
  11. package/lib/module/screens/MediaView/VideoPlayer.js.map +1 -1
  12. package/lib/module/screens/MediaView/index.js +7 -9
  13. package/lib/module/screens/MediaView/index.js.map +1 -1
  14. package/lib/module/screens/chat-detail/ChatListLegend.js +11 -2
  15. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  16. package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js +1 -1
  17. package/lib/module/translation/resources/i18n.js +1 -0
  18. package/lib/module/translation/resources/i18n.js.map +1 -1
  19. package/lib/module/utils/videoThumbnail.js +76 -3
  20. package/lib/module/utils/videoThumbnail.js.map +1 -1
  21. package/lib/typescript/src/components/AttachmentPreview.d.ts.map +1 -1
  22. package/lib/typescript/src/components/MergedImageGrid.d.ts +2 -2
  23. package/lib/typescript/src/components/MergedImageGrid.d.ts.map +1 -1
  24. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  25. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  26. package/lib/typescript/src/screens/MediaView/VideoPlayer.d.ts.map +1 -1
  27. package/lib/typescript/src/screens/MediaView/index.d.ts.map +1 -1
  28. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  29. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  30. package/lib/typescript/src/utils/videoThumbnail.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/components/AttachmentPreview.tsx +11 -5
  33. package/src/components/MergedImageGrid.tsx +36 -23
  34. package/src/components/messages/mergedMessage/index.tsx +5 -7
  35. package/src/hooks/useImageAttachment.ts +22 -12
  36. package/src/hooks/useVideoAttachment.ts +1 -1
  37. package/src/screens/MediaView/VideoPlayer.tsx +1 -0
  38. package/src/screens/MediaView/index.tsx +7 -6
  39. package/src/screens/chat-detail/ChatListLegend.tsx +9 -2
  40. package/src/services/attachmentHandlers/videoAttachmentHandler.ts +1 -1
  41. package/src/translation/resources/i18n.ts +1 -0
  42. 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 = 100 * 1024 * 1024; // 100MB
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 = videoPath.split('/').pop() || String(Date.now());
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: videoPath,
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","VideoThumbnailUtil","generateThumbnail","videoPath","options","time","quality","cacheId","fileName","split","pop","String","Date","now","cacheName","replace","Math","abs","charCodeAt","length","console","log","timeMs","result","url","timeStamp","format","dirSize","path","width","height","error","cleanupThumbnail","thumbnailPath","unlink","err","warn","batchGenerateThumbnails","videoPaths","results","thumbnail","push"],"sourceRoot":"../../../src","sources":["utils/videoThumbnail.ts"],"mappings":";;AAAA,OAAOA,IAAI,MAAM,iBAAiB;AAClC,SAASC,eAAe,QAAQ,+BAA+B;AAc/D,OAAO,MAAMC,kBAAkB,GAAG;EAChCC,iBAAiB,EAAE,MAAAA,CACjBC,SAAiB,EACjBC,OAAyB,GAAG,CAAC,CAAC,KACM;IACpC,MAAM;MAAEC,IAAI,GAAG,GAAG;MAAEC,OAAO,GAAG,EAAE;MAAEC;IAAQ,CAAC,GAAGH,OAAO,CAAC,CAAC;;IAEvD,IAAI;MACF;MACA;MACA,MAAMI,QAAQ,GAAGL,SAAS,CAACM,KAAK,CAAC,GAAG,CAAC,CAACC,GAAG,CAAC,CAAC,IAAIC,MAAM,CAACC,IAAI,CAACC,GAAG,CAAC,CAAC,CAAC;MACjE,MAAMC,SAAS,GACbP,OAAO,IACP,SAASC,QAAQ,CAACO,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,IAAIC,IAAI,CAACC,GAAG,CAC1DT,QAAQ,CAACU,UAAU,CAAC,CAAC,CAAC,GAAGV,QAAQ,CAACU,UAAU,CAACV,QAAQ,CAACW,MAAM,GAAG,CAAC,CAClE,CAAC,EAAE;MAELC,OAAO,CAACC,GAAG,CAAC,uCAAuC,EAAE;QACnDlB,SAAS;QACTmB,MAAM,EAAEjB,IAAI;QACZC,OAAO;QACPQ;MACF,CAAC,CAAC;MAEF,MAAMS,MAAM,GAAG,MAAMvB,eAAe,CAAC;QACnCwB,GAAG,EAAErB,SAAS;QACdsB,SAAS,EAAEpB,IAAI;QACfqB,MAAM,EAAE,KAAK;QACbC,OAAO,EAAE,GAAG;QACZb;MACF,CAAC,CAAC;MAEFM,OAAO,CAACC,GAAG,CAAC,oCAAoC,EAAEE,MAAM,CAACK,IAAI,CAAC;MAC9DR,OAAO,CAACC,GAAG,CAAC,sCAAsC,EAAE;QAClDO,IAAI,EAAEL,MAAM,CAACK,IAAI;QACjBC,KAAK,EAAEN,MAAM,CAACM,KAAK,IAAI,CAAC;QACxBC,MAAM,EAAEP,MAAM,CAACO,MAAM,IAAI;MAC3B,CAAC,CAAC;MAEF,OAAO;QACLF,IAAI,EAAEL,MAAM,CAACK,IAAI;QACjBC,KAAK,EAAEN,MAAM,CAACM,KAAK,IAAI,CAAC;QACxBC,MAAM,EAAEP,MAAM,CAACO,MAAM,IAAI;MAC3B,CAAC;IACH,CAAC,CAAC,OAAOC,KAAK,EAAE;MACdX,OAAO,CAACW,KAAK,CAAC,+CAA+C,EAAEA,KAAK,CAAC;MACrE,OAAO,IAAI;IACb;EACF,CAAC;EAEDC,gBAAgB,EAAE,MAAOC,aAAqB,IAAoB;IAChE,IAAI;MACF,IAAI,CAACA,aAAa,EAAE;MACpB,MAAMlC,IAAI,CAACmC,MAAM,CAACD,aAAa,CAAC;MAChCb,OAAO,CAACC,GAAG,CAAC,8BAA8B,EAAEY,aAAa,CAAC;IAC5D,CAAC,CAAC,OAAOE,GAAG,EAAE;MACZf,OAAO,CAACgB,IAAI,CAAC,kCAAkC,EAAED,GAAG,CAAC;IACvD;EACF,CAAC;EAEDE,uBAAuB,EAAE,MAAAA,CACvBC,UAAoB,EACpBlC,OAAyB,GAAG,CAAC,CAAC,KACC;IAC/B,MAAMmC,OAA0B,GAAG,EAAE;IAErC,KAAK,MAAMpC,SAAS,IAAImC,UAAU,EAAE;MAClC,MAAME,SAAS,GAAG,MAAMvC,kBAAkB,CAACC,iBAAiB,CAC1DC,SAAS,EACTC,OACF,CAAC;MACD,IAAIoC,SAAS,EAAE;QACbD,OAAO,CAACE,IAAI,CAACD,SAAS,CAAC;MACzB;IACF;IAEA,OAAOD,OAAO;EAChB;AACF,CAAC;AAED,eAAetC,kBAAkB","ignoreList":[]}
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;AAwBD,eAAO,MAAM,iBAAiB,2EACS,sBAAsB,iCA4J5D,CAAC"}
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: 1 image (1x1), 2 images (1x2), 3 images (1x3), 4+ images (2x2)
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":"AAIA,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,iCAkLhE,CAAC"}
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,iCAmC/D,CAAC"}
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,wBAuSrC,CAAC"}
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,iCA8IpC,CAAC;AAGH,eAAe,WAAW,CAAC"}
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":"AA+BA,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
+ {"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,iCAmWjB,CAAC"}
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":";;;AA2BA,wBAEE"}
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":"AAGA,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;AAED,eAAO,MAAM,kBAAkB;mCAEhB,MAAM,YACR,gBAAgB,KACxB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;sCA8CM,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;0CAWhD,MAAM,EAAE,YACX,gBAAgB,KACxB,OAAO,CAAC,eAAe,EAAE,CAAC;CAe9B,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droppii-org/chat-mobile",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "Droppii chat mobile",
5
5
  "keywords": [
6
6
  "react-native",
@@ -30,9 +30,12 @@ interface AttachmentPreviewProps {
30
30
  }
31
31
 
32
32
  const formatDuration = (seconds: number): string => {
33
- const mins = Math.floor(seconds / 60);
34
- const secs = seconds % 60;
35
- return `${mins}:${secs.toString().padStart(2, '0')}`;
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={{ alignItems: 'center' }}
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 { KContainer, KImage, KLabel, KColors } from '@droppii/libs';
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: 1 image (1x1), 2 images (1x2), 3 images (1x3), 4+ images (2x2)
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 = 4, onMediaPress }: MergedImageGridProps) => {
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
- // Calculate columns based on image count
110
- let columns = 1;
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: 'wrap',
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
- <KContainer.View marginB="0.5rem">
32
- <MergedImageGrid
33
- images={imageMessages}
34
- maxDisplay={4}
35
- onMediaPress={onMediaPress}
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
- const MAX_FILE_SIZE = 5 * 1024 * 1024;
241
- const oversizedImages = images.filter(
242
- (img) => (img.size || 0) > MAX_FILE_SIZE
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
- if (oversizedImages.length > 0) {
246
- console.warn('ImageAttach', 'Step 5: Some images too large', {
247
- count: oversizedImages.length,
248
- maxSize: MAX_FILE_SIZE,
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('chat:image_too_large'),
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 = 100 * 1024 * 1024; // 100MB for videos
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
  );
@@ -140,6 +140,7 @@ const VideoPlayer = memo((props: Props) => {
140
140
  <KContainer.View
141
141
  style={[styles.videoContainer, memoHeight]}
142
142
  collapsable={false}
143
+ edges={['bottom']}
143
144
  onLayout={handleContainerLayout}
144
145
  >
145
146
  {renderIndicator()}
@@ -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
- // useEffect(() => {
62
- // StatusBar.setHidden(true);
63
- // return () => {
64
- // StatusBar.setHidden(false);
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
- itemLayout: true,
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={!isLoadingEarlier}
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 = 100 * 1024 * 1024; // 100MB
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?',