@droppii-org/chat-mobile 0.2.14 → 0.2.16
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/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/hooks/useImageAttachment.js +34 -63
- package/lib/module/hooks/useImageAttachment.js.map +1 -1
- package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +3 -2
- package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/MediaView/index.js +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/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/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/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 +11 -5
- package/lib/module/utils/videoThumbnail.js.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/hooks/useImageAttachment.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.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/chat-detail/ChatComposer.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/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 +3 -3
- package/src/components/ThreadCard/ThreadCard.tsx +2 -1
- package/src/components/messages/imageMessage/index.tsx +0 -1
- package/src/hooks/useImageAttachment.ts +45 -70
- package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +3 -2
- package/src/index.tsx +8 -1
- package/src/screens/MediaView/index.tsx +1 -1
- package/src/screens/chat-detail/ChatComposer.tsx +9 -3
- package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +81 -25
- package/src/services/attachmentHandlers/imageAttachmentHandler.ts +11 -1
- package/src/services/auth.ts +77 -0
- package/src/services/imageUpload.ts +116 -73
- 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 +14 -3
- 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,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
|
|
@@ -73,25 +73,31 @@ export const useAttachmentSendHandler = ({
|
|
|
73
73
|
);
|
|
74
74
|
|
|
75
75
|
// Auto-generate thumbnails for gallery videos
|
|
76
|
+
// Use unique ID per video to avoid race conditions and path mismatches
|
|
76
77
|
const galleryVideoThumbnails: Record<
|
|
77
78
|
string,
|
|
78
79
|
{ url: string; size: number }
|
|
79
80
|
> = {};
|
|
81
|
+
const failedVideos: string[] = []; // Track failed videos
|
|
80
82
|
|
|
81
83
|
if (uploadedVideosFromGallery.length > 0) {
|
|
82
84
|
console.log(
|
|
83
85
|
'[ChatComposer] Auto-generating thumbnails for gallery videos...'
|
|
84
86
|
);
|
|
85
87
|
|
|
86
|
-
for (
|
|
88
|
+
for (let idx = 0; idx < uploadedVideosFromGallery.length; idx++) {
|
|
89
|
+
const vidItem = uploadedVideosFromGallery[idx]!;
|
|
90
|
+
const videoId = vidItem.uploadedUrl || `video-${idx}`; // Use unique ID instead of path
|
|
87
91
|
let cachedVideoPath: string | null = null;
|
|
92
|
+
|
|
88
93
|
try {
|
|
89
94
|
const originalVideoPath = vidItem.image.path;
|
|
90
|
-
|
|
95
|
+
let videoDuration = (vidItem.image as any).duration || 0;
|
|
91
96
|
const videoMime = (vidItem.image as any).mime;
|
|
92
97
|
const filename = vidItem.image.filename || 'video.mp4';
|
|
93
98
|
|
|
94
|
-
console.log('[ChatComposer] Generating thumbnail for:', {
|
|
99
|
+
console.log('[ChatComposer] Generating thumbnail for video:', {
|
|
100
|
+
index: idx,
|
|
95
101
|
filename,
|
|
96
102
|
originalVideoPath,
|
|
97
103
|
videoDuration,
|
|
@@ -99,9 +105,20 @@ export const useAttachmentSendHandler = ({
|
|
|
99
105
|
uploadedUrl: vidItem.uploadedUrl,
|
|
100
106
|
});
|
|
101
107
|
|
|
108
|
+
// ✅ FIX: Validate duration is reasonable
|
|
109
|
+
if (videoDuration <= 0 || videoDuration > 3600000) {
|
|
110
|
+
// Duration should be between 0 and 1 hour (3600000ms)
|
|
111
|
+
console.warn(
|
|
112
|
+
'[ChatComposer] Invalid duration, using default:',
|
|
113
|
+
videoDuration
|
|
114
|
+
);
|
|
115
|
+
videoDuration = 5000; // Default 5 seconds
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
// Copy video to persistent location
|
|
103
119
|
const ext = filename.split('.').pop() || 'mp4';
|
|
104
|
-
|
|
120
|
+
// ✅ FIX: Use unique ID to avoid race condition
|
|
121
|
+
const cachedFileName = `thumb-cache-${idx}-${Date.now()}-${Math.random()
|
|
105
122
|
.toString(36)
|
|
106
123
|
.slice(2)}.${ext}`;
|
|
107
124
|
cachedVideoPath = `${RNFS.DocumentDirectoryPath}/${cachedFileName}`;
|
|
@@ -114,10 +131,16 @@ export const useAttachmentSendHandler = ({
|
|
|
114
131
|
await RNFS.copyFile(originalVideoPath, cachedVideoPath);
|
|
115
132
|
console.log('[ChatComposer] Video cached successfully');
|
|
116
133
|
|
|
117
|
-
//
|
|
118
|
-
|
|
134
|
+
// ✅ FIX: Choose frame at 30-40% for better representation
|
|
135
|
+
// (not exactly middle, to avoid black frames/transitions)
|
|
136
|
+
const timeMs = Math.min(
|
|
137
|
+
Math.max(1000, Math.floor(videoDuration * 0.35)),
|
|
138
|
+
videoDuration - 1000
|
|
139
|
+
);
|
|
119
140
|
|
|
120
|
-
console.log('[ChatComposer] Thumbnail settings:', {
|
|
141
|
+
console.log('[ChatComposer] Thumbnail settings for video:', {
|
|
142
|
+
index: idx,
|
|
143
|
+
videoDuration,
|
|
121
144
|
timeMs,
|
|
122
145
|
quality: 80,
|
|
123
146
|
videoPath: cachedVideoPath,
|
|
@@ -126,21 +149,27 @@ export const useAttachmentSendHandler = ({
|
|
|
126
149
|
// Generate thumbnail from cached copy
|
|
127
150
|
const thumbnail = await VideoThumbnailUtil.generateThumbnail(
|
|
128
151
|
cachedVideoPath,
|
|
129
|
-
{
|
|
152
|
+
{
|
|
153
|
+
time: timeMs,
|
|
154
|
+
quality: 80,
|
|
155
|
+
cacheId: `thumb-${idx}-${Date.now()}`,
|
|
156
|
+
}
|
|
130
157
|
);
|
|
131
158
|
|
|
132
159
|
if (!thumbnail) {
|
|
133
160
|
throw new Error('Failed to generate thumbnail');
|
|
134
161
|
}
|
|
135
162
|
|
|
136
|
-
console.log('[ChatComposer] Thumbnail generated:', {
|
|
163
|
+
console.log('[ChatComposer] Thumbnail generated for video:', {
|
|
164
|
+
index: idx,
|
|
137
165
|
path: thumbnail.path,
|
|
138
166
|
width: thumbnail.width,
|
|
139
167
|
height: thumbnail.height,
|
|
140
168
|
});
|
|
141
169
|
|
|
142
170
|
// Upload thumbnail
|
|
143
|
-
const baseName =
|
|
171
|
+
const baseName =
|
|
172
|
+
vidItem.image.filename?.split('.')[0] || `video-${idx}`;
|
|
144
173
|
const uploadedThumbnail = await ImageUploadAPI.uploadImageFile(
|
|
145
174
|
{
|
|
146
175
|
path: thumbnail.path,
|
|
@@ -152,6 +181,7 @@ export const useAttachmentSendHandler = ({
|
|
|
152
181
|
},
|
|
153
182
|
(progress) => {
|
|
154
183
|
console.log('[ChatComposer] Thumbnail upload progress:', {
|
|
184
|
+
videoIndex: idx,
|
|
155
185
|
percentage: progress,
|
|
156
186
|
});
|
|
157
187
|
}
|
|
@@ -161,33 +191,41 @@ export const useAttachmentSendHandler = ({
|
|
|
161
191
|
throw new Error('No thumbnail URL returned');
|
|
162
192
|
}
|
|
163
193
|
|
|
164
|
-
// Store
|
|
165
|
-
galleryVideoThumbnails[
|
|
194
|
+
// ✅ FIX: Store with unique ID instead of path
|
|
195
|
+
galleryVideoThumbnails[videoId] = {
|
|
166
196
|
url: uploadedThumbnail.url,
|
|
167
197
|
size: uploadedThumbnail.size || 0,
|
|
168
198
|
};
|
|
169
199
|
|
|
170
|
-
console.log('[ChatComposer] Thumbnail ready:', {
|
|
200
|
+
console.log('[ChatComposer] Thumbnail ready for video:', {
|
|
201
|
+
videoIndex: idx,
|
|
202
|
+
videoId,
|
|
171
203
|
url: uploadedThumbnail.url,
|
|
172
204
|
size: uploadedThumbnail.size,
|
|
173
205
|
});
|
|
174
206
|
console.log(
|
|
175
|
-
`✅ [ChatComposer] THUMBNAIL URL: ${uploadedThumbnail.url}`
|
|
207
|
+
`✅ [ChatComposer] Video #${idx} THUMBNAIL URL: ${uploadedThumbnail.url}`
|
|
176
208
|
);
|
|
177
209
|
} catch (error) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
210
|
+
// ✅ FIX: Skip failed video instead of aborting all
|
|
211
|
+
const errorMsg =
|
|
212
|
+
error instanceof Error ? error.message : String(error);
|
|
213
|
+
console.error(
|
|
214
|
+
'[ChatComposer] Thumbnail generation error for video:',
|
|
215
|
+
idx,
|
|
216
|
+
errorMsg
|
|
217
|
+
);
|
|
218
|
+
failedVideos.push(vidItem.image.filename);
|
|
219
|
+
|
|
220
|
+
// Mark this video as failed (no thumbnail)
|
|
221
|
+
galleryVideoThumbnails[videoId] = { url: '', size: 0 };
|
|
185
222
|
} finally {
|
|
186
223
|
// Cleanup cached video file
|
|
187
224
|
if (cachedVideoPath) {
|
|
188
225
|
try {
|
|
189
226
|
await RNFS.unlink(cachedVideoPath);
|
|
190
227
|
console.log('[ChatComposer] Cleaned up cached video:', {
|
|
228
|
+
videoIndex: idx,
|
|
191
229
|
path: cachedVideoPath,
|
|
192
230
|
});
|
|
193
231
|
} catch (err) {
|
|
@@ -196,6 +234,19 @@ export const useAttachmentSendHandler = ({
|
|
|
196
234
|
}
|
|
197
235
|
}
|
|
198
236
|
}
|
|
237
|
+
|
|
238
|
+
// Show error about failed videos if any
|
|
239
|
+
if (failedVideos.length > 0) {
|
|
240
|
+
UIUtils.toast.open({
|
|
241
|
+
title: 'Thumbnail Generation Failed',
|
|
242
|
+
message: `Failed for: ${failedVideos.join(', ')}. Videos will be sent without thumbnails.`,
|
|
243
|
+
theme: 'warning',
|
|
244
|
+
});
|
|
245
|
+
console.warn(
|
|
246
|
+
'[ChatComposer] Failed to generate thumbnails for:',
|
|
247
|
+
failedVideos
|
|
248
|
+
);
|
|
249
|
+
}
|
|
199
250
|
}
|
|
200
251
|
|
|
201
252
|
const allUploadedVideos = [...uploadedVideosFromGallery];
|
|
@@ -210,10 +261,13 @@ export const useAttachmentSendHandler = ({
|
|
|
210
261
|
|
|
211
262
|
// Log thumbnail URLs
|
|
212
263
|
if (uploadedVideosFromGallery.length > 0) {
|
|
213
|
-
uploadedVideosFromGallery.forEach((vid: any) => {
|
|
214
|
-
|
|
264
|
+
uploadedVideosFromGallery.forEach((vid: any, vidIdx: number) => {
|
|
265
|
+
// ✅ Use same key as storage: uploadedUrl or video-${idx}
|
|
266
|
+
const videoId = vid.uploadedUrl || `video-${vidIdx}`;
|
|
267
|
+
const thumbnail = galleryVideoThumbnails[videoId];
|
|
215
268
|
console.log(`✅ [ChatComposer] Gallery Video Thumbnail:`, {
|
|
216
269
|
filename: vid.image.filename,
|
|
270
|
+
videoId,
|
|
217
271
|
thumbnailUrl: thumbnail?.url || 'NOT FOUND',
|
|
218
272
|
videoUrl: vid.uploadedUrl,
|
|
219
273
|
});
|
|
@@ -228,8 +282,10 @@ export const useAttachmentSendHandler = ({
|
|
|
228
282
|
type: 'image' as const,
|
|
229
283
|
})),
|
|
230
284
|
// Videos from gallery picker with auto-generated thumbnails
|
|
231
|
-
...uploadedVideosFromGallery.map((vid: any) => {
|
|
232
|
-
|
|
285
|
+
...uploadedVideosFromGallery.map((vid: any, vidIdx: number) => {
|
|
286
|
+
// ✅ Use same key as storage: uploadedUrl or video-${idx}
|
|
287
|
+
const videoId = vid.uploadedUrl || `video-${vidIdx}`;
|
|
288
|
+
const thumbnail = galleryVideoThumbnails[videoId];
|
|
233
289
|
return {
|
|
234
290
|
type: 'video' as const,
|
|
235
291
|
video: vid.image,
|
|
@@ -9,7 +9,14 @@ export const ImageAttachmentHandler: IAttachmentHandler = {
|
|
|
9
9
|
return { valid: false, reason: 'Image exceeds 5MB limit' };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const validMimes = [
|
|
12
|
+
const validMimes = [
|
|
13
|
+
'image/jpeg',
|
|
14
|
+
'image/png',
|
|
15
|
+
'image/webp',
|
|
16
|
+
'image/jpg',
|
|
17
|
+
'image/heic',
|
|
18
|
+
'image/heif',
|
|
19
|
+
];
|
|
13
20
|
if (!validMimes.includes(attachment.sourceFile.mimeType)) {
|
|
14
21
|
return {
|
|
15
22
|
valid: false,
|
|
@@ -28,6 +35,7 @@ export const ImageAttachmentHandler: IAttachmentHandler = {
|
|
|
28
35
|
console.log('[ImageAttachmentHandler] Building payload for', {
|
|
29
36
|
filename: attachment.sourceFile.filename,
|
|
30
37
|
url: uploadedUrl,
|
|
38
|
+
// path: attachment.sourceFile.path,
|
|
31
39
|
});
|
|
32
40
|
|
|
33
41
|
const picInfo = {
|
|
@@ -43,10 +51,12 @@ export const ImageAttachmentHandler: IAttachmentHandler = {
|
|
|
43
51
|
sourcePicture: picInfo,
|
|
44
52
|
bigPicture: picInfo,
|
|
45
53
|
snapshotPicture: picInfo,
|
|
54
|
+
sourcePath: attachment.sourceFile.path, // Required by OpenIM SDK to attach image to message
|
|
46
55
|
};
|
|
47
56
|
|
|
48
57
|
console.log('[ImageAttachmentHandler] Payload created for', {
|
|
49
58
|
fileName: attachment.sourceFile.filename,
|
|
59
|
+
sourcePath: attachment.sourceFile.path,
|
|
50
60
|
});
|
|
51
61
|
|
|
52
62
|
return payload;
|
package/src/services/auth.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
let authToken: string | null = null;
|
|
8
|
+
let tokenRefreshCallback: (() => Promise<string>) | null = null;
|
|
9
|
+
let tokenRefreshFailedCallback: (() => void) | null = null;
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Set auth token (call once at app startup)
|
|
@@ -26,9 +28,84 @@ export const getAuthToken = (): string => {
|
|
|
26
28
|
return authToken;
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Set callback for token refresh when 401 Unauthorized occurs
|
|
33
|
+
* Callback should refresh token from server and return the new token
|
|
34
|
+
* Example:
|
|
35
|
+
* setTokenRefreshCallback(async () => {
|
|
36
|
+
* const newToken = await api.post('/auth/refresh');
|
|
37
|
+
* return newToken;
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
export const setTokenRefreshCallback = (
|
|
41
|
+
callback: () => Promise<string>
|
|
42
|
+
): void => {
|
|
43
|
+
tokenRefreshCallback = callback;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Set callback when token refresh fails
|
|
48
|
+
* Called when token refresh throws error or returns invalid token
|
|
49
|
+
* Use this to redirect to login, show error message, etc.
|
|
50
|
+
* Example:
|
|
51
|
+
* setTokenRefreshFailedCallback(() => {
|
|
52
|
+
* navigation.navigate('Login');
|
|
53
|
+
* showToast('Your session expired. Please login again.');
|
|
54
|
+
* });
|
|
55
|
+
*/
|
|
56
|
+
export const setTokenRefreshFailedCallback = (callback: () => void): void => {
|
|
57
|
+
tokenRefreshFailedCallback = callback;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Refresh auth token when 401 Unauthorized occurs
|
|
62
|
+
* Calls the refresh callback and updates the stored token
|
|
63
|
+
* If refresh fails:
|
|
64
|
+
* 1. Clears the invalid token
|
|
65
|
+
* 2. Calls tokenRefreshFailedCallback (if set) to notify app
|
|
66
|
+
* 3. Throws error
|
|
67
|
+
* @returns new auth token
|
|
68
|
+
* @throws Error if callback not set or refresh fails
|
|
69
|
+
*/
|
|
70
|
+
export const refreshAuthToken = async (): Promise<string> => {
|
|
71
|
+
if (!tokenRefreshCallback) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Token refresh callback not set. Call setTokenRefreshCallback() during app initialization.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const newToken = await tokenRefreshCallback();
|
|
79
|
+
if (!newToken) {
|
|
80
|
+
throw new Error('Refresh callback returned empty token');
|
|
81
|
+
}
|
|
82
|
+
setAuthToken(newToken);
|
|
83
|
+
return newToken;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Clear invalid token
|
|
86
|
+
clearAuthToken();
|
|
87
|
+
|
|
88
|
+
// Notify app that refresh failed
|
|
89
|
+
if (tokenRefreshFailedCallback) {
|
|
90
|
+
try {
|
|
91
|
+
tokenRefreshFailedCallback();
|
|
92
|
+
} catch (callbackError) {
|
|
93
|
+
console.error(
|
|
94
|
+
'[Auth] Token refresh failed callback error:',
|
|
95
|
+
callbackError
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Throw original error
|
|
101
|
+
throw error instanceof Error ? error : new Error('Token refresh failed');
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
29
105
|
/**
|
|
30
106
|
* Clear token (logout)
|
|
31
107
|
*/
|
|
32
108
|
export const clearAuthToken = () => {
|
|
33
109
|
authToken = null;
|
|
110
|
+
tokenRefreshCallback = null;
|
|
34
111
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
2
2
|
import RNFS from 'react-native-fs';
|
|
3
3
|
import { getImageUploadEndpoint, getMimeType } from '../utils/imageUtils';
|
|
4
|
-
import { getAuthToken } from './auth';
|
|
4
|
+
import { getAuthToken, refreshAuthToken } from './auth';
|
|
5
5
|
import { getApiEndpoint } from '../config/api-endpoints';
|
|
6
|
-
import {
|
|
6
|
+
import { apiInstance } from './apis';
|
|
7
7
|
import type {
|
|
8
8
|
ImageFile,
|
|
9
9
|
UploadedImage,
|
|
@@ -11,31 +11,12 @@ import type {
|
|
|
11
11
|
} from '../types/imageUpload';
|
|
12
12
|
|
|
13
13
|
export namespace ImageUploadAPI {
|
|
14
|
-
|
|
14
|
+
const performUpload = async (
|
|
15
|
+
uploadUrl: string,
|
|
15
16
|
file: ImageFile,
|
|
17
|
+
token: string,
|
|
16
18
|
onProgress?: (progress: number) => void
|
|
17
|
-
): Promise<
|
|
18
|
-
let token: string;
|
|
19
|
-
let uploadUrl: string;
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
token = getAuthToken();
|
|
23
|
-
const relativePath = getApiEndpoint('imageUpload');
|
|
24
|
-
const baseUrl = getBaseUrl();
|
|
25
|
-
|
|
26
|
-
if (!baseUrl) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
'Image upload base URL not configured. Call initChatSdk() with apiBaseUrl during app initialization.'
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Construct absolute URL for ReactNativeBlobUtil.fetch()
|
|
33
|
-
uploadUrl = getImageUploadEndpoint(`${baseUrl}${relativePath}`);
|
|
34
|
-
} catch (e) {
|
|
35
|
-
if (__DEV__) console.error('[ImageUploadAPI] Auth error:', e);
|
|
36
|
-
throw e;
|
|
37
|
-
}
|
|
38
|
-
|
|
19
|
+
): Promise<any> => {
|
|
39
20
|
return new Promise((resolve, reject) => {
|
|
40
21
|
ReactNativeBlobUtil.fetch(
|
|
41
22
|
'POST',
|
|
@@ -56,57 +37,119 @@ export namespace ImageUploadAPI {
|
|
|
56
37
|
const progress = Math.round((written / total) * 100);
|
|
57
38
|
onProgress?.(progress);
|
|
58
39
|
})
|
|
59
|
-
.then((response) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (__DEV__)
|
|
64
|
-
console.error('[ImageUploadAPI] Server error response', {
|
|
65
|
-
status,
|
|
66
|
-
data: response.data,
|
|
67
|
-
});
|
|
68
|
-
reject(new Error(`Upload failed with status ${status}`));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
40
|
+
.then((response) => resolve(response))
|
|
41
|
+
.catch((error) => reject(error));
|
|
42
|
+
});
|
|
43
|
+
};
|
|
71
44
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
45
|
+
const parseUploadResponse = (responseData: string): UploadedImage | null => {
|
|
46
|
+
try {
|
|
47
|
+
const json = JSON.parse(responseData);
|
|
48
|
+
const uploaded = json.data?.[0] || json.files?.[0];
|
|
49
|
+
const imageUrl = uploaded?.publicUrl || uploaded?.url || uploaded?.data;
|
|
50
|
+
if (imageUrl) {
|
|
51
|
+
return {
|
|
52
|
+
url: imageUrl,
|
|
53
|
+
name: uploaded?.fileName,
|
|
54
|
+
width: uploaded?.width,
|
|
55
|
+
height: uploaded?.height,
|
|
56
|
+
size: uploaded?.size,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (__DEV__) console.error('[ImageUploadAPI] Parse error:', err);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
81
65
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
66
|
+
export const uploadImageFile = async (
|
|
67
|
+
file: ImageFile,
|
|
68
|
+
onProgress?: (progress: number) => void
|
|
69
|
+
): Promise<UploadedImage> => {
|
|
70
|
+
let token: string;
|
|
71
|
+
let uploadUrl: string;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
token = getAuthToken();
|
|
75
|
+
const relativePath = getApiEndpoint('imageUpload');
|
|
76
|
+
const baseUrl = apiInstance?.defaults?.baseURL;
|
|
77
|
+
|
|
78
|
+
if (!baseUrl) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'Image upload base URL not configured. Call initApiInstance() during app initialization.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
uploadUrl = getImageUploadEndpoint(`${baseUrl}${relativePath}`);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (__DEV__) console.error('[ImageUploadAPI] Auth error:', e);
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let response = await performUpload(uploadUrl, file, token, onProgress);
|
|
91
|
+
const status = response.respInfo?.status;
|
|
92
|
+
|
|
93
|
+
// Handle 401 Unauthorized - refresh token and retry once
|
|
94
|
+
if (status === 401) {
|
|
95
|
+
if (__DEV__)
|
|
96
|
+
console.warn('[ImageUploadAPI] Got 401, attempting token refresh...');
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const newToken = await refreshAuthToken();
|
|
100
|
+
if (__DEV__)
|
|
101
|
+
console.log('[ImageUploadAPI] Token refreshed, retrying upload...');
|
|
102
|
+
|
|
103
|
+
// Retry upload with new token
|
|
104
|
+
response = await performUpload(uploadUrl, file, newToken, onProgress);
|
|
105
|
+
const retryStatus = response.respInfo?.status;
|
|
106
|
+
|
|
107
|
+
if (retryStatus === 401) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'Auth token still invalid after refresh. (401 Unauthorized)'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (retryStatus && retryStatus >= 400) {
|
|
114
|
+
throw new Error(`Upload failed with status ${retryStatus}`);
|
|
115
|
+
}
|
|
116
|
+
} catch (refreshError) {
|
|
117
|
+
if (__DEV__)
|
|
118
|
+
console.error('[ImageUploadAPI] Token refresh failed:', refreshError);
|
|
119
|
+
throw refreshError instanceof Error
|
|
120
|
+
? refreshError
|
|
121
|
+
: new Error('Token refresh failed');
|
|
122
|
+
}
|
|
123
|
+
} else if (status && status >= 400) {
|
|
124
|
+
if (__DEV__)
|
|
125
|
+
console.error('[ImageUploadAPI] Server error response', {
|
|
126
|
+
status,
|
|
127
|
+
data: response.data,
|
|
108
128
|
});
|
|
109
|
-
|
|
129
|
+
throw new Error(`Upload failed with status ${status}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!response.data || response.data.length === 0) {
|
|
133
|
+
if (__DEV__)
|
|
134
|
+
console.error('[ImageUploadAPI] Empty response', {
|
|
135
|
+
dataType: typeof response.data,
|
|
136
|
+
data: response.data,
|
|
137
|
+
});
|
|
138
|
+
throw new Error('Empty response from server');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const uploadedImage = parseUploadResponse(response.data);
|
|
142
|
+
if (!uploadedImage) {
|
|
143
|
+
throw new Error('No URL in response');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
url: uploadedImage.url,
|
|
148
|
+
name: uploadedImage.name,
|
|
149
|
+
width: file.width,
|
|
150
|
+
height: file.height,
|
|
151
|
+
size: file.size,
|
|
152
|
+
};
|
|
110
153
|
};
|
|
111
154
|
|
|
112
155
|
export const uploadImageQueue = async (
|
|
@@ -139,6 +139,7 @@ class ChatImageDimens {
|
|
|
139
139
|
/**
|
|
140
140
|
* Get optimized image URL with custom dimensions (generic approach)
|
|
141
141
|
* Useful for dynamic sizing based on actual content
|
|
142
|
+
* Automatically applies pixel ratio for high-DPI displays
|
|
142
143
|
*/
|
|
143
144
|
public static getOptimizedImageUrlWithDimensions(
|
|
144
145
|
url: string | undefined,
|
|
@@ -154,6 +155,20 @@ class ChatImageDimens {
|
|
|
154
155
|
return url;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Check if image format is supported for WebP conversion
|
|
159
|
+
// Only convert standard formats (jpg, jpeg, png) to WebP
|
|
160
|
+
// For unsupported formats (heic, heif, etc.), return original URL
|
|
161
|
+
const unsupportedFormats = ['.heic', '.heif'];
|
|
162
|
+
const urlLower = url.toLowerCase();
|
|
163
|
+
const isUnsupportedFormat = unsupportedFormats.some((format) =>
|
|
164
|
+
urlLower.includes(format)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (isUnsupportedFormat) {
|
|
168
|
+
// Return original URL without optimization for unsupported formats
|
|
169
|
+
return url;
|
|
170
|
+
}
|
|
171
|
+
|
|
157
172
|
const domainRegex = /^(https?:\/\/[^/]+)/;
|
|
158
173
|
const domainMatches = url.match(domainRegex);
|
|
159
174
|
|
|
@@ -164,8 +179,13 @@ class ChatImageDimens {
|
|
|
164
179
|
const domain = domainMatches[1];
|
|
165
180
|
const restOfUrl = url.replace(domain as string, '');
|
|
166
181
|
|
|
182
|
+
// Account for device pixel ratio: request higher resolution for high-DPI displays
|
|
183
|
+
const pixelRatio = PixelRatio.get();
|
|
184
|
+
const optimizedWidth = Math.round(width * pixelRatio);
|
|
185
|
+
const optimizedHeight = Math.round(height * pixelRatio);
|
|
186
|
+
|
|
167
187
|
// Build optimized URL: domain/cache/WIDTHxHEIGHT/path.webp
|
|
168
|
-
let optimizedUrl = `${domain}/cache/${
|
|
188
|
+
let optimizedUrl = `${domain}/cache/${optimizedWidth}x${optimizedHeight}${restOfUrl}`;
|
|
169
189
|
|
|
170
190
|
if (!restOfUrl.endsWith('.webp')) {
|
|
171
191
|
optimizedUrl += '.webp';
|