@droppii-org/chat-mobile 0.2.17 → 0.2.19

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 (199) hide show
  1. package/lib/module/assets/images/icon_excel.png +0 -0
  2. package/lib/module/assets/images/icon_img.png +0 -0
  3. package/lib/module/assets/images/icon_pdf.png +0 -0
  4. package/lib/module/assets/images/icon_txt.png +0 -0
  5. package/lib/module/assets/images/icon_word.png +0 -0
  6. package/lib/module/assets/images/index.js +5 -0
  7. package/lib/module/assets/images/index.js.map +1 -1
  8. package/lib/module/components/AttachmentPreview/FilePreview.js +107 -0
  9. package/lib/module/components/AttachmentPreview/FilePreview.js.map +1 -0
  10. package/lib/module/components/AttachmentPreview/MediaPreview.js +212 -0
  11. package/lib/module/components/AttachmentPreview/MediaPreview.js.map +1 -0
  12. package/lib/module/components/AttachmentPreview/index.js +49 -0
  13. package/lib/module/components/AttachmentPreview/index.js.map +1 -0
  14. package/lib/module/components/ThreadCard/AvatarSection.js.map +1 -1
  15. package/lib/module/components/ThreadCard/ThreadCard.js +17 -9
  16. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  17. package/lib/module/components/flows/inputButtons/index.js +7 -3
  18. package/lib/module/components/flows/inputButtons/index.js.map +1 -1
  19. package/lib/module/components/messages/fileMessage/index.js +122 -12
  20. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  21. package/lib/module/context/ChatContext.js +27 -4
  22. package/lib/module/context/ChatContext.js.map +1 -1
  23. package/lib/module/context/ChatDetailContext.js.map +1 -1
  24. package/lib/module/core/useChatListener.js +2 -4
  25. package/lib/module/core/useChatListener.js.map +1 -1
  26. package/lib/module/core/useUserListener.js +0 -7
  27. package/lib/module/core/useUserListener.js.map +1 -1
  28. package/lib/module/hooks/flows/useFlow.js +14 -3
  29. package/lib/module/hooks/flows/useFlow.js.map +1 -1
  30. package/lib/module/hooks/message/useSendMessage.js +1 -1
  31. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  32. package/lib/module/hooks/useConversationList.js +3 -27
  33. package/lib/module/hooks/useConversationList.js.map +1 -1
  34. package/lib/module/hooks/useFileAttachment.js +116 -0
  35. package/lib/module/hooks/useFileAttachment.js.map +1 -0
  36. package/lib/module/hooks/useFileDownload.js +175 -0
  37. package/lib/module/hooks/useFileDownload.js.map +1 -0
  38. package/lib/module/hooks/useLinkPreview/useLinkPreview.js +1 -1
  39. package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -1
  40. package/lib/module/index.js +1 -1
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/screens/chat-detail/ChatComposer.js +44 -36
  43. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  44. package/lib/module/screens/chat-detail/ChatDetail.js +1 -6
  45. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  46. package/lib/module/screens/chat-detail/ChatDetailHeader.js +6 -6
  47. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  48. package/lib/module/screens/chat-detail/ChatLinkPreview.js +3 -1
  49. package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -1
  50. package/lib/module/screens/chat-detail/ChatListLegend.js +2 -2
  51. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  52. package/lib/module/screens/chat-detail/conversationHeader.utils.js +9 -0
  53. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
  54. package/lib/module/screens/chat-detail/hooks/useAttachmentMapping.js +51 -0
  55. package/lib/module/screens/chat-detail/hooks/useAttachmentMapping.js.map +1 -0
  56. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js +39 -13
  57. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +1 -1
  58. package/lib/module/screens/inbox/Inbox.js +5 -1
  59. package/lib/module/screens/inbox/Inbox.js.map +1 -1
  60. package/lib/module/services/apis.js +0 -20
  61. package/lib/module/services/apis.js.map +1 -1
  62. package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js +6 -6
  63. package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js.map +1 -1
  64. package/lib/module/services/attachmentHandlers/index.js +1 -1
  65. package/lib/module/services/attachmentHandlers/index.js.map +1 -1
  66. package/lib/module/store/conversation.js +1 -38
  67. package/lib/module/store/conversation.js.map +1 -1
  68. package/lib/module/translation/resources/i18n.js +9 -0
  69. package/lib/module/translation/resources/i18n.js.map +1 -1
  70. package/lib/module/types/auth.js.map +1 -1
  71. package/lib/module/types/chat.js.map +1 -1
  72. package/lib/module/utils/conversation.js +1 -28
  73. package/lib/module/utils/conversation.js.map +1 -1
  74. package/lib/module/utils/device.js +32 -0
  75. package/lib/module/utils/device.js.map +1 -1
  76. package/lib/module/utils/imageUtils.js +44 -28
  77. package/lib/module/utils/imageUtils.js.map +1 -1
  78. package/lib/module/utils/message.js.map +1 -1
  79. package/lib/module/utils/messageUtils.js +5 -3
  80. package/lib/module/utils/messageUtils.js.map +1 -1
  81. package/lib/typescript/src/assets/images/index.d.ts +5 -0
  82. package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
  83. package/lib/typescript/src/components/AttachmentPreview/FilePreview.d.ts +6 -0
  84. package/lib/typescript/src/components/AttachmentPreview/FilePreview.d.ts.map +1 -0
  85. package/lib/typescript/src/components/AttachmentPreview/MediaPreview.d.ts +9 -0
  86. package/lib/typescript/src/components/AttachmentPreview/MediaPreview.d.ts.map +1 -0
  87. package/lib/typescript/src/components/AttachmentPreview/index.d.ts +9 -0
  88. package/lib/typescript/src/components/AttachmentPreview/index.d.ts.map +1 -0
  89. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +2 -2
  90. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  91. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  92. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  93. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  94. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  95. package/lib/typescript/src/core/useUserListener.d.ts.map +1 -1
  96. package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -1
  97. package/lib/typescript/src/hooks/useConversationList.d.ts +2 -2
  98. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  99. package/lib/typescript/src/hooks/useFileAttachment.d.ts +20 -0
  100. package/lib/typescript/src/hooks/useFileAttachment.d.ts.map +1 -0
  101. package/lib/typescript/src/hooks/useFileDownload.d.ts +15 -0
  102. package/lib/typescript/src/hooks/useFileDownload.d.ts.map +1 -0
  103. package/lib/typescript/src/hooks/useImageAttachment.d.ts +1 -2
  104. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  105. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -1
  106. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  107. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  108. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  109. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  110. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  111. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts.map +1 -1
  112. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  113. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +7 -5
  114. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
  115. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentMapping.d.ts +10 -0
  116. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentMapping.d.ts.map +1 -0
  117. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts +4 -1
  118. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +1 -1
  119. package/lib/typescript/src/screens/chat-detail/types.d.ts +6 -9
  120. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  121. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  122. package/lib/typescript/src/services/apis.d.ts +0 -4
  123. package/lib/typescript/src/services/apis.d.ts.map +1 -1
  124. package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts +2 -2
  125. package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts.map +1 -1
  126. package/lib/typescript/src/services/attachmentHandlers/index.d.ts +1 -1
  127. package/lib/typescript/src/services/attachmentHandlers/index.d.ts.map +1 -1
  128. package/lib/typescript/src/store/conversation.d.ts +5 -8
  129. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  130. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  131. package/lib/typescript/src/types/attachment.d.ts +22 -1
  132. package/lib/typescript/src/types/attachment.d.ts.map +1 -1
  133. package/lib/typescript/src/types/auth.d.ts +1 -0
  134. package/lib/typescript/src/types/auth.d.ts.map +1 -1
  135. package/lib/typescript/src/types/chat.d.ts +15 -13
  136. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  137. package/lib/typescript/src/utils/conversation.d.ts +2 -5
  138. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  139. package/lib/typescript/src/utils/device.d.ts +7 -0
  140. package/lib/typescript/src/utils/device.d.ts.map +1 -1
  141. package/lib/typescript/src/utils/imageUtils.d.ts +1 -0
  142. package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
  143. package/lib/typescript/src/utils/message.d.ts +3 -3
  144. package/lib/typescript/src/utils/message.d.ts.map +1 -1
  145. package/lib/typescript/src/utils/messageUtils.d.ts +2 -1
  146. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  147. package/package.json +6 -2
  148. package/src/assets/images/icon_excel.png +0 -0
  149. package/src/assets/images/icon_img.png +0 -0
  150. package/src/assets/images/icon_pdf.png +0 -0
  151. package/src/assets/images/icon_txt.png +0 -0
  152. package/src/assets/images/icon_word.png +0 -0
  153. package/src/assets/images/index.ts +5 -0
  154. package/src/components/AttachmentPreview/FilePreview.tsx +137 -0
  155. package/src/components/AttachmentPreview/MediaPreview.tsx +238 -0
  156. package/src/components/AttachmentPreview/index.tsx +68 -0
  157. package/src/components/ThreadCard/AvatarSection.tsx +2 -3
  158. package/src/components/ThreadCard/ThreadCard.tsx +22 -15
  159. package/src/components/flows/inputButtons/index.tsx +4 -1
  160. package/src/components/messages/fileMessage/index.tsx +156 -13
  161. package/src/context/ChatContext.tsx +35 -3
  162. package/src/context/ChatDetailContext.tsx +1 -1
  163. package/src/core/useChatListener.ts +2 -4
  164. package/src/core/useUserListener.ts +0 -8
  165. package/src/hooks/flows/useFlow.ts +12 -3
  166. package/src/hooks/message/useSendMessage.ts +1 -1
  167. package/src/hooks/useConversationList.ts +6 -40
  168. package/src/hooks/useFileAttachment.ts +163 -0
  169. package/src/hooks/useFileDownload.ts +200 -0
  170. package/src/hooks/useImageAttachment.ts +1 -1
  171. package/src/hooks/useLinkPreview/useLinkPreview.ts +6 -1
  172. package/src/screens/chat-detail/ChatComposer.tsx +61 -30
  173. package/src/screens/chat-detail/ChatDetail.tsx +1 -7
  174. package/src/screens/chat-detail/ChatDetailHeader.tsx +6 -7
  175. package/src/screens/chat-detail/ChatLinkPreview.tsx +16 -12
  176. package/src/screens/chat-detail/ChatListLegend.tsx +2 -6
  177. package/src/screens/chat-detail/conversationHeader.utils.ts +16 -6
  178. package/src/screens/chat-detail/hooks/useAttachmentMapping.ts +58 -0
  179. package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +50 -11
  180. package/src/screens/chat-detail/types.ts +6 -13
  181. package/src/screens/inbox/Inbox.tsx +5 -1
  182. package/src/services/apis.ts +0 -38
  183. package/src/services/attachmentHandlers/fileAttachmentHandler.ts +8 -7
  184. package/src/services/attachmentHandlers/index.ts +4 -1
  185. package/src/store/conversation.ts +6 -55
  186. package/src/translation/resources/i18n.ts +9 -0
  187. package/src/types/attachment.ts +23 -1
  188. package/src/types/auth.ts +1 -0
  189. package/src/types/chat.ts +16 -20
  190. package/src/utils/conversation.ts +2 -40
  191. package/src/utils/device.ts +58 -0
  192. package/src/utils/imageUtils.ts +50 -28
  193. package/src/utils/message.ts +3 -2
  194. package/src/utils/messageUtils.ts +11 -3
  195. package/lib/module/components/AttachmentPreview.js +0 -252
  196. package/lib/module/components/AttachmentPreview.js.map +0 -1
  197. package/lib/typescript/src/components/AttachmentPreview.d.ts +0 -28
  198. package/lib/typescript/src/components/AttachmentPreview.d.ts.map +0 -1
  199. package/src/components/AttachmentPreview.tsx +0 -310
@@ -5,6 +5,7 @@ import VideoThumbnailUtil from '../../../utils/videoThumbnail';
5
5
  import { ImageUploadAPI } from '../../../services/imageUpload';
6
6
  import UIUtils from '../../../utils/ui';
7
7
  import type { DMessageItem } from '../../../types/chat';
8
+ import type { FileWithUploadStatus } from '../../../hooks/useFileAttachment';
8
9
 
9
10
  interface AttachmentItem {
10
11
  image: any;
@@ -16,12 +17,14 @@ interface AttachmentItem {
16
17
 
17
18
  interface UseAttachmentSendHandlerProps {
18
19
  selectedImages: AttachmentItem[];
20
+ selectedFiles: FileWithUploadStatus[];
19
21
  sendAttachments: (
20
22
  items: any[],
21
23
  _: any,
22
24
  onMessageSent?: (message: DMessageItem) => void
23
25
  ) => Promise<boolean>;
24
26
  clearAllImages: () => void;
27
+ clearAllFiles?: () => void;
25
28
  onSend?: () => void;
26
29
  onMessageSent?: (message: DMessageItem) => void;
27
30
  textMessage?: string;
@@ -31,8 +34,10 @@ interface UseAttachmentSendHandlerProps {
31
34
 
32
35
  export const useAttachmentSendHandler = ({
33
36
  selectedImages,
37
+ selectedFiles,
34
38
  sendAttachments,
35
39
  clearAllImages,
40
+ clearAllFiles,
36
41
  onSend,
37
42
  onMessageSent,
38
43
  textMessage,
@@ -316,26 +321,60 @@ export const useAttachmentSendHandler = ({
316
321
  if (success) {
317
322
  clearAllImages();
318
323
  console.log('[ChatComposer] Images cleared after successful send');
324
+ }
325
+ }
319
326
 
320
- // Send text message after attachments (follows attachment priority order)
321
- // Note: urlMetadata is only passed if preview is still visible (not dismissed)
322
- if (textMessage && onSendText) {
323
- console.log('[ChatComposer] Sending text message after attachments', {
324
- hasPreview: !!urlMetadata,
325
- previewUrl: urlMetadata?.url,
326
- });
327
- onSendText(textMessage, urlMetadata);
328
- }
327
+ // Send each file as its own individual message
328
+ const uploadedFiles = selectedFiles.filter(
329
+ (f) => f.uploadedUrl && !f.error
330
+ );
331
+ if (uploadedFiles.length > 0) {
332
+ console.log('[ChatComposer] Sending files individually:', {
333
+ count: uploadedFiles.length,
334
+ });
335
+ for (const file of uploadedFiles) {
336
+ await sendAttachments(
337
+ [
338
+ {
339
+ type: 'file' as const,
340
+ file: { path: file.uri, filename: file.name, size: file.size },
341
+ uploadProgress: 100,
342
+ error: null,
343
+ isUploading: false,
344
+ uploadedUrl: file.uploadedUrl,
345
+ },
346
+ ],
347
+ undefined,
348
+ onMessageSent
349
+ );
329
350
  }
351
+ clearAllFiles?.();
352
+ }
353
+
354
+ // Send text last
355
+ if (textMessage && onSendText) {
356
+ console.log('[ChatComposer] Sending text message after attachments', {
357
+ hasPreview: !!urlMetadata,
358
+ previewUrl: urlMetadata?.url,
359
+ });
360
+ onSendText(textMessage, urlMetadata);
330
361
  return;
331
362
  }
332
363
 
333
- // Send text message only if no attachments
334
- onSend?.();
364
+ const hasAnySent =
365
+ uploadedImages.length > 0 ||
366
+ allUploadedVideos.length > 0 ||
367
+ uploadedFiles.length > 0;
368
+ if (!hasAnySent) {
369
+ onSend?.();
370
+ }
371
+ return;
335
372
  }, [
336
373
  selectedImages,
374
+ selectedFiles,
337
375
  sendAttachments,
338
376
  clearAllImages,
377
+ clearAllFiles,
339
378
  onSend,
340
379
  onMessageSent,
341
380
  textMessage,
@@ -1,13 +1,9 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
3
  import type { MediaItem } from '../../hooks/useMediaViewer';
4
- import type {
5
- DChatApplicationType,
6
- DConversationItem,
7
- DMessageItem,
8
- } from '../../types/chat';
4
+ import type { DMessageItem } from '../../types/chat';
9
5
  import type { IUrlMetadata } from '../../types/common';
10
- import type { PeerType } from '@droppii/openim-rn-client-sdk';
6
+ import type { ConversationItem } from '@droppii/openim-rn-client-sdk';
11
7
 
12
8
  export type DChatActionIconProvider = 'MaterialCommunityIcons' | 'DroppiiNew';
13
9
 
@@ -29,14 +25,13 @@ export interface ChatDetailHeaderProps {
29
25
  subtitle?: string;
30
26
  avatarUri?: string | null;
31
27
  avatarFullName?: string;
32
- peerType?: PeerType;
33
- applicationType?: DConversationItem['applicationType'];
34
28
  showAddMember?: boolean;
35
29
  onBack?: () => void;
36
30
  onPressSearch?: () => void;
37
31
  onPressAddMember?: () => void;
38
32
  onPressMenu?: () => void;
39
33
  onPressAvatar?: () => void;
34
+ conversationId: string;
40
35
  }
41
36
 
42
37
  export interface ChatQuickActionsRenderParams {
@@ -103,9 +98,9 @@ export interface ChatDetailProps extends Omit<
103
98
  | 'subtitle'
104
99
  | 'avatarUri'
105
100
  | 'avatarFullName'
106
- | 'peerType'
107
101
  | 'applicationType'
108
102
  | 'showAddMember'
103
+ | 'conversationId'
109
104
  > {
110
105
  conversationId: string;
111
106
  enabled?: boolean;
@@ -113,16 +108,14 @@ export interface ChatDetailProps extends Omit<
113
108
  subtitle?: string;
114
109
  avatarUri?: string | null;
115
110
  avatarFullName?: string;
116
- peerType?: PeerType;
117
- applicationType?: DChatApplicationType;
118
111
  showAddMember?: boolean;
119
- getSubtitle?: (conversation: DConversationItem) => string | undefined;
112
+ getSubtitle?: (conversation: ConversationItem) => string | undefined;
120
113
  renderChat?: (item: DMessageItem) => ReactNode;
121
114
  showQuickActions?: boolean;
122
115
  showAttachmentActions?: boolean;
123
116
  quickActions?: DChatQuickAction[];
124
117
  getQuickActions?: (
125
- conversation: DConversationItem
118
+ conversation: ConversationItem
126
119
  ) => DChatQuickAction[] | undefined;
127
120
  attachmentActions?: DChatAttachmentAction[];
128
121
  inputPlaceholder?: string;
@@ -3,6 +3,8 @@ import { StyleSheet } from 'react-native';
3
3
  import { TabView } from 'react-native-tab-view';
4
4
  import { KContainer, KImage, KLabel, KColors } from '@droppii/libs';
5
5
  import { MessagesTab } from './MessagesTab';
6
+ import { useUserStore } from '../../store';
7
+ import { LoginStatus } from '@droppii/openim-rn-client-sdk';
6
8
 
7
9
  interface InboxProps {
8
10
  onPressThread?: (item: string) => void;
@@ -15,6 +17,7 @@ type TabRoute = { key: string; title: string };
15
17
 
16
18
  const Inbox = memo(
17
19
  ({ onPressThread, onPressSearch, onPressEdit, onPressGroup }: InboxProps) => {
20
+ const connectStatus = useUserStore((state) => state.connectStatus);
18
21
  const [index, setIndex] = useState(0);
19
22
  const [routes] = useState<TabRoute[]>([
20
23
  { key: 'messages', title: 'Tin nhắn' },
@@ -22,12 +25,13 @@ const Inbox = memo(
22
25
 
23
26
  const renderScene = useCallback(
24
27
  ({ route }: { route: TabRoute }) => {
28
+ if (connectStatus !== LoginStatus.Logged) return null;
25
29
  if (route.key === 'messages') {
26
30
  return <MessagesTab onPressThread={onPressThread} />;
27
31
  }
28
32
  return null;
29
33
  },
30
- [onPressThread]
34
+ [onPressThread, connectStatus]
31
35
  );
32
36
 
33
37
  const renderTabBar = useCallback(
@@ -1,11 +1,5 @@
1
1
  import { type AxiosInstance } from 'axios';
2
- import type {
3
- DConversationItem,
4
- DConversationQueryParams,
5
- } from '../types/chat';
6
2
  import OpenIMSDK, { LoginStatus } from '@droppii/openim-rn-client-sdk';
7
- import type { BaseResponse, GetOpenIMTokenResponse } from '../types/auth';
8
- import { Platform } from 'react-native';
9
3
  import { useConversationStore } from '../store';
10
4
 
11
5
  export let apiInstance: AxiosInstance | null = null;
@@ -19,25 +13,6 @@ export namespace ChatAPI {
19
13
  return (await OpenIMSDK.getLoginStatus(operationID)) === LoginStatus.Logged;
20
14
  };
21
15
 
22
- export const login = async (
23
- instance: AxiosInstance,
24
- operationID?: string
25
- ) => {
26
- const isLoggedIn = await getLoginStatus(operationID);
27
-
28
- if (isLoggedIn) return;
29
-
30
- const response = await instance.post<BaseResponse<GetOpenIMTokenResponse>>(
31
- `/chat-service/v1/auth/token?platform=${Platform.OS.toUpperCase()}`
32
- );
33
-
34
- const token = response?.data?.data;
35
-
36
- if (token == null) return;
37
-
38
- return OpenIMSDK.login({ userID: token.id, token: token.token });
39
- };
40
-
41
16
  export const logout = async (operationID?: string) => {
42
17
  useConversationStore.getState().reset();
43
18
  return OpenIMSDK.logout(operationID);
@@ -46,17 +21,4 @@ export namespace ChatAPI {
46
21
  export const getUser = async (operationID?: string | undefined) => {
47
22
  return OpenIMSDK.getSelfUserInfo(operationID);
48
23
  };
49
-
50
- export async function queryConversations(params: DConversationQueryParams) {
51
- if (!apiInstance) {
52
- throw new Error(
53
- 'apiInstance is not initialized. Call initApiInstance() first.'
54
- );
55
- }
56
- const response = await apiInstance.post<BaseResponse<DConversationItem[]>>(
57
- '/chat-service/v1/app/conversations/query',
58
- params
59
- );
60
- return response.data;
61
- }
62
24
  }
@@ -6,12 +6,13 @@ interface FileElem {
6
6
  fileName: string;
7
7
  fileSize: number;
8
8
  fileType: string;
9
- url: string;
9
+ sourceUrl: string;
10
10
  }
11
11
 
12
- export class FileAttachmentHandler implements IAttachmentHandler {
13
- private static readonly MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB
12
+ const MB = 1024 * 1024;
13
+ export const MAX_FILE_SIZE = 50 * MB;
14
14
 
15
+ export class FileAttachmentHandler implements IAttachmentHandler {
15
16
  validate(
16
17
  attachment: Attachment
17
18
  ): { valid: true } | { valid: false; reason: string } {
@@ -38,10 +39,10 @@ export class FileAttachmentHandler implements IAttachmentHandler {
38
39
  };
39
40
  }
40
41
 
41
- if (sourceFile.size > FileAttachmentHandler.MAX_FILE_SIZE) {
42
+ if (sourceFile.size > MAX_FILE_SIZE) {
42
43
  return {
43
44
  valid: false,
44
- reason: `File size exceeds maximum (${FileAttachmentHandler.MAX_FILE_SIZE / 1024 / 1024}MB)`,
45
+ reason: `File size exceeds maximum (${MAX_FILE_SIZE / 1024 / 1024}MB)`,
45
46
  };
46
47
  }
47
48
 
@@ -60,7 +61,7 @@ export class FileAttachmentHandler implements IAttachmentHandler {
60
61
  fileName: sourceFile.filename,
61
62
  fileSize: sourceFile.size,
62
63
  fileType: sourceFile.mimeType,
63
- url: uploadedUrl,
64
+ sourceUrl: uploadedUrl,
64
65
  };
65
66
 
66
67
  console.warn('[FileAttachmentHandler] Built payload', {
@@ -68,7 +69,7 @@ export class FileAttachmentHandler implements IAttachmentHandler {
68
69
  fileName: fileElem.fileName,
69
70
  fileSize: fileElem.fileSize,
70
71
  fileType: fileElem.fileType,
71
- url: uploadedUrl.substring(0, 50) + '...',
72
+ sourceUrl: uploadedUrl.substring(0, 50) + '...',
72
73
  });
73
74
 
74
75
  return fileElem;
@@ -7,4 +7,7 @@ export function initializeAttachmentHandlers(): void {
7
7
 
8
8
  export { ImageAttachmentHandler } from './imageAttachmentHandler';
9
9
  export { default as VideoAttachmentHandler } from './videoAttachmentHandler';
10
- export { default as FileAttachmentHandler } from './fileAttachmentHandler';
10
+ export {
11
+ default as FileAttachmentHandler,
12
+ MAX_FILE_SIZE,
13
+ } from './fileAttachmentHandler';
@@ -1,32 +1,23 @@
1
1
  import { create } from 'zustand';
2
- import {
3
- sortConversation,
4
- mergeOpenIMIntoConversation,
5
- } from '../utils/conversation';
6
- import type { DConversationItem } from '../types/chat';
7
- import type {
8
- ConversationItem,
9
- MessageItem,
10
- } from '@droppii/openim-rn-client-sdk';
2
+ import { sortConversation } from '../utils/conversation';
3
+ import type { ConversationItem } from '@droppii/openim-rn-client-sdk';
11
4
 
12
5
  type ConversationID = string;
13
6
 
14
7
  export interface ConversationStore {
15
8
  currentConversationId?: ConversationID;
16
9
  setCurrentConversation(id: ConversationID): void;
17
- getCurrentConversation(): DConversationItem | undefined;
10
+ getCurrentConversation(): ConversationItem | undefined;
18
11
  list: ConversationID[];
19
- map: Record<ConversationID, DConversationItem>;
12
+ map: Record<ConversationID, ConversationItem>;
20
13
  /** Signal incremented when a truly new conversation arrives and a full refetch is needed. */
21
14
  newConversationSignal: number;
22
- updateConversations(conversations: DConversationItem[]): void;
15
+ updateConversations(conversations: ConversationItem[]): void;
23
16
  /**
24
17
  * Patches existing conversations with real-time data from OpenIM events.
25
18
  * Returns true if any conversation was not found in the store (triggers refetch).
26
19
  */
27
- mergeOpenIMConversations(conversations: ConversationItem[]): boolean;
28
20
  removeConversation(id: ConversationID): void;
29
- updateLastMessage(id: ConversationID, message: MessageItem): void;
30
21
  reset(): void;
31
22
  }
32
23
 
@@ -56,35 +47,6 @@ export const useConversationStore = create<ConversationStore>()((set, get) => ({
56
47
  )
57
48
  );
58
49
  },
59
- mergeOpenIMConversations(conversations) {
60
- const currentMap = get().map;
61
- let hasUnknown = false;
62
- let changed = false;
63
-
64
- const nextMap = { ...currentMap };
65
- for (const openim of conversations) {
66
- const existing = currentMap[openim.conversationID];
67
- if (existing == null) {
68
- hasUnknown = true;
69
- continue;
70
- }
71
- nextMap[openim.conversationID] = mergeOpenIMIntoConversation(
72
- existing,
73
- openim
74
- );
75
- changed = true;
76
- }
77
-
78
- if (changed) {
79
- set(sortConversation(nextMap));
80
- }
81
-
82
- if (hasUnknown) {
83
- set((s) => ({ newConversationSignal: s.newConversationSignal + 1 }));
84
- }
85
-
86
- return hasUnknown;
87
- },
88
50
  removeConversation(id) {
89
51
  const newMap = { ...get().map };
90
52
  delete newMap[id];
@@ -94,17 +56,6 @@ export const useConversationStore = create<ConversationStore>()((set, get) => ({
94
56
  list: get().list.filter((item) => item !== id),
95
57
  });
96
58
  },
97
- updateLastMessage(id, message) {
98
- const conversation = get().map[id];
99
- if (conversation == null) return;
100
-
101
- set(
102
- sortConversation({
103
- [id]: { ...conversation, lastMessage: message },
104
- ...get().map,
105
- })
106
- );
107
- },
108
59
  reset() {
109
60
  set({
110
61
  currentConversationId: undefined,
@@ -115,6 +66,6 @@ export const useConversationStore = create<ConversationStore>()((set, get) => ({
115
66
  },
116
67
  }));
117
68
 
118
- export const useConversation = (id: string): DConversationItem | undefined => {
69
+ export const useConversation = (id: string): ConversationItem | undefined => {
119
70
  return useConversationStore((state) => state.map[id]);
120
71
  };
@@ -1,6 +1,15 @@
1
1
  const i18nKey: Record<string, string> = {
2
2
  'thread_card_bot_crm_name': 'Droppii Hỗ Trợ',
3
3
  'thread_card_fallback_name': 'Người dùng',
4
+ 'chat:file_too_large': 'Tệp quá lớn. Tối đa 10MB',
5
+ 'chat:file_max_count': 'Chỉ có thể đính kèm tối đa 3 tệp',
6
+ 'chat:file_pick_failed': 'Không thể chọn tệp',
7
+ 'chat:file_upload_failed': 'Lỗi tải tệp lên',
8
+ 'chat:file_preview_unsupported': 'Không thể xem trước tệp này',
9
+ 'chat:file_downloading': 'Đang tải {{received}}/{{total}} MB',
10
+ 'chat:file_downloaded_success': 'Đã tải thành công {{fileName}}',
11
+ 'chat:file_download_failed': 'Tải tệp thất bại',
12
+ 'chat:file_open_failed': 'Không thể mở tệp này',
4
13
  'chat:image_preview': 'Xem trước ảnh',
5
14
  'chat:image_too_large': 'Ảnh quá lớn. Tối đa 5MB',
6
15
  'chat:video_too_large': 'Video quá lớn. Tối đa 200MB',
@@ -1,4 +1,4 @@
1
- export type AttachmentType = 'image' | 'video' | 'file' | 'audio';
1
+ export type AttachmentType = 'image' | 'video' | 'file' | 'audio' | 'document';
2
2
 
3
3
  export interface BaseAttachment {
4
4
  id: string;
@@ -83,3 +83,25 @@ export interface AttachmentSendResult {
83
83
  mergedMessageId?: string;
84
84
  errors: Record<string, string>;
85
85
  }
86
+
87
+ interface AttachmentMetadata {
88
+ id?: string;
89
+ path: string;
90
+ filename?: string;
91
+ size?: number;
92
+ duration?: number;
93
+ mimeType?: string;
94
+ width?: number;
95
+ height?: number;
96
+ uri?: string;
97
+ }
98
+
99
+ export interface AttachmentItem {
100
+ metadata: AttachmentMetadata;
101
+ uploadProgress: number;
102
+ error: string | null;
103
+ isUploading: boolean;
104
+ uploadedUrl?: string;
105
+ thumbnailUrl?: string;
106
+ type: AttachmentType;
107
+ }
package/src/types/auth.ts CHANGED
@@ -16,6 +16,7 @@ type Pageable = {
16
16
  export type GetOpenIMTokenResponse = {
17
17
  id: string;
18
18
  token: string;
19
+ expiresIn: number;
19
20
  };
20
21
 
21
22
  export enum SyncStatus {
package/src/types/chat.ts CHANGED
@@ -1,8 +1,4 @@
1
- import {
2
- type MessageItem,
3
- type ConversationItem,
4
- PeerType,
5
- } from '@droppii/openim-rn-client-sdk';
1
+ import { type MessageItem } from '@droppii/openim-rn-client-sdk';
6
2
  import type { PropsWithChildren } from 'react';
7
3
  import type { IUrlMetadata } from './common';
8
4
  import type {
@@ -10,6 +6,7 @@ import type {
10
6
  IConversationFlowStep,
11
7
  IRunConversationFlowPayload,
12
8
  } from './flows';
9
+ import type { GetOpenIMTokenResponse } from './auth';
13
10
 
14
11
  export const EventProvider = {
15
12
  ga: 'ga',
@@ -31,6 +28,7 @@ export interface ChatContextType {
31
28
  export type ChatProviderProps = PropsWithChildren<{
32
29
  logGA?: LogGA;
33
30
  applicationType: DChatApplicationType;
31
+ getChatToken: () => Promise<GetOpenIMTokenResponse | null>;
34
32
  }>;
35
33
 
36
34
  export enum DChatApplicationType {
@@ -66,20 +64,6 @@ export interface DConversationPeer {
66
64
  group: null; // to be defined when group is implemented
67
65
  }
68
66
 
69
- export interface DConversationItem extends ConversationItem {
70
- // Droppii API fields
71
- conversationId: string;
72
- chatCategory: DChatCategory;
73
- chatType: PeerType;
74
- applicationType: DChatApplicationType;
75
- memberRole: DMemberRole;
76
- pinnedAt: string | null;
77
- isMuted: boolean;
78
- peer: DConversationPeerUser;
79
- // Merged from OpenIM (available after getConversationListSplit merge)
80
- lastMessage?: DMessageItem;
81
- }
82
-
83
67
  export interface DConversationQueryParams {
84
68
  applicationType: string;
85
69
  page: number;
@@ -146,7 +130,19 @@ export interface ChatDetailContextType {
146
130
  conversationFlow?: IConversationFlow;
147
131
  refetchConversationFlow: () => void;
148
132
  isBotConversation: boolean;
149
- setConverationFlowStep: (step: IConversationFlowStep) => void;
133
+ setConverationFlowStep: (step?: IConversationFlowStep) => void;
150
134
  }
151
135
 
152
136
  export interface ChatDetailProviderProps extends PropsWithChildren {}
137
+
138
+ export interface IConversationItemEx {
139
+ applicationType: DChatApplicationType;
140
+ sessionInfo?: {
141
+ type: 'SESSION_INFO';
142
+ data: {
143
+ ownerId: string;
144
+ botId: string;
145
+ };
146
+ };
147
+ chatCategory: DChatCategory;
148
+ }
@@ -3,7 +3,6 @@ import {
3
3
  SessionType,
4
4
  type ConversationItem,
5
5
  } from '@droppii/openim-rn-client-sdk';
6
- import { type DConversationItem, type DMessageItem } from '../types/chat';
7
6
  import { trans } from '../translation';
8
7
 
9
8
  export const conversationCompare = (
@@ -24,43 +23,6 @@ export const conversationCompare = (
24
23
  return bCompare - aCompare;
25
24
  };
26
25
 
27
- export const dConversationCompare = (
28
- a: DConversationItem,
29
- b: DConversationItem
30
- ) => {
31
- if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
32
-
33
- const aTime =
34
- a.draftTextTime > a.latestMsgSendTime
35
- ? a.draftTextTime
36
- : a.latestMsgSendTime;
37
- const bTime =
38
- b.draftTextTime > b.latestMsgSendTime
39
- ? b.draftTextTime
40
- : b.latestMsgSendTime;
41
-
42
- return bTime - aTime;
43
- };
44
-
45
- export const mergeOpenIMIntoConversation = (
46
- existing: DConversationItem,
47
- openim: ConversationItem
48
- ): DConversationItem => {
49
- const { latestMsg, ...openimRest } = openim;
50
-
51
- let lastMessage: DMessageItem | undefined = existing.lastMessage;
52
- if (latestMsg) {
53
- try {
54
- lastMessage = JSON.parse(latestMsg) as DMessageItem;
55
- } catch {
56
- // keep existing lastMessage if parse fails
57
- }
58
- }
59
-
60
- // Spread existing first so peerType (not in ConversationItem) is preserved
61
- return { ...existing, ...openimRest, lastMessage };
62
- };
63
-
64
26
  export const getConversationID = (
65
27
  sessionType: SessionType,
66
28
  senderID?: string,
@@ -76,9 +38,9 @@ export const getConversationID = (
76
38
  return [prefix, senderID, receiverID].filter(Boolean).join('_');
77
39
  };
78
40
 
79
- export const sortConversation = (map: Record<string, DConversationItem>) => {
41
+ export const sortConversation = (map: Record<string, ConversationItem>) => {
80
42
  const list = Object.keys(map).sort((a, b) =>
81
- dConversationCompare(map[a]!, map[b]!)
43
+ conversationCompare(map[a]!, map[b]!)
82
44
  );
83
45
  return { map, list };
84
46
  };
@@ -1,4 +1,18 @@
1
1
  import ImagePicker from 'react-native-image-crop-picker';
2
+ import {
3
+ pick as pickDocument,
4
+ types as documentTypes,
5
+ isErrorWithCode,
6
+ errorCodes,
7
+ keepLocalCopy,
8
+ } from '@react-native-documents/picker';
9
+
10
+ export interface PickedFile {
11
+ uri: string;
12
+ name: string;
13
+ size: number;
14
+ mimeType: string;
15
+ }
2
16
 
3
17
  const DeviceUtils = {
4
18
  openImagePicker: async (multiple: boolean = false) => {
@@ -68,6 +82,50 @@ const DeviceUtils = {
68
82
  throw error;
69
83
  }
70
84
  },
85
+
86
+ openFilePicker: async (
87
+ multiple: boolean = false
88
+ ): Promise<PickedFile[] | null> => {
89
+ try {
90
+ const results = await pickDocument({
91
+ allowMultiSelection: multiple,
92
+ type: [documentTypes.allFiles],
93
+ mode: 'import',
94
+ });
95
+
96
+ // Copy picked files to app's document directory so uploads can access them
97
+ const copies = await keepLocalCopy({
98
+ files: results.map((r) => ({
99
+ uri: r.uri,
100
+ fileName: r.name ?? 'file',
101
+ })) as [
102
+ { uri: string; fileName: string },
103
+ ...{ uri: string; fileName: string }[],
104
+ ],
105
+ destination: 'documentDirectory',
106
+ });
107
+
108
+ const files = results.map((r, i) => ({
109
+ uri: decodeURIComponent(
110
+ copies[i]?.status === 'success' ? copies[i].localUri : r.uri
111
+ ),
112
+ name: r.name ?? 'file',
113
+ size: r.size ?? 0,
114
+ mimeType: r.type ?? 'application/octet-stream',
115
+ }));
116
+
117
+ return files;
118
+ } catch (error: any) {
119
+ if (
120
+ isErrorWithCode(error) &&
121
+ error.code === errorCodes.OPERATION_CANCELED
122
+ ) {
123
+ return null;
124
+ }
125
+ console.error('[File Picker] Error:', error);
126
+ throw error;
127
+ }
128
+ },
71
129
  };
72
130
 
73
131
  export default DeviceUtils;