@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.
- package/lib/module/assets/images/icon_excel.png +0 -0
- package/lib/module/assets/images/icon_img.png +0 -0
- package/lib/module/assets/images/icon_pdf.png +0 -0
- package/lib/module/assets/images/icon_txt.png +0 -0
- package/lib/module/assets/images/icon_word.png +0 -0
- package/lib/module/assets/images/index.js +5 -0
- package/lib/module/assets/images/index.js.map +1 -1
- package/lib/module/components/AttachmentPreview/FilePreview.js +107 -0
- package/lib/module/components/AttachmentPreview/FilePreview.js.map +1 -0
- package/lib/module/components/AttachmentPreview/MediaPreview.js +212 -0
- package/lib/module/components/AttachmentPreview/MediaPreview.js.map +1 -0
- package/lib/module/components/AttachmentPreview/index.js +49 -0
- package/lib/module/components/AttachmentPreview/index.js.map +1 -0
- package/lib/module/components/ThreadCard/AvatarSection.js.map +1 -1
- package/lib/module/components/ThreadCard/ThreadCard.js +17 -9
- package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
- package/lib/module/components/flows/inputButtons/index.js +7 -3
- package/lib/module/components/flows/inputButtons/index.js.map +1 -1
- package/lib/module/components/messages/fileMessage/index.js +122 -12
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/context/ChatContext.js +27 -4
- package/lib/module/context/ChatContext.js.map +1 -1
- package/lib/module/context/ChatDetailContext.js.map +1 -1
- package/lib/module/core/useChatListener.js +2 -4
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/core/useUserListener.js +0 -7
- package/lib/module/core/useUserListener.js.map +1 -1
- package/lib/module/hooks/flows/useFlow.js +14 -3
- package/lib/module/hooks/flows/useFlow.js.map +1 -1
- package/lib/module/hooks/message/useSendMessage.js +1 -1
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/hooks/useConversationList.js +3 -27
- package/lib/module/hooks/useConversationList.js.map +1 -1
- package/lib/module/hooks/useFileAttachment.js +116 -0
- package/lib/module/hooks/useFileAttachment.js.map +1 -0
- package/lib/module/hooks/useFileDownload.js +175 -0
- package/lib/module/hooks/useFileDownload.js.map +1 -0
- package/lib/module/hooks/useLinkPreview/useLinkPreview.js +1 -1
- package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +44 -36
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +1 -6
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetailHeader.js +6 -6
- package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatLinkPreview.js +3 -1
- package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +2 -2
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/conversationHeader.utils.js +9 -0
- package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useAttachmentMapping.js +51 -0
- package/lib/module/screens/chat-detail/hooks/useAttachmentMapping.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js +39 -13
- package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +1 -1
- package/lib/module/screens/inbox/Inbox.js +5 -1
- package/lib/module/screens/inbox/Inbox.js.map +1 -1
- package/lib/module/services/apis.js +0 -20
- package/lib/module/services/apis.js.map +1 -1
- package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js +6 -6
- package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js.map +1 -1
- package/lib/module/services/attachmentHandlers/index.js +1 -1
- package/lib/module/services/attachmentHandlers/index.js.map +1 -1
- package/lib/module/store/conversation.js +1 -38
- package/lib/module/store/conversation.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +9 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/auth.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/utils/conversation.js +1 -28
- package/lib/module/utils/conversation.js.map +1 -1
- package/lib/module/utils/device.js +32 -0
- package/lib/module/utils/device.js.map +1 -1
- package/lib/module/utils/imageUtils.js +44 -28
- package/lib/module/utils/imageUtils.js.map +1 -1
- package/lib/module/utils/message.js.map +1 -1
- package/lib/module/utils/messageUtils.js +5 -3
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/typescript/src/assets/images/index.d.ts +5 -0
- package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
- package/lib/typescript/src/components/AttachmentPreview/FilePreview.d.ts +6 -0
- package/lib/typescript/src/components/AttachmentPreview/FilePreview.d.ts.map +1 -0
- package/lib/typescript/src/components/AttachmentPreview/MediaPreview.d.ts +9 -0
- package/lib/typescript/src/components/AttachmentPreview/MediaPreview.d.ts.map +1 -0
- package/lib/typescript/src/components/AttachmentPreview/index.d.ts +9 -0
- package/lib/typescript/src/components/AttachmentPreview/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +2 -2
- package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/core/useUserListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useConversationList.d.ts +2 -2
- package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useFileAttachment.d.ts +20 -0
- package/lib/typescript/src/hooks/useFileAttachment.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useFileDownload.d.ts +15 -0
- package/lib/typescript/src/hooks/useFileDownload.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useImageAttachment.d.ts +1 -2
- package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +7 -5
- package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentMapping.d.ts +10 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentMapping.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts +4 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts +6 -9
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
- package/lib/typescript/src/services/apis.d.ts +0 -4
- package/lib/typescript/src/services/apis.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts +2 -2
- package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentHandlers/index.d.ts +1 -1
- package/lib/typescript/src/services/attachmentHandlers/index.d.ts.map +1 -1
- package/lib/typescript/src/store/conversation.d.ts +5 -8
- package/lib/typescript/src/store/conversation.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/attachment.d.ts +22 -1
- package/lib/typescript/src/types/attachment.d.ts.map +1 -1
- package/lib/typescript/src/types/auth.d.ts +1 -0
- package/lib/typescript/src/types/auth.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +15 -13
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/utils/conversation.d.ts +2 -5
- package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
- package/lib/typescript/src/utils/device.d.ts +7 -0
- package/lib/typescript/src/utils/device.d.ts.map +1 -1
- package/lib/typescript/src/utils/imageUtils.d.ts +1 -0
- package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
- package/lib/typescript/src/utils/message.d.ts +3 -3
- package/lib/typescript/src/utils/message.d.ts.map +1 -1
- package/lib/typescript/src/utils/messageUtils.d.ts +2 -1
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/assets/images/icon_excel.png +0 -0
- package/src/assets/images/icon_img.png +0 -0
- package/src/assets/images/icon_pdf.png +0 -0
- package/src/assets/images/icon_txt.png +0 -0
- package/src/assets/images/icon_word.png +0 -0
- package/src/assets/images/index.ts +5 -0
- package/src/components/AttachmentPreview/FilePreview.tsx +137 -0
- package/src/components/AttachmentPreview/MediaPreview.tsx +238 -0
- package/src/components/AttachmentPreview/index.tsx +68 -0
- package/src/components/ThreadCard/AvatarSection.tsx +2 -3
- package/src/components/ThreadCard/ThreadCard.tsx +22 -15
- package/src/components/flows/inputButtons/index.tsx +4 -1
- package/src/components/messages/fileMessage/index.tsx +156 -13
- package/src/context/ChatContext.tsx +35 -3
- package/src/context/ChatDetailContext.tsx +1 -1
- package/src/core/useChatListener.ts +2 -4
- package/src/core/useUserListener.ts +0 -8
- package/src/hooks/flows/useFlow.ts +12 -3
- package/src/hooks/message/useSendMessage.ts +1 -1
- package/src/hooks/useConversationList.ts +6 -40
- package/src/hooks/useFileAttachment.ts +163 -0
- package/src/hooks/useFileDownload.ts +200 -0
- package/src/hooks/useImageAttachment.ts +1 -1
- package/src/hooks/useLinkPreview/useLinkPreview.ts +6 -1
- package/src/screens/chat-detail/ChatComposer.tsx +61 -30
- package/src/screens/chat-detail/ChatDetail.tsx +1 -7
- package/src/screens/chat-detail/ChatDetailHeader.tsx +6 -7
- package/src/screens/chat-detail/ChatLinkPreview.tsx +16 -12
- package/src/screens/chat-detail/ChatListLegend.tsx +2 -6
- package/src/screens/chat-detail/conversationHeader.utils.ts +16 -6
- package/src/screens/chat-detail/hooks/useAttachmentMapping.ts +58 -0
- package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +50 -11
- package/src/screens/chat-detail/types.ts +6 -13
- package/src/screens/inbox/Inbox.tsx +5 -1
- package/src/services/apis.ts +0 -38
- package/src/services/attachmentHandlers/fileAttachmentHandler.ts +8 -7
- package/src/services/attachmentHandlers/index.ts +4 -1
- package/src/store/conversation.ts +6 -55
- package/src/translation/resources/i18n.ts +9 -0
- package/src/types/attachment.ts +23 -1
- package/src/types/auth.ts +1 -0
- package/src/types/chat.ts +16 -20
- package/src/utils/conversation.ts +2 -40
- package/src/utils/device.ts +58 -0
- package/src/utils/imageUtils.ts +50 -28
- package/src/utils/message.ts +3 -2
- package/src/utils/messageUtils.ts +11 -3
- package/lib/module/components/AttachmentPreview.js +0 -252
- package/lib/module/components/AttachmentPreview.js.map +0 -1
- package/lib/typescript/src/components/AttachmentPreview.d.ts +0 -28
- package/lib/typescript/src/components/AttachmentPreview.d.ts.map +0 -1
- 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 {
|
|
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
|
-
() =>
|
|
141
|
-
|
|
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 =
|
|
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
|
-
|
|
252
|
+
const handleImageAttachRef = useRef(handleImageAttach);
|
|
253
|
+
handleImageAttachRef.current = handleImageAttach;
|
|
236
254
|
const debouncedImagePickerRef = useRef(
|
|
237
|
-
debounce(
|
|
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={
|
|
302
|
-
|
|
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
|
-
|
|
38
|
-
<
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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={
|
|
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 {
|
|
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?:
|
|
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?:
|
|
15
|
+
conversation?: ConversationItem
|
|
16
16
|
): string | null => {
|
|
17
17
|
return conversation?.faceURL ?? null;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export const getConversationSubtitle = (
|
|
21
|
-
conversation?:
|
|
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?:
|
|
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
|
+
};
|