@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
@@ -0,0 +1,200 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Platform, Share } from 'react-native';
3
+ import RNFS from 'react-native-fs';
4
+ import FileViewer from 'react-native-file-viewer';
5
+ import UIUtils from '../utils/ui';
6
+ import { trans } from '../translation';
7
+
8
+ export type DownloadStatus = 'idle' | 'downloading' | 'done' | 'error';
9
+
10
+ export interface DownloadState {
11
+ status: DownloadStatus;
12
+ progress: number;
13
+ receivedMB: number;
14
+ totalMB: number;
15
+ localPath: string | null;
16
+ error: string | null;
17
+ }
18
+
19
+ const INITIAL_STATE: DownloadState = {
20
+ status: 'idle',
21
+ progress: 0,
22
+ receivedMB: 0,
23
+ totalMB: 0,
24
+ localPath: null,
25
+ error: null,
26
+ };
27
+
28
+ const CacheDir = RNFS.CachesDirectoryPath;
29
+ const DownloadDir =
30
+ Platform.OS === 'android'
31
+ ? RNFS.DownloadDirectoryPath
32
+ : RNFS.CachesDirectoryPath;
33
+
34
+ export const useFileDownload = (fileName: string, url: string) => {
35
+ const [state, setState] = useState<DownloadState>(INITIAL_STATE);
36
+ const jobIdRef = useRef<number | null>(null);
37
+ const cancelledRef = useRef(false);
38
+
39
+ const openFile = useCallback(async (path: string) => {
40
+ try {
41
+ await FileViewer.open(path, {
42
+ showOpenWithDialog: true,
43
+ showAppsSuggestions: true,
44
+ });
45
+ } catch {
46
+ UIUtils.toast.open({
47
+ title: trans('chat:file_open_failed'),
48
+ theme: 'warning',
49
+ });
50
+ }
51
+ }, []);
52
+
53
+ const runDownload = useCallback(
54
+ async (destDir: string): Promise<string | null> => {
55
+ if (!url || !fileName) return null;
56
+
57
+ const destPath = `${destDir}/${fileName}`;
58
+ cancelledRef.current = false;
59
+ setState({
60
+ status: 'downloading',
61
+ progress: 0,
62
+ receivedMB: 0,
63
+ totalMB: 0,
64
+ localPath: null,
65
+ error: null,
66
+ });
67
+
68
+ const MB = 1024 * 1024;
69
+
70
+ const ret = RNFS.downloadFile({
71
+ fromUrl: url,
72
+ toFile: destPath,
73
+ progress: (res) => {
74
+ if (cancelledRef.current) return;
75
+ const r = res.bytesWritten;
76
+ const t = res.contentLength;
77
+ setState((prev) => ({
78
+ ...prev,
79
+ progress: t > 0 ? Math.round((r / t) * 100) : 0,
80
+ receivedMB: Math.round((r / MB) * 10) / 10,
81
+ totalMB: Math.round((t / MB) * 10) / 10,
82
+ }));
83
+ },
84
+ });
85
+
86
+ jobIdRef.current = ret.jobId;
87
+
88
+ try {
89
+ const result = await ret.promise;
90
+ if (cancelledRef.current) return null;
91
+
92
+ if (result.statusCode < 200 || result.statusCode >= 300) {
93
+ throw new Error(`HTTP ${result.statusCode}`);
94
+ }
95
+
96
+ setState({
97
+ status: 'done',
98
+ progress: 100,
99
+ receivedMB: 0,
100
+ totalMB: 0,
101
+ localPath: destPath,
102
+ error: null,
103
+ });
104
+ return destPath;
105
+ } catch (e: any) {
106
+ if (cancelledRef.current) return null;
107
+ setState({
108
+ status: 'error',
109
+ progress: 0,
110
+ receivedMB: 0,
111
+ totalMB: 0,
112
+ localPath: null,
113
+ error: e?.message ?? trans('chat:file_download_failed'),
114
+ });
115
+ return null;
116
+ }
117
+ },
118
+ [fileName, url]
119
+ );
120
+
121
+ const cancel = useCallback(() => {
122
+ cancelledRef.current = true;
123
+ if (jobIdRef.current !== null) {
124
+ RNFS.stopDownload(jobIdRef.current);
125
+ jobIdRef.current = null;
126
+ }
127
+ setState(INITIAL_STATE);
128
+ }, []);
129
+
130
+ useEffect(() => () => cancel(), [cancel]);
131
+
132
+ // Eye button — open from cache/downloads if available, otherwise download to cache
133
+ const handleView = useCallback(async () => {
134
+ if (state.status === 'downloading') return;
135
+
136
+ if (state.localPath) {
137
+ try {
138
+ const exists = await RNFS.exists(state.localPath);
139
+ if (exists) {
140
+ await openFile(state.localPath);
141
+ return;
142
+ }
143
+ } catch {
144
+ // fall through to search dirs
145
+ }
146
+ }
147
+
148
+ const searchDirs =
149
+ Platform.OS === 'android' ? [DownloadDir, CacheDir] : [CacheDir];
150
+ for (const dir of searchDirs) {
151
+ const dirPath = `${dir}/${fileName}`;
152
+ try {
153
+ const exists = await RNFS.exists(dirPath);
154
+ if (exists) {
155
+ setState({
156
+ status: 'done',
157
+ progress: 100,
158
+ receivedMB: 0,
159
+ totalMB: 0,
160
+ localPath: dirPath,
161
+ error: null,
162
+ });
163
+ await openFile(dirPath);
164
+ return;
165
+ }
166
+ } catch {
167
+ // try next dir
168
+ }
169
+ }
170
+
171
+ const path = await runDownload(CacheDir);
172
+ if (path) openFile(path);
173
+ }, [state.status, state.localPath, fileName, openFile, runDownload]);
174
+
175
+ // Download button — cancel if in progress; otherwise download and expose to Files app.
176
+ // iOS: download to cache then show share sheet (user can tap "Save to Files").
177
+ // Android: download to Downloads dir and trigger media scan.
178
+ const handleDownload = useCallback(async () => {
179
+ if (state.status === 'downloading') {
180
+ cancel();
181
+ return;
182
+ }
183
+ if (Platform.OS === 'ios') {
184
+ const cachedPath = `${CacheDir}/${fileName}`;
185
+ const exists = await RNFS.exists(cachedPath);
186
+ const localPath = exists ? cachedPath : await runDownload(CacheDir);
187
+ if (localPath) {
188
+ await Share.share({ url: `file://${localPath}` });
189
+ }
190
+ } else {
191
+ await runDownload(DownloadDir);
192
+ }
193
+ UIUtils.toast.open({
194
+ title: trans('chat:file_downloaded_success', { fileName }),
195
+ theme: 'info',
196
+ });
197
+ }, [state.status, fileName, runDownload, cancel]);
198
+
199
+ return { state, handleView, handleDownload };
200
+ };
@@ -7,7 +7,7 @@ import { getMimeType, isVideoFromPicker } from '../utils/imageUtils';
7
7
  import { ImageUploadAPI } from '../services/imageUpload';
8
8
  import VideoThumbnailUtil from '../utils/videoThumbnail';
9
9
 
10
- interface ImageWithUploadStatus {
10
+ export interface ImageWithUploadStatus {
11
11
  image: PickedImage;
12
12
  uploadProgress: number;
13
13
  error: string | null;
@@ -27,5 +27,10 @@ export function useLinkPreview(value: string | undefined) {
27
27
 
28
28
  const isVisible = !!detectedUrl && !isDismissed && (isLoading || !!metadata);
29
29
 
30
- return { isVisible, metadata: metadata || undefined, isLoading, dismiss };
30
+ return {
31
+ isVisible,
32
+ metadata: !isLoading ? metadata || undefined : undefined,
33
+ isLoading,
34
+ dismiss,
35
+ };
31
36
  }
@@ -21,15 +21,17 @@ import { getAttachmentPanelHeight } from './constants';
21
21
  import type { ChatComposerProps } from './types';
22
22
  import { useLinkPreview } from '../../hooks/useLinkPreview/useLinkPreview';
23
23
  import { useImageAttachment } from '../../hooks/useImageAttachment';
24
+ import { useFileAttachment } from '../../hooks/useFileAttachment';
24
25
  import { useSendAttachment } from '../../hooks/useSendAttachment';
25
26
  import { useChatComposerState } from './hooks/useChatComposerState';
26
27
  import { useChatComposerAnimation } from './hooks/useChatComposerAnimation';
27
28
  import { useAttachmentSendHandler } from './hooks/useAttachmentSendHandler';
28
- import { isVideoFromPicker } from '../../utils/imageUtils';
29
29
  import { useChatDetailContext } from '../../context/ChatDetailContext';
30
30
  import { trans } from '../../translation';
31
31
  import { useRunFlow } from '../../hooks/flows/useFlow';
32
32
  import { ReactFlowsNodeTypes } from '../../types/flows';
33
+ import { useAttachmentMapping } from './hooks/useAttachmentMapping';
34
+ import type { AttachmentType } from '../../types/attachment';
33
35
 
34
36
  export const ChatComposer = memo(
35
37
  ({
@@ -73,6 +75,15 @@ export const ChatComposer = memo(
73
75
  clearAllImages,
74
76
  } = useImageAttachment();
75
77
 
78
+ // File attachment
79
+ const {
80
+ selectedFiles,
81
+ isFilePreviewVisible,
82
+ handleFileAttach,
83
+ handleFileRemove,
84
+ clearAllFiles,
85
+ } = useFileAttachment();
86
+
76
87
  // Send attachment
77
88
  const { send: sendAttachments } = useSendAttachment();
78
89
 
@@ -95,8 +106,10 @@ export const ChatComposer = memo(
95
106
  // Attachment send handler
96
107
  const { handleSend: handleAttachmentSend } = useAttachmentSendHandler({
97
108
  selectedImages,
109
+ selectedFiles,
98
110
  sendAttachments,
99
111
  clearAllImages,
112
+ clearAllFiles,
100
113
  onSend,
101
114
  onMessageSent,
102
115
  textMessage: (value ?? '').trim(),
@@ -135,15 +148,18 @@ export const ChatComposer = memo(
135
148
  onAttachmentOpenChange: setIsAttachmentOpen,
136
149
  });
137
150
 
138
- // Check if any attachments are currently uploading
151
+ // Check if any attachments are currently uploading or have errors
139
152
  const isUploadingAttachments = useMemo(
140
- () => selectedImages.some((img: any) => img.isUploading),
141
- [selectedImages]
153
+ () =>
154
+ selectedImages.some((img: any) => img.isUploading) ||
155
+ selectedFiles.some((f) => f.isUploading || !!f.error),
156
+ [selectedImages, selectedFiles]
142
157
  );
143
158
 
144
159
  // Send handler - includes attachment and preview link features
145
160
  const handleSend = useCallback(() => {
146
- const hasAttachments = selectedImages.length > 0;
161
+ const hasAttachments =
162
+ selectedImages.length > 0 || selectedFiles.length > 0;
147
163
  const hasText = (value ?? '').trim().length > 0;
148
164
  // Only send metadata if preview is CURRENTLY visible (user didn't dismiss it)
149
165
  const previewMetadata =
@@ -184,6 +200,7 @@ export const ChatComposer = memo(
184
200
  urlMetadata,
185
201
  showLinkPreview,
186
202
  selectedImages,
203
+ selectedFiles,
187
204
  handleAttachmentSend,
188
205
  value,
189
206
  isUploadingAttachments,
@@ -232,9 +249,22 @@ export const ChatComposer = memo(
232
249
  }
233
250
  }, [value, setIsInputWrapped]);
234
251
 
235
- // Debounced image picker (prevent rapid clicks)
252
+ const handleImageAttachRef = useRef(handleImageAttach);
253
+ handleImageAttachRef.current = handleImageAttach;
236
254
  const debouncedImagePickerRef = useRef(
237
- debounce(handleImageAttach, 300, { leading: true, trailing: false })
255
+ debounce(() => handleImageAttachRef.current(), 300, {
256
+ leading: true,
257
+ trailing: false,
258
+ })
259
+ );
260
+
261
+ const handleFileAttachRef = useRef(handleFileAttach);
262
+ handleFileAttachRef.current = handleFileAttach;
263
+ const debouncedFilePickerRef = useRef(
264
+ debounce(() => handleFileAttachRef.current(), 300, {
265
+ leading: true,
266
+ trailing: false,
267
+ })
238
268
  );
239
269
 
240
270
  // Attachment action handler
@@ -242,6 +272,8 @@ export const ChatComposer = memo(
242
272
  async (action: any) => {
243
273
  if (action.id === 'gallery') {
244
274
  debouncedImagePickerRef.current();
275
+ } else if (action.id === 'file') {
276
+ debouncedFilePickerRef.current();
245
277
  } else {
246
278
  onAttachmentAction?.(action);
247
279
  }
@@ -257,6 +289,24 @@ export const ChatComposer = memo(
257
289
  [insets.bottom, isAttachmentOpen]
258
290
  );
259
291
 
292
+ const attachments = useAttachmentMapping({
293
+ selectedImages,
294
+ selectedFiles,
295
+ });
296
+
297
+ const onRemove = useCallback(
298
+ (type: AttachmentType, data: unknown) => {
299
+ switch (type) {
300
+ case 'file':
301
+ return handleFileRemove(data as string);
302
+
303
+ default:
304
+ return handleImageRemove(data as number);
305
+ }
306
+ },
307
+ [handleImageRemove, handleFileRemove]
308
+ );
309
+
260
310
  if (
261
311
  isBotConversation &&
262
312
  conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
@@ -278,6 +328,7 @@ export const ChatComposer = memo(
278
328
  </KContainer.View>
279
329
  );
280
330
  }
331
+
281
332
  return (
282
333
  <KContainer.View background={'rgba(131, 137, 157, 0.05)'}>
283
334
  <ChatQuickActions
@@ -297,29 +348,9 @@ export const ChatComposer = memo(
297
348
  )}
298
349
 
299
350
  <AttachmentPreview
300
- visible={isImagePreviewVisible}
301
- attachments={selectedImages.map((img: any) => {
302
- const isVideo = isVideoFromPicker(img.image);
303
- return {
304
- type: isVideo ? ('video' as const) : ('image' as const),
305
- metadata: {
306
- path: img.image.path,
307
- filename: img.image.filename,
308
- size: img.image.size,
309
- width: img.image.width,
310
- height: img.image.height,
311
- ...(isVideo && { duration: (img.image as any).duration }),
312
- },
313
- uploadProgress: img.uploadProgress,
314
- error: img.error,
315
- isUploading: img.isUploading,
316
- uploadedUrl: img.uploadedUrl,
317
- ...(isVideo && { thumbnailUrl: img.thumbnailUrl }),
318
- };
319
- })}
320
- onRemove={(type, index) => {
321
- handleImageRemove(index);
322
- }}
351
+ visible={isImagePreviewVisible || isFilePreviewVisible}
352
+ attachments={attachments}
353
+ onRemove={onRemove}
323
354
  />
324
355
 
325
356
  <KContainer.View
@@ -43,8 +43,6 @@ const ChatDetail = memo(
43
43
  subtitle,
44
44
  avatarUri,
45
45
  avatarFullName,
46
- peerType,
47
- applicationType,
48
46
  showAddMember,
49
47
  getSubtitle,
50
48
  onBack,
@@ -139,9 +137,6 @@ const ChatDetail = memo(
139
137
  const resolvedAvatarUri =
140
138
  avatarUri ?? getConversationAvatarUri(conversation);
141
139
  const resolvedAvatarFullName = avatarFullName ?? resolvedTitle;
142
- const resolvedPeerType = peerType;
143
- const resolvedApplicationType =
144
- applicationType ?? conversation?.applicationType;
145
140
  const resolvedShowAddMember =
146
141
  showAddMember ?? shouldShowAddMember(conversation);
147
142
  const resolvedSubtitle = useMemo(() => {
@@ -234,12 +229,11 @@ const ChatDetail = memo(
234
229
  <ChatDetailProvider>
235
230
  <KContainer.Page flex edges={['bottom']}>
236
231
  <ChatDetailHeader
232
+ conversationId={conversationId}
237
233
  title={resolvedTitle}
238
234
  subtitle={resolvedSubtitle}
239
235
  avatarUri={resolvedAvatarUri}
240
236
  avatarFullName={resolvedAvatarFullName}
241
- peerType={resolvedPeerType}
242
- applicationType={resolvedApplicationType}
243
237
  showAddMember={resolvedShowAddMember}
244
238
  onBack={onBack ?? handleBackDefault}
245
239
  onPressSearch={
@@ -9,6 +9,7 @@ import {
9
9
  import { AvatarSection } from '../../components/ThreadCard/AvatarSection';
10
10
  import { NamePrefixIcon } from '../../components/ThreadCard/NamePrefixIcon';
11
11
  import type { ChatDetailHeaderProps } from './types';
12
+ import { useConversation } from '../../store';
12
13
 
13
14
  const ChatDetailHeader = memo(
14
15
  ({
@@ -16,15 +17,15 @@ const ChatDetailHeader = memo(
16
17
  subtitle,
17
18
  avatarUri,
18
19
  avatarFullName,
19
- peerType,
20
- applicationType,
21
20
  showAddMember = false,
21
+ conversationId,
22
22
  onBack,
23
23
  onPressSearch,
24
24
  onPressAddMember,
25
25
  onPressMenu,
26
26
  onPressAvatar,
27
27
  }: ChatDetailHeaderProps) => {
28
+ const conversation = useConversation(conversationId);
28
29
  const rightButtons = useMemo(() => {
29
30
  const buttons = [
30
31
  {
@@ -80,13 +81,12 @@ const ChatDetailHeader = memo(
80
81
  <AvatarSection
81
82
  avatar={avatarUri ?? null}
82
83
  fullName={avatarFullName ?? title}
83
- peerType={peerType}
84
- applicationType={applicationType}
84
+ peerType={conversation?.peerType}
85
85
  />
86
86
 
87
87
  <KContainer.View flex marginL="0.5rem">
88
88
  <KContainer.View row alignItems>
89
- <NamePrefixIcon peerType={peerType} />
89
+ <NamePrefixIcon peerType={conversation?.peerType} />
90
90
  <KLabel.Text
91
91
  typo="TextLgBold"
92
92
  color={KColors.gray.dark}
@@ -112,12 +112,11 @@ const ChatDetailHeader = memo(
112
112
  [
113
113
  avatarFullName,
114
114
  avatarUri,
115
- peerType,
116
- applicationType,
117
115
  onBack,
118
116
  onPressAvatar,
119
117
  subtitle,
120
118
  title,
119
+ conversation?.peerType,
121
120
  ]
122
121
  );
123
122
 
@@ -34,18 +34,22 @@ export const ChatLinkPreview = memo(
34
34
  />
35
35
  )}
36
36
  </KContainer.View>
37
- <KContainer.View style={styles.center}>
38
- <KLabel.Text typo="TextNmMedium" numberOfLines={1}>
39
- {metadata?.title}
40
- </KLabel.Text>
41
- <KLabel.Text
42
- typo="TextSmNormal"
43
- color={KColors.gray.light}
44
- numberOfLines={1}
45
- >
46
- {metadata?.description}
47
- </KLabel.Text>
48
- </KContainer.View>
37
+ {metadata ? (
38
+ <KContainer.View style={styles.center}>
39
+ <KLabel.Text typo="TextNmMedium" numberOfLines={1}>
40
+ {metadata?.title}
41
+ </KLabel.Text>
42
+ <KLabel.Text
43
+ typo="TextSmNormal"
44
+ color={KColors.gray.light}
45
+ numberOfLines={1}
46
+ >
47
+ {metadata?.description}
48
+ </KLabel.Text>
49
+ </KContainer.View>
50
+ ) : (
51
+ <KContainer.View flex />
52
+ )}
49
53
  <KContainer.Touchable onPress={onDismiss} style={styles.right}>
50
54
  <KImage.VectorIcons name="close-o" size={16} />
51
55
  </KContainer.Touchable>
@@ -349,12 +349,8 @@ export const ChatListLegend = memo(
349
349
  renderItem={renderItem}
350
350
  estimatedItemSize={avgItemHeightRef.current}
351
351
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
352
- recycleItems={!isBotConversation}
353
- extraData={
354
- !isBotConversation
355
- ? `${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`
356
- : undefined
357
- }
352
+ recycleItems={false}
353
+ extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`}
358
354
  alignItemsAtEnd
359
355
  maintainVisibleContentPosition={maintainVisiblePositionConfig}
360
356
  maintainScrollAtEnd={maintainScrollConfig}
@@ -1,8 +1,8 @@
1
- import { PeerType } from '@droppii/openim-rn-client-sdk';
2
- import { type DConversationItem } from '../../types/chat';
1
+ import { PeerType, type ConversationItem } from '@droppii/openim-rn-client-sdk';
2
+ import type { IConversationItemEx } from '../../types/chat';
3
3
 
4
4
  export const getConversationTitle = (
5
- conversation?: DConversationItem
5
+ conversation?: ConversationItem
6
6
  ): string => {
7
7
  if (!conversation) {
8
8
  return 'Tin nhắn';
@@ -12,13 +12,13 @@ export const getConversationTitle = (
12
12
  };
13
13
 
14
14
  export const getConversationAvatarUri = (
15
- conversation?: DConversationItem
15
+ conversation?: ConversationItem
16
16
  ): string | null => {
17
17
  return conversation?.faceURL ?? null;
18
18
  };
19
19
 
20
20
  export const getConversationSubtitle = (
21
- conversation?: DConversationItem
21
+ conversation?: ConversationItem
22
22
  ): string | undefined => {
23
23
  if (!conversation) {
24
24
  return undefined;
@@ -40,7 +40,17 @@ export const getConversationSubtitle = (
40
40
  };
41
41
 
42
42
  export const shouldShowAddMember = (
43
- conversation?: DConversationItem
43
+ conversation?: ConversationItem
44
44
  ): boolean => {
45
45
  return conversation?.peerType === PeerType.Group;
46
46
  };
47
+
48
+ export const getConversationEx = (ex?: string): IConversationItemEx | {} => {
49
+ let result: IConversationItemEx | {} = {};
50
+ try {
51
+ result = JSON.parse(ex || '{}');
52
+ } catch {
53
+ ///
54
+ }
55
+ return result;
56
+ };
@@ -0,0 +1,58 @@
1
+ import { isVideoFromPicker } from '../../../utils/imageUtils';
2
+ import type { ImageWithUploadStatus } from '../../../hooks/useImageAttachment';
3
+ import type { FileWithUploadStatus } from '../../../hooks/useFileAttachment';
4
+ import type { AttachmentItem } from '../../../types/attachment';
5
+ import { useMemo } from 'react';
6
+
7
+ type Props = {
8
+ selectedImages: ImageWithUploadStatus[];
9
+ selectedFiles: FileWithUploadStatus[];
10
+ };
11
+
12
+ const mapMedia = (img: ImageWithUploadStatus): AttachmentItem => {
13
+ const isVideo = isVideoFromPicker(img.image);
14
+ return {
15
+ type: isVideo ? ('video' as const) : ('image' as const),
16
+ metadata: {
17
+ path: img.image.path,
18
+ filename: img.image.filename,
19
+ size: img.image.size,
20
+ width: img.image.width,
21
+ height: img.image.height,
22
+ ...(isVideo && { duration: (img.image as any).duration }),
23
+ },
24
+ uploadProgress: img.uploadProgress,
25
+ error: img.error,
26
+ isUploading: img.isUploading,
27
+ uploadedUrl: img.uploadedUrl,
28
+ ...(isVideo && { thumbnailUrl: img.thumbnailUrl }),
29
+ };
30
+ };
31
+
32
+ const mapFile = (file: FileWithUploadStatus): AttachmentItem => {
33
+ return {
34
+ type: 'file',
35
+ metadata: {
36
+ id: file.id,
37
+ path: file.uri,
38
+ filename: file.name,
39
+ size: file.size,
40
+ mimeType: file.mimeType,
41
+ uri: file.uri,
42
+ },
43
+ uploadProgress: file.uploadProgress,
44
+ error: file.error,
45
+ isUploading: file.isUploading,
46
+ uploadedUrl: file.uploadedUrl,
47
+ };
48
+ };
49
+
50
+ export const useAttachmentMapping = ({
51
+ selectedImages,
52
+ selectedFiles,
53
+ }: Props) => {
54
+ return useMemo(
55
+ () => [...selectedImages.map(mapMedia), ...selectedFiles.map(mapFile)],
56
+ [selectedImages, selectedFiles]
57
+ );
58
+ };