@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.
- 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/ThreadCard/ThreadCard.js +2 -1
- package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
- package/lib/module/components/messages/imageMessage/index.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 +50 -71
- package/lib/module/hooks/useImageAttachment.js.map +1 -1
- package/lib/module/hooks/useVideoAttachment.js +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +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 +8 -10
- package/lib/module/screens/MediaView/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +10 -3
- package/lib/module/screens/chat-detail/ChatComposer.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/screens/chat-detail/hooks/useAttachmentSendHandler.js +66 -25
- package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +1 -1
- package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js +6 -3
- package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js.map +1 -1
- package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js +1 -1
- package/lib/module/services/auth.js +69 -0
- package/lib/module/services/auth.js.map +1 -1
- package/lib/module/services/imageUpload.js +84 -58
- package/lib/module/services/imageUpload.js.map +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/chatImageDimens.js +18 -1
- package/lib/module/utils/chatImageDimens.js.map +1 -1
- package/lib/module/utils/dateTimeUtils.js +44 -0
- package/lib/module/utils/dateTimeUtils.js.map +1 -0
- package/lib/module/utils/device.js +2 -1
- package/lib/module/utils/device.js.map +1 -1
- package/lib/module/utils/imageUtils.js +4 -2
- package/lib/module/utils/imageUtils.js.map +1 -1
- package/lib/module/{components/ThreadCard/thread-card.utils.js → utils/messageUtils.js} +18 -42
- package/lib/module/utils/messageUtils.js.map +1 -0
- package/lib/module/utils/videoThumbnail.js +86 -7
- 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/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/imageMessage/index.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/index.d.ts +1 -1
- package/lib/typescript/src/index.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/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/imageAttachmentHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/auth.d.ts +32 -0
- package/lib/typescript/src/services/auth.d.ts.map +1 -1
- package/lib/typescript/src/services/imageUpload.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/utils/chatImageDimens.d.ts +1 -0
- package/lib/typescript/src/utils/chatImageDimens.d.ts.map +1 -1
- package/lib/typescript/src/utils/dateTimeUtils.d.ts +6 -0
- package/lib/typescript/src/utils/dateTimeUtils.d.ts.map +1 -0
- package/lib/typescript/src/utils/device.d.ts.map +1 -1
- package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
- package/lib/typescript/src/utils/messageUtils.d.ts +8 -0
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -0
- package/lib/typescript/src/utils/videoThumbnail.d.ts +1 -0
- package/lib/typescript/src/utils/videoThumbnail.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/AttachmentPreview.tsx +11 -5
- package/src/components/MergedImageGrid.tsx +36 -23
- package/src/components/ThreadCard/ThreadCard.tsx +2 -1
- package/src/components/messages/imageMessage/index.tsx +0 -1
- package/src/components/messages/mergedMessage/index.tsx +5 -7
- package/src/hooks/useImageAttachment.ts +67 -82
- package/src/hooks/useVideoAttachment.ts +1 -1
- package/src/index.tsx +8 -1
- package/src/screens/MediaView/VideoPlayer.tsx +1 -0
- package/src/screens/MediaView/index.tsx +8 -7
- package/src/screens/chat-detail/ChatComposer.tsx +9 -3
- package/src/screens/chat-detail/ChatListLegend.tsx +9 -2
- package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +81 -25
- package/src/services/attachmentHandlers/imageAttachmentHandler.ts +11 -1
- package/src/services/attachmentHandlers/videoAttachmentHandler.ts +1 -1
- package/src/services/auth.ts +77 -0
- package/src/services/imageUpload.ts +116 -73
- package/src/translation/resources/i18n.ts +1 -0
- package/src/utils/chatImageDimens.ts +21 -1
- package/src/utils/dateTimeUtils.ts +48 -0
- package/src/utils/device.ts +1 -0
- package/src/utils/imageUtils.ts +10 -2
- package/src/{components/ThreadCard/thread-card.utils.ts → utils/messageUtils.ts} +18 -56
- package/src/utils/videoThumbnail.ts +90 -5
- package/lib/module/components/ThreadCard/thread-card.utils.js.map +0 -1
- package/lib/typescript/src/components/ThreadCard/thread-card.utils.d.ts +0 -4
- 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 {
|
|
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',
|
|
@@ -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
|
|
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 {
|
|
@@ -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>
|
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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', '
|
|
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:
|
|
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', '
|
|
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', '
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
const timeMs = Math.min(
|
|
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
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
{
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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(
|
|
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 =
|
|
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 {
|
|
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';
|
|
@@ -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') {
|
|
@@ -249,7 +250,7 @@ const styles = StyleSheet.create({
|
|
|
249
250
|
right: 0,
|
|
250
251
|
zIndex: 100,
|
|
251
252
|
minHeight: 100,
|
|
252
|
-
paddingTop:
|
|
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
|
-
|
|
244
|
+
debouncedImagePickerRef.current();
|
|
239
245
|
} else {
|
|
240
246
|
onAttachmentAction?.(action);
|
|
241
247
|
}
|
|
242
248
|
},
|
|
243
|
-
[onAttachmentAction
|
|
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
|
-
|
|
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}
|