@droppii-org/chat-mobile 0.2.23 → 0.2.24

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 (93) hide show
  1. package/lib/module/config/uploadLimits.js +13 -0
  2. package/lib/module/config/uploadLimits.js.map +1 -0
  3. package/lib/module/hooks/useChatCompose.js +142 -0
  4. package/lib/module/hooks/useChatCompose.js.map +1 -0
  5. package/lib/module/hooks/useImageAttachment.js +71 -151
  6. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  7. package/lib/module/hooks/useSendAttachment.js +2 -2
  8. package/lib/module/hooks/useSendAttachment.js.map +1 -1
  9. package/lib/module/hooks/useVideoAttachment.js +4 -4
  10. package/lib/module/hooks/useVideoAttachment.js.map +1 -1
  11. package/lib/module/index.js +0 -1
  12. package/lib/module/index.js.map +1 -1
  13. package/lib/module/screens/chat-detail/ChatComposer.js +50 -185
  14. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  15. package/lib/module/screens/chat-detail/ChatDetail.js +16 -137
  16. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  17. package/lib/module/screens/chat-detail/index.js +0 -1
  18. package/lib/module/screens/chat-detail/index.js.map +1 -1
  19. package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js +2 -2
  20. package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js.map +1 -1
  21. package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js +3 -3
  22. package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js.map +1 -1
  23. package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js +2 -1
  24. package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js.map +1 -1
  25. package/lib/module/services/attachmentOrchestrator.js +6 -5
  26. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  27. package/lib/module/utils/imageUtils.js +3 -3
  28. package/lib/module/utils/imageUtils.js.map +1 -1
  29. package/lib/module/utils/pLimit.js +19 -0
  30. package/lib/module/utils/pLimit.js.map +1 -0
  31. package/lib/module/utils/videoThumbnail.js +8 -3
  32. package/lib/module/utils/videoThumbnail.js.map +1 -1
  33. package/lib/module/utils/videoThumbnailUpload.js +76 -0
  34. package/lib/module/utils/videoThumbnailUpload.js.map +1 -0
  35. package/lib/typescript/src/config/uploadLimits.d.ts +7 -0
  36. package/lib/typescript/src/config/uploadLimits.d.ts.map +1 -0
  37. package/lib/typescript/src/hooks/useChatCompose.d.ts +27 -0
  38. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -0
  39. package/lib/typescript/src/hooks/useImageAttachment.d.ts +2 -2
  40. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  41. package/lib/typescript/src/hooks/useSendAttachment.d.ts +1 -1
  42. package/lib/typescript/src/hooks/useSendAttachment.d.ts.map +1 -1
  43. package/lib/typescript/src/hooks/useVideoAttachment.d.ts.map +1 -1
  44. package/lib/typescript/src/index.d.ts +0 -2
  45. package/lib/typescript/src/index.d.ts.map +1 -1
  46. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +5 -2
  47. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  48. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  49. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  50. package/lib/typescript/src/screens/chat-detail/index.d.ts +0 -2
  51. package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
  52. package/lib/typescript/src/screens/chat-detail/types.d.ts +1 -35
  53. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  54. package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts.map +1 -1
  55. package/lib/typescript/src/services/attachmentHandlers/imageAttachmentHandler.d.ts.map +1 -1
  56. package/lib/typescript/src/services/attachmentHandlers/videoAttachmentHandler.d.ts.map +1 -1
  57. package/lib/typescript/src/services/attachmentOrchestrator.d.ts +1 -1
  58. package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
  59. package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
  60. package/lib/typescript/src/utils/pLimit.d.ts +6 -0
  61. package/lib/typescript/src/utils/pLimit.d.ts.map +1 -0
  62. package/lib/typescript/src/utils/videoThumbnail.d.ts.map +1 -1
  63. package/lib/typescript/src/utils/videoThumbnailUpload.d.ts +11 -0
  64. package/lib/typescript/src/utils/videoThumbnailUpload.d.ts.map +1 -0
  65. package/package.json +1 -1
  66. package/src/config/uploadLimits.ts +11 -0
  67. package/src/hooks/useChatCompose.ts +186 -0
  68. package/src/hooks/useImageAttachment.ts +110 -197
  69. package/src/hooks/useSendAttachment.ts +3 -6
  70. package/src/hooks/useVideoAttachment.ts +4 -4
  71. package/src/index.tsx +1 -5
  72. package/src/screens/chat-detail/ChatComposer.tsx +88 -240
  73. package/src/screens/chat-detail/ChatDetail.tsx +24 -195
  74. package/src/screens/chat-detail/index.ts +1 -5
  75. package/src/screens/chat-detail/types.ts +1 -38
  76. package/src/services/attachmentHandlers/fileAttachmentHandler.ts +3 -2
  77. package/src/services/attachmentHandlers/imageAttachmentHandler.ts +6 -3
  78. package/src/services/attachmentHandlers/videoAttachmentHandler.ts +2 -1
  79. package/src/services/attachmentOrchestrator.ts +6 -8
  80. package/src/utils/imageUtils.ts +3 -4
  81. package/src/utils/pLimit.ts +20 -0
  82. package/src/utils/videoThumbnail.ts +10 -3
  83. package/src/utils/videoThumbnailUpload.ts +103 -0
  84. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js +0 -288
  85. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +0 -1
  86. package/lib/module/utils/giftedChatMessage.js +0 -90
  87. package/lib/module/utils/giftedChatMessage.js.map +0 -1
  88. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts +0 -26
  89. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +0 -1
  90. package/lib/typescript/src/utils/giftedChatMessage.d.ts +0 -10
  91. package/lib/typescript/src/utils/giftedChatMessage.d.ts.map +0 -1
  92. package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +0 -386
  93. package/src/utils/giftedChatMessage.ts +0 -137
@@ -1,4 +1,4 @@
1
- import { memo, useState, useCallback, useEffect, useMemo, useRef } from 'react';
1
+ import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
2
2
  import { Platform, StyleSheet } from 'react-native';
3
3
 
4
4
  import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
@@ -14,24 +14,13 @@ import { useConversation, useConversationStore } from '../../store';
14
14
  import ChatDetailHeader from './ChatDetailHeader';
15
15
  import { ChatListLegend } from './ChatListLegend';
16
16
  import { ChatComposer } from './ChatComposer';
17
- import {
18
- DEFAULT_ATTACHMENT_ACTIONS,
19
- DEFAULT_Chat_QUICK_ACTIONS,
20
- } from './constants';
21
17
  import {
22
18
  getConversationAvatarUri,
23
19
  getConversationSubtitle,
24
20
  getConversationTitle,
25
21
  shouldShowAddMember,
26
22
  } from './conversationHeader.utils';
27
- import type {
28
- ChatDetailProps,
29
- DChatQuickAction,
30
- DChatAttachmentAction,
31
- } from './types';
32
- import { useSendMessage } from '../../hooks/message/useSendMessage';
33
- import type { IUrlMetadata } from '../../types/common';
34
- import { useMessageStore } from '../../store/message';
23
+ import type { ChatDetailProps } from './types';
35
24
  import { ChatDetailProvider } from '../../context/ChatDetailContext';
36
25
  import { normalizeUrl } from '../../utils/url';
37
26
 
@@ -50,58 +39,27 @@ const ChatDetail = memo(
50
39
  onPressAddMember,
51
40
  onPressMenu,
52
41
  onPressAvatar,
53
- showQuickActions = true,
54
- quickActions,
55
- inputPlaceholder,
56
- onChangeInput,
57
- onPressAttach,
58
- onPressEmoji,
59
- attachmentActions,
60
- onQuickActionPress,
61
- onAttachmentAction,
62
- renderQuickAction,
63
- renderQuickActions,
64
- renderAttachmentAction,
65
- attachmentColumns,
66
42
  onPressUrl,
67
43
  customMessageItemFactory,
68
44
  }: ChatDetailProps) => {
69
45
  const navigation = useNavigation();
70
- const [internalInput, setInternalInput] = useState<string>('');
71
46
  const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
72
47
  const conversation = useConversation(conversationId);
73
- // const user = conversation?.peer;
74
48
 
75
- // Default handlers when props not provided
76
49
  const handleBackDefault = useCallback(() => {
77
50
  navigation.goBack();
78
51
  }, [navigation]);
79
52
 
80
- const quickActionsDefault = useMemo<DChatQuickAction[]>(
81
- () =>
82
- DEFAULT_Chat_QUICK_ACTIONS.map((action) => ({
83
- ...action,
84
- onPress: () => {
85
- console.log('[quick-action]', action.id);
86
- },
87
- })),
88
- []
89
- );
90
-
91
- const attachmentActionsDefault = useMemo<DChatAttachmentAction[]>(
92
- () =>
93
- DEFAULT_ATTACHMENT_ACTIONS.map((action) => ({
94
- ...action,
95
- onPress: () => {
96
- console.log('[attachment-action]', action.id);
97
- },
98
- })),
53
+ const handleMediaPress = useCallback(
54
+ (
55
+ items: Parameters<MediaViewerModalHandle['show']>[0],
56
+ index?: number
57
+ ) => {
58
+ mediaViewerRef.current?.show(items, index);
59
+ },
99
60
  []
100
61
  );
101
62
 
102
- const { sendMessage } = useSendMessage();
103
- const { pushNewMessage } = useMessageStore();
104
-
105
63
  const {
106
64
  messages,
107
65
  currentUserId,
@@ -109,25 +67,7 @@ const ChatDetail = memo(
109
67
  isLoading,
110
68
  isLoadingEarlier,
111
69
  hasMoreEarlier,
112
- } = useChatMessages({
113
- conversationId,
114
- enabled,
115
- });
116
-
117
- // Callback for attachment messages - add them to the message list
118
- const handleAttachmentMessageSent = useCallback(
119
- (message: any) => {
120
- if (__DEV__)
121
- console.log('[ChatDetail] Attachment message received:', {
122
- clientMsgID: message?.clientMsgID,
123
- contentType: message?.contentType,
124
- });
125
- if (message) {
126
- pushNewMessage(message);
127
- }
128
- },
129
- [pushNewMessage]
130
- );
70
+ } = useChatMessages({ conversationId, enabled });
131
71
 
132
72
  useEffect(() => {
133
73
  useConversationStore.getState().setCurrentConversation(conversationId);
@@ -140,84 +80,18 @@ const ChatDetail = memo(
140
80
  const resolvedShowAddMember =
141
81
  showAddMember ?? shouldShowAddMember(conversation);
142
82
  const resolvedSubtitle = useMemo(() => {
143
- if (subtitle !== undefined) {
144
- return subtitle;
145
- }
146
-
147
- if (getSubtitle && conversation) {
148
- return getSubtitle(conversation);
149
- }
150
-
83
+ if (subtitle !== undefined) return subtitle;
84
+ if (getSubtitle && conversation) return getSubtitle(conversation);
151
85
  return getConversationSubtitle(conversation);
152
86
  }, [conversation, getSubtitle, subtitle]);
153
87
 
154
- const currentInput = internalInput;
155
-
156
- const handleChangeText = useCallback(
157
- (text?: string) => {
158
- const newText = text ?? '';
159
- if (onChangeInput) {
160
- onChangeInput(newText);
161
- } else {
162
- setInternalInput(newText);
163
- }
164
- },
165
- [onChangeInput]
166
- );
167
-
168
- const handleSend = useCallback(
169
- async (urlMetadata?: any) => {
170
- // Support captured text passed from attachment handler
171
- const capturedText = (urlMetadata as any)?._capturedText;
172
- const cleanMetadata = capturedText
173
- ? (({ _capturedText, ...rest }) => rest)(urlMetadata)
174
- : (urlMetadata as IUrlMetadata);
175
-
176
- const text = (capturedText || currentInput).trim();
177
-
178
- // Validate: must have text OR preview metadata (text is optional if preview exists)
179
- // If metadata exists, trust it (useLinkPreview is responsible for validity)
180
- if (!text && !cleanMetadata) {
181
- if (__DEV__)
182
- console.warn('[ChatDetail] No text or preview link to send');
183
- return;
184
- }
185
-
186
- // Log if metadata seems incomplete
187
- if (cleanMetadata && !cleanMetadata.url) {
188
- if (__DEV__)
189
- console.warn(
190
- '[ChatDetail] Preview metadata missing URL - check useLinkPreview'
191
- );
192
- }
193
-
194
- try {
195
- await sendMessage({
196
- files: [],
197
- plainText: text,
198
- urlMetadata: cleanMetadata || undefined,
199
- });
88
+ const insets = useSafeAreaInsets();
200
89
 
201
- // Clear input after successful send
202
- if (onChangeInput) {
203
- onChangeInput('');
204
- } else {
205
- setInternalInput('');
206
- }
207
- } catch (error) {
208
- if (__DEV__)
209
- console.error('[ChatDetail] Failed to send message:', {
210
- error,
211
- text: text.substring(0, 50),
212
- hasPreview: !!cleanMetadata,
213
- });
214
- }
215
- },
216
- [currentInput, onChangeInput, sendMessage]
90
+ const keyboardVerticalOffset = useMemo(
91
+ () => Platform.select({ android: insets.bottom, default: 0 }),
92
+ [insets.bottom]
217
93
  );
218
94
 
219
- const insets = useSafeAreaInsets();
220
-
221
95
  const handlePressUrl = useCallback(
222
96
  (url: string) => {
223
97
  onPressUrl?.(normalizeUrl(url));
@@ -236,25 +110,16 @@ const ChatDetail = memo(
236
110
  avatarFullName={resolvedAvatarFullName}
237
111
  showAddMember={resolvedShowAddMember}
238
112
  onBack={onBack ?? handleBackDefault}
239
- onPressSearch={
240
- onPressSearch ?? (() => console.log('[onPressSearch]'))
241
- }
242
- onPressAddMember={
243
- onPressAddMember ?? (() => console.log('[onPressAddMember]'))
244
- }
245
- onPressMenu={onPressMenu ?? (() => console.log('[onPressMenu]'))}
246
- onPressAvatar={
247
- onPressAvatar ?? (() => console.log('[onPressAvatar]'))
248
- }
113
+ onPressSearch={onPressSearch}
114
+ onPressAddMember={onPressAddMember}
115
+ onPressMenu={onPressMenu}
116
+ onPressAvatar={onPressAvatar}
249
117
  />
250
118
 
251
119
  <KeyboardAvoidingView
252
120
  style={styles.keyboardAvoiding}
253
121
  behavior="padding"
254
- keyboardVerticalOffset={Platform.select({
255
- android: insets.bottom,
256
- default: 0,
257
- })}
122
+ keyboardVerticalOffset={keyboardVerticalOffset}
258
123
  >
259
124
  <ChatListLegend
260
125
  messages={messages}
@@ -264,45 +129,11 @@ const ChatDetail = memo(
264
129
  isLoading={isLoading}
265
130
  isLoadingEarlier={isLoadingEarlier}
266
131
  hasMoreEarlier={hasMoreEarlier}
267
- onMediaPress={(items, index) => {
268
- mediaViewerRef.current?.show(items, index);
269
- }}
132
+ onMediaPress={handleMediaPress}
270
133
  onPressUrl={handlePressUrl}
271
134
  />
272
135
 
273
- <ChatComposer
274
- value={currentInput}
275
- placeholder={inputPlaceholder}
276
- onChangeText={handleChangeText}
277
- onSend={handleSend}
278
- onMessageSent={handleAttachmentMessageSent}
279
- onPressAttach={
280
- onPressAttach ?? (() => console.log('[onPressAttach]'))
281
- }
282
- onPressEmoji={
283
- onPressEmoji ?? (() => console.log('[onPressEmoji]'))
284
- }
285
- showQuickActions={showQuickActions}
286
- quickActions={quickActions ?? quickActionsDefault}
287
- attachmentActions={attachmentActions ?? attachmentActionsDefault}
288
- onQuickActionPress={
289
- onQuickActionPress ??
290
- ((action: DChatQuickAction) => {
291
- console.log('[onQuickActionPress]', action.id, action.meta);
292
- })
293
- }
294
- onAttachmentAction={
295
- onAttachmentAction ??
296
- ((action: DChatAttachmentAction) => {
297
- console.log('[onAttachmentAction]', action.id, action.meta);
298
- })
299
- }
300
- renderQuickAction={renderQuickAction}
301
- renderQuickActions={renderQuickActions}
302
- renderAttachmentAction={renderAttachmentAction}
303
- attachmentColumns={attachmentColumns}
304
- isEmptyMessage={messages?.length === 0}
305
- />
136
+ <ChatComposer isEmptyMessage={messages?.length === 0} />
306
137
  </KeyboardAvoidingView>
307
138
 
308
139
  <MediaViewerModal ref={mediaViewerRef} />
@@ -315,9 +146,7 @@ const ChatDetail = memo(
315
146
  ChatDetail.displayName = 'ChatDetail';
316
147
 
317
148
  const styles = StyleSheet.create({
318
- keyboardAvoiding: {
319
- flex: 1,
320
- },
149
+ keyboardAvoiding: { flex: 1 },
321
150
  });
322
151
 
323
152
  export default ChatDetail;
@@ -27,12 +27,8 @@ export type {
27
27
  ChatQuickActionsRenderParams,
28
28
  ChatAttachmentPanelProps,
29
29
  } from './types';
30
- export type { DGiftedChatMessage } from '../../utils/giftedChatMessage';
30
+
31
31
  export {
32
32
  DChatMessageType,
33
33
  CUSTOM_MESSAGE_DATA_TYPE,
34
34
  } from '../../types/message';
35
- export {
36
- mapOpenIMMessageToGiftedChat,
37
- mapOpenIMMessagesToGiftedChat,
38
- } from '../../utils/giftedChatMessage';
@@ -1,8 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
-
3
2
  import type { MediaItem } from '../../hooks/useMediaViewer';
4
3
  import type { DMessageItem } from '../../types/chat';
5
- import type { IUrlMetadata } from '../../types/common';
6
4
  import type { ConversationItem } from '@droppii/openim-rn-client-sdk';
7
5
 
8
6
  export type DChatActionIconProvider = 'MaterialCommunityIcons' | 'DroppiiNew';
@@ -58,23 +56,7 @@ export interface ChatAttachmentPanelProps {
58
56
  }
59
57
 
60
58
  export interface ChatComposerProps {
61
- value?: string;
62
- placeholder?: string;
63
- onChangeText?: (text?: string) => void;
64
- onSend?: (urlMetadata?: IUrlMetadata) => void;
65
- onPressAttach?: () => void;
66
- onPressEmoji?: () => void;
67
- onMessageSent?: (message: DMessageItem) => void;
68
- showQuickActions?: boolean;
69
- quickActions?: DChatQuickAction[];
70
- attachmentActions?: DChatAttachmentAction[];
71
- onQuickActionPress?: (action: DChatQuickAction) => void;
72
- onAttachmentAction?: (action: DChatAttachmentAction) => void;
73
- renderQuickAction?: ChatQuickActionsProps['renderAction'];
74
- renderQuickActions?: ChatQuickActionsProps['renderQuickActions'];
75
- renderAttachmentAction?: ChatAttachmentPanelProps['renderAction'];
76
- attachmentColumns?: number;
77
- isEmptyMessage: boolean;
59
+ isEmptyMessage?: boolean;
78
60
  }
79
61
 
80
62
  export interface ChatListProps {
@@ -110,25 +92,6 @@ export interface ChatDetailProps extends Omit<
110
92
  avatarFullName?: string;
111
93
  showAddMember?: boolean;
112
94
  getSubtitle?: (conversation: ConversationItem) => string | undefined;
113
- renderChat?: (item: DMessageItem) => ReactNode;
114
- showQuickActions?: boolean;
115
- showAttachmentActions?: boolean;
116
- quickActions?: DChatQuickAction[];
117
- getQuickActions?: (
118
- conversation: ConversationItem
119
- ) => DChatQuickAction[] | undefined;
120
- attachmentActions?: DChatAttachmentAction[];
121
- inputPlaceholder?: string;
122
- onChangeInput?: (text: string) => void;
123
- onSend?: () => void;
124
- onPressAttach?: () => void;
125
- onPressEmoji?: () => void;
126
- onQuickActionPress?: (action: DChatQuickAction) => void;
127
- onAttachmentAction?: (action: DChatAttachmentAction) => void;
128
- renderQuickAction?: ChatQuickActionsProps['renderAction'];
129
- renderQuickActions?: ChatQuickActionsProps['renderQuickActions'];
130
- renderAttachmentAction?: ChatAttachmentPanelProps['renderAction'];
131
- attachmentColumns?: number;
132
95
  onPressUrl?: (url: string) => void;
133
96
  customMessageItemFactory?: (item: DMessageItem) => ReactNode;
134
97
  }
@@ -9,8 +9,9 @@ interface FileElem {
9
9
  sourceUrl: string;
10
10
  }
11
11
 
12
- const MB = 1024 * 1024;
13
- export const MAX_FILE_SIZE = 50 * MB;
12
+ import { UPLOAD_LIMITS } from '../../config/uploadLimits';
13
+
14
+ export const MAX_FILE_SIZE = UPLOAD_LIMITS.file;
14
15
 
15
16
  export class FileAttachmentHandler implements IAttachmentHandler {
16
17
  validate(
@@ -1,12 +1,15 @@
1
1
  import type { ImageAttachment } from '../../types/attachment';
2
2
  import type { IAttachmentHandler } from '../../types/attachmentHandler';
3
3
  import { generateUUID } from '../../utils/imageUtils';
4
+ import { UPLOAD_LIMITS } from '../../config/uploadLimits';
4
5
 
5
6
  export const ImageAttachmentHandler: IAttachmentHandler = {
6
7
  validate(attachment: ImageAttachment) {
7
- const MAX_SIZE = 5 * 1024 * 1024;
8
- if (attachment.sourceFile.size > MAX_SIZE) {
9
- return { valid: false, reason: 'Image exceeds 5MB limit' };
8
+ if (attachment.sourceFile.size > UPLOAD_LIMITS.image) {
9
+ return {
10
+ valid: false,
11
+ reason: `Image exceeds ${UPLOAD_LIMITS.image / 1024 / 1024}MB limit`,
12
+ };
10
13
  }
11
14
 
12
15
  const validMimes = [
@@ -1,5 +1,6 @@
1
1
  import type { Attachment, VideoAttachment } from '../../types/attachment';
2
2
  import type { IAttachmentHandler } from '../../types/attachmentHandler';
3
+ import { UPLOAD_LIMITS } from '../../config/uploadLimits';
3
4
 
4
5
  interface VideoPicInfo {
5
6
  videoUUID: string;
@@ -18,7 +19,7 @@ interface VideoPicInfo {
18
19
  }
19
20
 
20
21
  export class VideoAttachmentHandler implements IAttachmentHandler {
21
- private static readonly MAX_VIDEO_SIZE = 200 * 1024 * 1024; // 200MB
22
+ private static readonly MAX_VIDEO_SIZE = UPLOAD_LIMITS.video;
22
23
  private static readonly SUPPORTED_TYPES = ['video/mp4', 'video/quicktime'];
23
24
 
24
25
  validate(
@@ -6,6 +6,7 @@ import type {
6
6
  } from '../types/attachment';
7
7
  import { getAttachmentHandler } from '../types/attachmentHandler';
8
8
  import { useConversationStore } from '../store';
9
+ import { useMessageStore } from '../store/message';
9
10
  import {
10
11
  getAttachmentPriorityConfig,
11
12
  type AttachmentType,
@@ -15,8 +16,7 @@ export namespace AttachmentOrchestratorService {
15
16
  export async function sendPreparedAttachments(
16
17
  attachments: Attachment[],
17
18
  uploadedUrls: Record<string, string>,
18
- options: AttachmentSendOptions = { strategy: 'individual' },
19
- onMessageSent?: (message: any) => void
19
+ options: AttachmentSendOptions = { strategy: 'individual' }
20
20
  ): Promise<AttachmentSendResult> {
21
21
  const result: AttachmentSendResult = {
22
22
  success: false,
@@ -90,8 +90,7 @@ export namespace AttachmentOrchestratorService {
90
90
  try {
91
91
  const mergedMsgId = await sendMergedMessage(
92
92
  payloads.map((p) => p.payload),
93
- options.includeCaption,
94
- onMessageSent
93
+ options.includeCaption
95
94
  );
96
95
  console.log('[AttachmentOrchestrator] Messages sent via merger:', {
97
96
  messageId: mergedMsgId,
@@ -116,8 +115,7 @@ export namespace AttachmentOrchestratorService {
116
115
 
117
116
  async function sendMergedMessage(
118
117
  payloads: unknown[],
119
- _caption?: string,
120
- onMessageSent?: (message: any) => void
118
+ _caption?: string
121
119
  ): Promise<string> {
122
120
  const currentConversation = useConversationStore
123
121
  .getState()
@@ -290,8 +288,8 @@ export namespace AttachmentOrchestratorService {
290
288
  index: i + 1,
291
289
  });
292
290
 
293
- if (onMessageSent && result) {
294
- onMessageSent(result);
291
+ if (result) {
292
+ useMessageStore.getState().pushNewMessage(result);
295
293
  }
296
294
  }
297
295
 
@@ -1,5 +1,6 @@
1
1
  import type { ImageFile } from '../types/imageUpload';
2
2
  import Images from '../assets/images';
3
+ import { UPLOAD_LIMITS } from '../config/uploadLimits';
3
4
 
4
5
  export const generateUUID = (): string => {
5
6
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
@@ -81,10 +82,8 @@ export const formatFileSize = (bytes: number): string => {
81
82
  };
82
83
 
83
84
  export const validateImageFile = (file: ImageFile): string | null => {
84
- const MAX_SIZE = 5 * 1024 * 1024;
85
-
86
- if (file.size > MAX_SIZE) {
87
- return `Image too large. Max ${formatFileSize(MAX_SIZE)}`;
85
+ if (file.size > UPLOAD_LIMITS.image) {
86
+ return `Image too large. Max ${formatFileSize(UPLOAD_LIMITS.image)}`;
88
87
  }
89
88
 
90
89
  const validMimes = [
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Run `items` through `fn` with at most `concurrency` items in-flight at once.
3
+ * Errors per-item are swallowed — wrap `fn` if you need to capture them.
4
+ */
5
+ export async function pLimit<T>(
6
+ items: T[],
7
+ fn: (item: T, index: number) => Promise<void>,
8
+ concurrency: number
9
+ ): Promise<void> {
10
+ let idx = 0;
11
+ const worker = async () => {
12
+ while (idx < items.length) {
13
+ const i = idx++;
14
+ await fn(items[i]!, i);
15
+ }
16
+ };
17
+ await Promise.all(
18
+ Array.from({ length: Math.min(concurrency, items.length) }, worker)
19
+ );
20
+ }
@@ -28,6 +28,9 @@ interface ThumbnailResult {
28
28
  * The app strips "file://" before calling, so local videos fall into the broken
29
29
  * branch. The fix is to KEEP the file:// scheme so the working overload is used.
30
30
  *
31
+ * iOS is the opposite: AVAsset / react-native-create-thumbnail requires a bare
32
+ * path without any scheme prefix. Strip "file://" on iOS.
33
+ *
31
34
  * For Android photo-picker synthetic paths we additionally copy the file into the
32
35
  * app cache first (a clean, app-private file the retriever can always open) and
33
36
  * still hand it back with a file:// prefix. Returns the path to pass to
@@ -36,9 +39,13 @@ interface ThumbnailResult {
36
39
  const ensureRetrievableVideoPath = async (
37
40
  videoPath: string
38
41
  ): Promise<{ path: string; cleanupPath: string | null }> => {
39
- // iOS (and any non-Android platform) is left completely untouched.
42
+ // iOS AVAsset / react-native-create-thumbnail requires a bare file path
43
+ // without the file:// scheme. Strip it if present.
40
44
  if (Platform.OS !== 'android') {
41
- return { path: videoPath, cleanupPath: null };
45
+ const barePath = videoPath.startsWith('file://')
46
+ ? videoPath.slice('file://'.length)
47
+ : videoPath;
48
+ return { path: barePath, cleanupPath: null };
42
49
  }
43
50
 
44
51
  // content:// is handled by a dedicated native branch — pass through unchanged.
@@ -112,7 +119,6 @@ export const VideoThumbnailUtil = {
112
119
  cacheName,
113
120
  });
114
121
 
115
- console.log('✅ [VideoThumbnail] THUMBNAIL PATH:', result.path);
116
122
  console.log('[VideoThumbnail] Thumbnail generated', {
117
123
  path: result.path,
118
124
  width: result.width || 0,
@@ -147,6 +153,7 @@ export const VideoThumbnailUtil = {
147
153
  }
148
154
  },
149
155
 
156
+ // Serial by design — reduces memory pressure when processing multiple videos.
150
157
  batchGenerateThumbnails: async (
151
158
  videoPaths: string[],
152
159
  options: ThumbnailOptions = {}
@@ -0,0 +1,103 @@
1
+ import RNFS from 'react-native-fs';
2
+ import VideoThumbnailUtil from './videoThumbnail';
3
+ import { ImageUploadAPI } from '../services/imageUpload';
4
+ import UIUtils from './ui';
5
+ import type { Video as PickedVideo } from 'react-native-image-crop-picker';
6
+ import type { ImageWithUploadStatus } from '../hooks/useImageAttachment';
7
+
8
+ export type VideoWithUploadStatus = Omit<ImageWithUploadStatus, 'image'> & {
9
+ image: PickedVideo;
10
+ };
11
+
12
+ export interface VideoThumbnailResult {
13
+ url: string;
14
+ size: number;
15
+ }
16
+
17
+ export async function generateAndUploadVideoThumbnails(
18
+ videos: VideoWithUploadStatus[]
19
+ ): Promise<Record<string, VideoThumbnailResult>> {
20
+ const thumbnails: Record<string, VideoThumbnailResult> = {};
21
+ const failed: string[] = [];
22
+
23
+ for (let idx = 0; idx < videos.length; idx++) {
24
+ const vidItem = videos[idx]!;
25
+ // Key by uploadedUrl so callers can look up by the same value.
26
+ const videoId = vidItem.uploadedUrl || `video-${idx}`;
27
+ let cachedVideoPath: string | null = null;
28
+
29
+ try {
30
+ let videoDuration = vidItem.image.duration ?? 0;
31
+
32
+ if (videoDuration <= 0 || videoDuration > 3600000) {
33
+ videoDuration = 5000;
34
+ }
35
+
36
+ // Clamp upper bound to avoid negative timeMs on clips shorter than 1s.
37
+ const timeMs = Math.min(
38
+ Math.max(1000, Math.floor(videoDuration * 0.35)),
39
+ Math.max(0, videoDuration - 1000)
40
+ );
41
+
42
+ // Copy the picker-returned path to CachesDirectory before generating a
43
+ // thumbnail. On iOS the picker can return Photos-library paths that
44
+ // react-native-create-thumbnail cannot open directly; a copy to a
45
+ // stable app-owned location is required.
46
+ const filename = vidItem.image.filename || 'video.mp4';
47
+ const ext = filename.split('.').pop() || 'mp4';
48
+ const cachedFileName = `thumb-src-${idx}-${Date.now()}.${ext}`;
49
+ cachedVideoPath = `${RNFS.CachesDirectoryPath}/${cachedFileName}`;
50
+ await RNFS.copyFile(vidItem.image.path, cachedVideoPath);
51
+
52
+ const thumbnail = await VideoThumbnailUtil.generateThumbnail(
53
+ cachedVideoPath,
54
+ { time: timeMs, quality: 80, cacheId: `thumb-${idx}-${Date.now()}` }
55
+ );
56
+
57
+ if (!thumbnail) throw new Error('Failed to generate thumbnail');
58
+
59
+ const baseName = filename.split('.')[0] || `video-${idx}`;
60
+ const uploaded = await ImageUploadAPI.uploadImageFile(
61
+ {
62
+ path: thumbnail.path,
63
+ filename: `thumb-${baseName}.jpg`,
64
+ size: 0,
65
+ width: thumbnail.width || 320,
66
+ height: thumbnail.height || 180,
67
+ mime: 'image/jpeg',
68
+ },
69
+ () => {}
70
+ );
71
+
72
+ if (!uploaded?.url) throw new Error('No thumbnail URL returned');
73
+
74
+ thumbnails[videoId] = { url: uploaded.url, size: uploaded.size || 0 };
75
+
76
+ // Clean up the local thumbnail file after a successful upload.
77
+ VideoThumbnailUtil.cleanupThumbnail(thumbnail.path).catch((err) =>
78
+ console.warn('[videoThumbnailUpload] Thumbnail cleanup failed:', err)
79
+ );
80
+ } catch (error) {
81
+ const msg = error instanceof Error ? error.message : String(error);
82
+ console.error('[videoThumbnailUpload] Failed for video:', idx, msg);
83
+ failed.push(vidItem.image.filename || `video-${idx}`);
84
+ // Do NOT insert a poison entry — callers treat a missing key as "no thumbnail".
85
+ } finally {
86
+ if (cachedVideoPath) {
87
+ RNFS.unlink(cachedVideoPath).catch((err) =>
88
+ console.warn('[videoThumbnailUpload] Temp copy cleanup failed:', err)
89
+ );
90
+ }
91
+ }
92
+ }
93
+
94
+ if (failed.length > 0) {
95
+ UIUtils.toast.open({
96
+ title: 'Thumbnail Generation Failed',
97
+ message: `Failed for: ${failed.join(', ')}. Videos will be sent without thumbnails.`,
98
+ theme: 'warning',
99
+ });
100
+ }
101
+
102
+ return thumbnails;
103
+ }