@droppii-org/chat-mobile 0.2.15 → 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 (102) 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/ThreadCard/ThreadCard.js +2 -1
  6. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  7. package/lib/module/components/messages/imageMessage/index.js.map +1 -1
  8. package/lib/module/components/messages/mergedMessage/index.js +4 -7
  9. package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
  10. package/lib/module/hooks/useImageAttachment.js +50 -71
  11. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  12. package/lib/module/hooks/useVideoAttachment.js +1 -1
  13. package/lib/module/index.js +1 -1
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/module/screens/MediaView/VideoPlayer.js +1 -0
  16. package/lib/module/screens/MediaView/VideoPlayer.js.map +1 -1
  17. package/lib/module/screens/MediaView/index.js +8 -10
  18. package/lib/module/screens/MediaView/index.js.map +1 -1
  19. package/lib/module/screens/chat-detail/ChatComposer.js +10 -3
  20. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  21. package/lib/module/screens/chat-detail/ChatListLegend.js +11 -2
  22. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  23. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js +66 -25
  24. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +1 -1
  25. package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js +6 -3
  26. package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js.map +1 -1
  27. package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js +1 -1
  28. package/lib/module/services/auth.js +69 -0
  29. package/lib/module/services/auth.js.map +1 -1
  30. package/lib/module/services/imageUpload.js +84 -58
  31. package/lib/module/services/imageUpload.js.map +1 -1
  32. package/lib/module/translation/resources/i18n.js +1 -0
  33. package/lib/module/translation/resources/i18n.js.map +1 -1
  34. package/lib/module/utils/chatImageDimens.js +18 -1
  35. package/lib/module/utils/chatImageDimens.js.map +1 -1
  36. package/lib/module/utils/dateTimeUtils.js +44 -0
  37. package/lib/module/utils/dateTimeUtils.js.map +1 -0
  38. package/lib/module/utils/device.js +2 -1
  39. package/lib/module/utils/device.js.map +1 -1
  40. package/lib/module/utils/imageUtils.js +4 -2
  41. package/lib/module/utils/imageUtils.js.map +1 -1
  42. package/lib/module/{components/ThreadCard/thread-card.utils.js → utils/messageUtils.js} +18 -42
  43. package/lib/module/utils/messageUtils.js.map +1 -0
  44. package/lib/module/utils/videoThumbnail.js +86 -7
  45. package/lib/module/utils/videoThumbnail.js.map +1 -1
  46. package/lib/typescript/src/components/AttachmentPreview.d.ts.map +1 -1
  47. package/lib/typescript/src/components/MergedImageGrid.d.ts +2 -2
  48. package/lib/typescript/src/components/MergedImageGrid.d.ts.map +1 -1
  49. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  50. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  51. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  52. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  53. package/lib/typescript/src/index.d.ts +1 -1
  54. package/lib/typescript/src/index.d.ts.map +1 -1
  55. package/lib/typescript/src/screens/MediaView/VideoPlayer.d.ts.map +1 -1
  56. package/lib/typescript/src/screens/MediaView/index.d.ts.map +1 -1
  57. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  58. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  59. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +1 -1
  60. package/lib/typescript/src/services/attachmentHandlers/imageAttachmentHandler.d.ts.map +1 -1
  61. package/lib/typescript/src/services/auth.d.ts +32 -0
  62. package/lib/typescript/src/services/auth.d.ts.map +1 -1
  63. package/lib/typescript/src/services/imageUpload.d.ts.map +1 -1
  64. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  65. package/lib/typescript/src/utils/chatImageDimens.d.ts +1 -0
  66. package/lib/typescript/src/utils/chatImageDimens.d.ts.map +1 -1
  67. package/lib/typescript/src/utils/dateTimeUtils.d.ts +6 -0
  68. package/lib/typescript/src/utils/dateTimeUtils.d.ts.map +1 -0
  69. package/lib/typescript/src/utils/device.d.ts.map +1 -1
  70. package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
  71. package/lib/typescript/src/utils/messageUtils.d.ts +8 -0
  72. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -0
  73. package/lib/typescript/src/utils/videoThumbnail.d.ts +1 -0
  74. package/lib/typescript/src/utils/videoThumbnail.d.ts.map +1 -1
  75. package/package.json +2 -2
  76. package/src/components/AttachmentPreview.tsx +11 -5
  77. package/src/components/MergedImageGrid.tsx +36 -23
  78. package/src/components/ThreadCard/ThreadCard.tsx +2 -1
  79. package/src/components/messages/imageMessage/index.tsx +0 -1
  80. package/src/components/messages/mergedMessage/index.tsx +5 -7
  81. package/src/hooks/useImageAttachment.ts +67 -82
  82. package/src/hooks/useVideoAttachment.ts +1 -1
  83. package/src/index.tsx +8 -1
  84. package/src/screens/MediaView/VideoPlayer.tsx +1 -0
  85. package/src/screens/MediaView/index.tsx +8 -7
  86. package/src/screens/chat-detail/ChatComposer.tsx +9 -3
  87. package/src/screens/chat-detail/ChatListLegend.tsx +9 -2
  88. package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +81 -25
  89. package/src/services/attachmentHandlers/imageAttachmentHandler.ts +11 -1
  90. package/src/services/attachmentHandlers/videoAttachmentHandler.ts +1 -1
  91. package/src/services/auth.ts +77 -0
  92. package/src/services/imageUpload.ts +116 -73
  93. package/src/translation/resources/i18n.ts +1 -0
  94. package/src/utils/chatImageDimens.ts +21 -1
  95. package/src/utils/dateTimeUtils.ts +48 -0
  96. package/src/utils/device.ts +1 -0
  97. package/src/utils/imageUtils.ts +10 -2
  98. package/src/{components/ThreadCard/thread-card.utils.ts → utils/messageUtils.ts} +18 -56
  99. package/src/utils/videoThumbnail.ts +90 -5
  100. package/lib/module/components/ThreadCard/thread-card.utils.js.map +0 -1
  101. package/lib/typescript/src/components/ThreadCard/thread-card.utils.d.ts +0 -4
  102. package/lib/typescript/src/components/ThreadCard/thread-card.utils.d.ts.map +0 -1
@@ -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',
@@ -4,7 +4,8 @@ import { KContainer, KImage, KLabel, KColors } from '@droppii/libs';
4
4
  import { AvatarSection } from './AvatarSection';
5
5
  import { NamePrefixIcon } from './NamePrefixIcon';
6
6
  import { UnreadBadge } from './UnreadBadge';
7
- import { formatTimestamp, getLastMessageText } from './thread-card.utils';
7
+ import { formatTimestamp } from '../../utils/dateTimeUtils';
8
+ import { getLastMessageText } from '../../utils/messageUtils';
8
9
  import { useConversation, useUserStore } from '../../store';
9
10
 
10
11
  interface ThreadCardProps {
@@ -84,7 +84,6 @@ export const ImageMessage = memo(
84
84
  imageDimensions.width,
85
85
  imageDimensions.height
86
86
  );
87
-
88
87
  return (
89
88
  <KContainer.View style={[messageStyles.wrapper, styles.wrapper]}>
90
89
  <KContainer.Touchable
@@ -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>
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useState } from 'react';
2
- import RNFS from 'react-native-fs';
3
2
  import type { Image as PickedImage } from 'react-native-image-crop-picker';
4
3
  import DeviceUtils from '../utils/device';
5
4
  import UIUtils from '../utils/ui';
@@ -48,7 +47,6 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
48
47
  const uploadImage = useCallback(
49
48
  async (index: number, image: PickedImage) => {
50
49
  console.log('Upload', 'Step 1: Starting upload for image', { index });
51
- let cachedFilePath: string | null = null;
52
50
 
53
51
  try {
54
52
  console.log('Upload', 'Step 2: Updating status to uploading...');
@@ -58,27 +56,10 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
58
56
  uploadProgress: 0,
59
57
  });
60
58
 
61
- try {
62
- console.log('Upload', 'Step 3: Preparing file...', {
63
- originalPath: image.path,
64
- filename: image.filename,
65
- });
66
- const ext = image.filename?.split('.').pop() || 'jpg';
67
- const fileName = `temp-image-${Date.now()}-${index}.${ext}`;
68
- cachedFilePath = `${RNFS.DocumentDirectoryPath}/${fileName}`;
69
-
70
- console.log('Upload', 'Step 4: Copying file to cache...', {
71
- from: image.path,
72
- to: cachedFilePath,
73
- });
74
- await RNFS.copyFile(image.path, cachedFilePath);
75
- console.log('Upload', 'Step 5: File copied successfully');
76
- } catch (fileError: any) {
77
- console.error('Upload', 'File copy error', fileError);
78
- throw new Error(
79
- `Failed to prepare file: ${fileError?.message || 'Unknown error'}`
80
- );
81
- }
59
+ // ✅ OPTIMIZED: Use original path directly, no copy
60
+ const filePath = image.path.startsWith('file://')
61
+ ? image.path.replace('file://', '')
62
+ : image.path;
82
63
 
83
64
  const filename = image.filename || 'image.jpg';
84
65
 
@@ -93,13 +74,14 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
93
74
 
94
75
  let uploadedImage;
95
76
  try {
96
- console.log('Upload', 'Step 6: Uploading to server...', {
77
+ console.log('Upload', 'Uploading to server...', {
97
78
  filename,
98
79
  size: image.size,
80
+ filePath,
99
81
  });
100
82
  uploadedImage = await ImageUploadAPI.uploadImageFile(
101
83
  {
102
- path: cachedFilePath || '',
84
+ path: filePath,
103
85
  filename,
104
86
  size: image.size || 0,
105
87
  width: image.width || 0,
@@ -108,7 +90,7 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
108
90
  },
109
91
  setProgress
110
92
  );
111
- console.log('Upload', 'Step 7: Upload successful!', {
93
+ console.log('Upload', 'Upload successful!', {
112
94
  url: uploadedImage?.url,
113
95
  });
114
96
  } catch (uploadError: any) {
@@ -122,27 +104,14 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
122
104
  throw new Error('No URL returned from server');
123
105
  }
124
106
 
125
- console.log('Upload', 'Step 8: Upload complete, preparing data...');
107
+ console.log('Upload', 'Upload complete, updating status...');
126
108
  updateImageStatus(index, {
127
109
  isUploading: false,
128
110
  uploadProgress: 100,
129
111
  uploadedUrl: uploadedImage.url,
130
112
  });
131
113
 
132
- console.log(
133
- 'Upload',
134
- 'Step 9: Ready to send - waiting for user confirmation'
135
- );
136
-
137
- console.log('Upload', 'Step 11: Cleaning up cache file...');
138
- try {
139
- if (cachedFilePath) {
140
- await ImageUploadAPI.cleanupImageFile(cachedFilePath);
141
- }
142
- } catch (cleanupError) {
143
- console.warn('Upload', 'Cleanup warning', cleanupError);
144
- }
145
- console.log('Upload', 'Step 12: COMPLETE ✓');
114
+ console.log('Upload', 'COMPLETE ✓');
146
115
  } catch (error: any) {
147
116
  console.error('Upload', 'ERROR', error);
148
117
  const errorMsg = error?.message || trans('chat:image_upload_failed');
@@ -151,14 +120,6 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
151
120
  error: errorMsg,
152
121
  });
153
122
 
154
- try {
155
- if (cachedFilePath) {
156
- await ImageUploadAPI.cleanupImageFile(cachedFilePath);
157
- }
158
- } catch (cleanupError) {
159
- console.warn('Upload', 'Cleanup after error', cleanupError);
160
- }
161
-
162
123
  console.error('uploadImage', 'Full error', error);
163
124
  }
164
125
  },
@@ -168,27 +129,48 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
168
129
  const generateVideoThumbnail = useCallback(
169
130
  async (index: number, image: PickedImage) => {
170
131
  if (!isVideoFromPicker(image)) return; // Only for videos
132
+ console.log('[ImageAttach] Generating thumbnail for video:', image);
171
133
 
172
134
  try {
173
- console.log('[ImageAttach] Generating thumbnail for video:', {
174
- index,
175
- filename: image.filename,
176
- });
135
+ let videoDuration = (image as any).duration || 0;
136
+
137
+ // ✅ FIX: Validate duration is reasonable
138
+ if (videoDuration <= 0 || videoDuration > 3600000) {
139
+ // Duration should be between 0 and 1 hour (3600000ms)
140
+ console.warn(
141
+ '[ImageAttach] Invalid duration, using default:',
142
+ videoDuration
143
+ );
144
+ videoDuration = 5000; // Default 5 seconds
145
+ }
177
146
 
178
- const videoDuration = (image as any).duration || 0;
179
- const timeMs = Math.min(1000, Math.floor(videoDuration / 2));
147
+ // FIX: Choose frame at 35% for better representation
148
+ const timeMs = Math.min(
149
+ Math.max(1000, Math.floor(videoDuration * 0.35)),
150
+ videoDuration - 1000
151
+ );
180
152
 
181
- // Copy video to safe location
182
- const ext = image.filename?.split('.').pop() || 'mp4';
183
- const cachedFileName = `thumb-cache-${Date.now()}-${index}.${ext}`;
184
- const cachedVideoPath = `${RNFS.DocumentDirectoryPath}/${cachedFileName}`;
153
+ // OPTIMIZED: Use original path directly, no copy
154
+ const videoPath = image.path.startsWith('file://')
155
+ ? image.path.replace('file://', '')
156
+ : image.path;
185
157
 
186
- await RNFS.copyFile(image.path, cachedVideoPath);
158
+ console.log('[ImageAttach] Generating thumbnail for video:', {
159
+ filename: image.filename,
160
+ videoPath,
161
+ videoDuration,
162
+ timeMs,
163
+ });
187
164
 
188
- // Generate thumbnail
165
+ // Generate thumbnail directly from original path
166
+ console.log('[ImageAttach] Generating thumbnail at timeMs:', timeMs);
189
167
  const thumbnail = await VideoThumbnailUtil.generateThumbnail(
190
- cachedVideoPath,
191
- { time: timeMs, quality: 80 }
168
+ videoPath,
169
+ {
170
+ time: timeMs,
171
+ quality: 80,
172
+ cacheId: `thumb-${index}-${Date.now()}`,
173
+ }
192
174
  );
193
175
 
194
176
  if (!thumbnail) throw new Error('Thumbnail generation failed');
@@ -225,13 +207,6 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
225
207
  console.log(
226
208
  `✅ [ImageAttach] THUMBNAIL URL (picked): ${uploadedThumbnail.url}`
227
209
  );
228
-
229
- // Cleanup cached video
230
- try {
231
- await RNFS.unlink(cachedVideoPath);
232
- } catch (err) {
233
- console.warn('[ImageAttach] Failed to clean cached video:', err);
234
- }
235
210
  } catch (error) {
236
211
  console.error('[ImageAttach] Video thumbnail generation error:', error);
237
212
  UIUtils.toast.open({
@@ -262,23 +237,33 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
262
237
  count: images.length,
263
238
  });
264
239
 
265
- const MAX_FILE_SIZE = 5 * 1024 * 1024;
266
- const oversizedImages = images.filter(
267
- (img) => (img.size || 0) > MAX_FILE_SIZE
268
- );
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
+ };
269
250
 
270
- if (oversizedImages.length > 0) {
271
- console.warn('ImageAttach', 'Step 5: Some images too large', {
272
- count: oversizedImages.length,
273
- 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,
274
257
  });
275
258
  UIUtils.toast.open({
276
- title: trans('chat:image_too_large'),
259
+ title: trans(
260
+ hasOversizedVideo
261
+ ? 'chat:video_too_large'
262
+ : 'chat:image_too_large'
263
+ ),
277
264
  theme: 'danger',
278
265
  });
279
- const validImages = images.filter(
280
- (img) => (img.size || 0) <= MAX_FILE_SIZE
281
- );
266
+ const validImages = images.filter(isWithinLimit);
282
267
  if (validImages.length === 0) return;
283
268
  images.splice(0, images.length, ...validImages);
284
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
  );
package/src/index.tsx CHANGED
@@ -1,5 +1,12 @@
1
1
  export { ChatAPI } from './services';
2
- export { setAuthToken, getAuthToken, clearAuthToken } from './services/auth';
2
+ export {
3
+ setAuthToken,
4
+ getAuthToken,
5
+ clearAuthToken,
6
+ setTokenRefreshCallback,
7
+ setTokenRefreshFailedCallback,
8
+ refreshAuthToken,
9
+ } from './services/auth';
3
10
  export { initBaseUrl, getBaseUrl } from './config/configuration';
4
11
  export { Config } from './config';
5
12
  export * from './core';
@@ -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') {
@@ -249,7 +250,7 @@ const styles = StyleSheet.create({
249
250
  right: 0,
250
251
  zIndex: 100,
251
252
  minHeight: 100,
252
- paddingTop: 20,
253
+ paddingTop: 30,
253
254
  },
254
255
  // Figma: Header-First-Row — flex row, padding 8, alignItems center
255
256
  headerRow: {
@@ -1,5 +1,6 @@
1
- import { memo, useCallback, useEffect, useMemo } from 'react';
1
+ import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
2
2
  import { Platform, StyleSheet } from 'react-native';
3
+ import { debounce } from 'lodash';
3
4
  import Reanimated from 'react-native-reanimated';
4
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
6
  import { KContainer, KColors, KImage, KButton } from '@droppii/libs';
@@ -231,16 +232,21 @@ export const ChatComposer = memo(
231
232
  }
232
233
  }, [value, setIsInputWrapped]);
233
234
 
235
+ // Debounced image picker (prevent rapid clicks)
236
+ const debouncedImagePickerRef = useRef(
237
+ debounce(handleImageAttach, 300, { leading: true, trailing: false })
238
+ );
239
+
234
240
  // Attachment action handler
235
241
  const handleAttachmentActionInternal = useCallback(
236
242
  async (action: any) => {
237
243
  if (action.id === 'gallery') {
238
- await handleImageAttach();
244
+ debouncedImagePickerRef.current();
239
245
  } else {
240
246
  onAttachmentAction?.(action);
241
247
  }
242
248
  },
243
- [onAttachmentAction, handleImageAttach]
249
+ [onAttachmentAction]
244
250
  );
245
251
 
246
252
  // Input row padding
@@ -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}