@droppii-org/chat-mobile 0.2.24 → 0.2.26
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/btn_resend.png +0 -0
- package/lib/module/assets/images/index.js +2 -1
- package/lib/module/assets/images/index.js.map +1 -1
- package/lib/module/components/AttachmentPreview/FilePreview.js +1 -1
- package/lib/module/components/AttachmentPreview/FilePreview.js.map +1 -1
- package/lib/module/components/flows/inputButtons/index.js +3 -6
- package/lib/module/components/flows/inputButtons/index.js.map +1 -1
- package/lib/module/components/messages/MessageStatusIndicator.js +70 -0
- package/lib/module/components/messages/MessageStatusIndicator.js.map +1 -0
- package/lib/module/components/messages/fileMessage/index.js +1 -1
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/components/messages/imageMessage/index.js +60 -51
- package/lib/module/components/messages/imageMessage/index.js.map +1 -1
- package/lib/module/components/messages/linkMessage/index.js +17 -9
- package/lib/module/components/messages/linkMessage/index.js.map +1 -1
- package/lib/module/components/messages/mergedMessage/index.js +5 -8
- package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
- package/lib/module/components/messages/styles.js +12 -0
- package/lib/module/components/messages/styles.js.map +1 -1
- package/lib/module/components/messages/textMessage/index.js +20 -9
- package/lib/module/components/messages/textMessage/index.js.map +1 -1
- package/lib/module/components/messages/videoMessage/index.js +7 -11
- package/lib/module/components/messages/videoMessage/index.js.map +1 -1
- package/lib/module/config/uploadLimits.js +1 -1
- package/lib/module/config/uploadLimits.js.map +1 -1
- package/lib/module/core/useChatListener.js +35 -4
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/core/useUserListener.js +17 -7
- package/lib/module/core/useUserListener.js.map +1 -1
- package/lib/module/hooks/message/useSendMessage.js +62 -15
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/hooks/useChatCompose.js +39 -28
- package/lib/module/hooks/useChatCompose.js.map +1 -1
- package/lib/module/hooks/useChatMessages.js +39 -8
- package/lib/module/hooks/useChatMessages.js.map +1 -1
- package/lib/module/hooks/useFileAttachment.js +1 -1
- package/lib/module/hooks/useFileAttachment.js.map +1 -1
- package/lib/module/hooks/useFileDownload.js +1 -1
- package/lib/module/hooks/useFileDownload.js.map +1 -1
- package/lib/module/hooks/useImageAttachment.js +12 -3
- package/lib/module/hooks/useImageAttachment.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +24 -1
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +40 -28
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/constants.js +3 -2
- package/lib/module/screens/chat-detail/constants.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +48 -15
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +98 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +110 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +64 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +37 -0
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/types.js +4 -0
- package/lib/module/screens/chat-detail/messageToolbox/types.js.map +1 -0
- package/lib/module/services/attachmentOrchestrator.js +34 -12
- package/lib/module/services/attachmentOrchestrator.js.map +1 -1
- package/lib/module/services/message.js +13 -5
- package/lib/module/services/message.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +9 -3
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/types/global.d.js +2 -0
- package/lib/module/types/global.d.js.map +1 -0
- package/lib/module/types/index.d.js +3 -0
- package/lib/module/types/index.d.js.map +1 -0
- package/lib/module/utils/UIUtils.js +37 -0
- package/lib/module/utils/UIUtils.js.map +1 -0
- package/lib/module/utils/ui.js +15 -13
- package/lib/module/utils/ui.js.map +1 -1
- package/lib/module/utils/videoThumbnailUpload.js +1 -1
- package/lib/module/utils/videoThumbnailUpload.js.map +1 -1
- package/lib/typescript/src/assets/images/index.d.ts +1 -0
- package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flows/inputButtons/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts +9 -0
- package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts +3 -8
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/styles.d.ts +15 -3
- package/lib/typescript/src/components/messages/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts +2 -8
- package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/videoMessage/index.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/message/useSendMessage.d.ts +2 -0
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/constants.d.ts +2 -2
- package/lib/typescript/src/screens/chat-detail/constants.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +4 -5
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +11 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +10 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts +11 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +18 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/types.d.ts +2 -0
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
- package/lib/typescript/src/services/message.d.ts +7 -0
- package/lib/typescript/src/services/message.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +11 -0
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/utils/UIUtils.d.ts +4 -0
- package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -0
- package/lib/typescript/src/utils/ui.d.ts +1 -12
- package/lib/typescript/src/utils/ui.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/assets/images/btn_resend.png +0 -0
- package/src/assets/images/index.ts +1 -0
- package/src/components/AttachmentPreview/FilePreview.tsx +1 -1
- package/src/components/flows/inputButtons/index.tsx +3 -5
- package/src/components/messages/MessageStatusIndicator.tsx +82 -0
- package/src/components/messages/fileMessage/index.tsx +0 -2
- package/src/components/messages/imageMessage/index.tsx +72 -58
- package/src/components/messages/linkMessage/index.tsx +16 -21
- package/src/components/messages/mergedMessage/index.tsx +13 -18
- package/src/components/messages/styles.ts +12 -0
- package/src/components/messages/textMessage/index.tsx +20 -20
- package/src/components/messages/videoMessage/index.tsx +8 -11
- package/src/config/uploadLimits.ts +1 -1
- package/src/core/useChatListener.ts +38 -4
- package/src/core/useUserListener.ts +17 -7
- package/src/hooks/message/useSendMessage.ts +103 -24
- package/src/hooks/useChatCompose.ts +42 -33
- package/src/hooks/useChatMessages.ts +54 -10
- package/src/hooks/useFileAttachment.ts +1 -1
- package/src/hooks/useFileDownload.ts +1 -1
- package/src/hooks/useImageAttachment.ts +13 -5
- package/src/index.tsx +2 -0
- package/src/screens/chat-detail/ChatDetail.tsx +29 -0
- package/src/screens/chat-detail/ChatListLegend.tsx +45 -28
- package/src/screens/chat-detail/constants.ts +3 -2
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +60 -15
- package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +114 -0
- package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +130 -0
- package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +99 -0
- package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +56 -0
- package/src/screens/chat-detail/messageToolbox/types.ts +13 -0
- package/src/screens/chat-detail/types.ts +2 -0
- package/src/services/attachmentOrchestrator.ts +45 -21
- package/src/services/message.ts +21 -4
- package/src/translation/resources/i18n.ts +11 -3
- package/src/types/chat.ts +6 -0
- package/src/types/global.d.ts +299 -0
- package/src/types/index.d.ts +1 -0
- package/src/utils/UIUtils.ts +31 -0
- package/src/utils/ui.ts +20 -16
- package/src/utils/videoThumbnailUpload.ts +1 -1
- package/lib/module/hooks/useVideoAttachment.js +0 -251
- package/lib/module/hooks/useVideoAttachment.js.map +0 -1
- package/lib/typescript/src/hooks/useVideoAttachment.d.ts +0 -29
- package/lib/typescript/src/hooks/useVideoAttachment.d.ts.map +0 -1
- package/src/hooks/useVideoAttachment.ts +0 -334
|
@@ -66,11 +66,17 @@ export function useChatCompose() {
|
|
|
66
66
|
(v) => v.uploadedUrl
|
|
67
67
|
) as unknown as VideoWithUploadStatus[];
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// Thumbnails are generated + uploaded at pick time (useImageAttachment).
|
|
70
|
+
// Only fall back to generating here for videos that don't already have one
|
|
71
|
+
// (e.g. thumbnail generation failed or hadn't finished before send).
|
|
72
|
+
const videosMissingThumbnail = uploadedVideos.filter(
|
|
73
|
+
(v) => !v.thumbnailUrl
|
|
74
|
+
);
|
|
70
75
|
let videoThumbnails: Record<string, { url: string; size: number }> = {};
|
|
71
|
-
if (
|
|
72
|
-
videoThumbnails =
|
|
73
|
-
|
|
76
|
+
if (videosMissingThumbnail.length > 0) {
|
|
77
|
+
videoThumbnails = await generateAndUploadVideoThumbnails(
|
|
78
|
+
videosMissingThumbnail
|
|
79
|
+
);
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
// Send images + videos
|
|
@@ -98,51 +104,54 @@ export function useChatCompose() {
|
|
|
98
104
|
error: vid.error,
|
|
99
105
|
isUploading: vid.isUploading,
|
|
100
106
|
uploadedUrl: vid.uploadedUrl,
|
|
101
|
-
thumbnailUrl: thumb?.url,
|
|
102
|
-
thumbnailSize: thumb?.size,
|
|
107
|
+
thumbnailUrl: vid.thumbnailUrl ?? thumb?.url,
|
|
108
|
+
thumbnailSize: vid.thumbnailSize ?? thumb?.size,
|
|
103
109
|
};
|
|
104
110
|
}
|
|
105
111
|
),
|
|
106
112
|
];
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
// Clear composer immediately — message is already in chat list with Sending status.
|
|
115
|
+
// User retries from the bubble, not the composer.
|
|
116
|
+
ImagePicker.clean().catch(() => {});
|
|
117
|
+
clearAllImages();
|
|
118
|
+
// Awaited so files (lower priority per attachment-priority config)
|
|
119
|
+
// only start sending after images/videos are done.
|
|
120
|
+
await sendAttachments(attachmentItems, undefined);
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
// Send each file as its own message
|
|
123
|
+
// Send each file as its own message (never merged), in priority order
|
|
124
|
+
// after images/videos, one at a time to preserve pick order.
|
|
117
125
|
const uploadedFiles = selectedFiles.filter(
|
|
118
126
|
(f) => f.uploadedUrl && !f.error
|
|
119
127
|
);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
if (uploadedFiles.length > 0) {
|
|
129
|
+
clearAllFiles();
|
|
130
|
+
for (const file of uploadedFiles) {
|
|
131
|
+
await sendAttachments(
|
|
132
|
+
[
|
|
133
|
+
{
|
|
134
|
+
type: 'file' as const,
|
|
135
|
+
file: { path: file.uri, filename: file.name, size: file.size },
|
|
136
|
+
uploadProgress: 100,
|
|
137
|
+
error: null,
|
|
138
|
+
isUploading: false,
|
|
139
|
+
uploadedUrl: file.uploadedUrl,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
undefined
|
|
143
|
+
);
|
|
144
|
+
}
|
|
134
145
|
}
|
|
135
|
-
if (uploadedFiles.length > 0) clearAllFiles();
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
// --- Text / link preview ---
|
|
139
149
|
if (text.trim() || urlMeta) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
urlMetadata: urlMeta,
|
|
144
|
-
});
|
|
150
|
+
const plainText = text.trim();
|
|
151
|
+
// Clear composer immediately — message is already in chat list with Sending status.
|
|
152
|
+
// User retries from the bubble, not the composer.
|
|
145
153
|
setText('');
|
|
154
|
+
await sendMessage({ files: [], plainText, urlMetadata: urlMeta });
|
|
146
155
|
}
|
|
147
156
|
}, [
|
|
148
157
|
imageAttachment,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
mergeMessages,
|
|
12
12
|
} from '../utils/message';
|
|
13
13
|
import { useMessageStore } from '../store/message';
|
|
14
|
+
import { useUserStore } from '../store/user';
|
|
15
|
+
import { useChatContext } from '../context';
|
|
14
16
|
|
|
15
17
|
type UseChatMessagesOptions = {
|
|
16
18
|
conversationId: string;
|
|
@@ -30,7 +32,11 @@ export function useChatMessages({
|
|
|
30
32
|
const pushNewMessage = useMessageStore((s) => s.pushNewMessage);
|
|
31
33
|
const reset = useMessageStore((s) => s.reset);
|
|
32
34
|
|
|
33
|
-
const
|
|
35
|
+
const { applicationType } = useChatContext();
|
|
36
|
+
const cachedUserId = useUserStore((s) => s.user?.userID);
|
|
37
|
+
const [currentUserId, setCurrentUserId] = useState<string | undefined>(
|
|
38
|
+
cachedUserId
|
|
39
|
+
);
|
|
34
40
|
const [isLoading, setIsLoading] = useState(false);
|
|
35
41
|
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
|
|
36
42
|
const [isLoadingNewer, setIsLoadingNewer] = useState(false);
|
|
@@ -46,27 +52,60 @@ export function useChatMessages({
|
|
|
46
52
|
setError(null);
|
|
47
53
|
|
|
48
54
|
try {
|
|
49
|
-
|
|
55
|
+
// Fetch userId and history independently so an offline SDK state
|
|
56
|
+
// doesn't block reading local SQLite history.
|
|
57
|
+
const [historyResult, userIdResult] = await Promise.allSettled([
|
|
50
58
|
ChatMessageAPI.fetchHistoryMessages(conversationId, {
|
|
51
59
|
count: pageSize,
|
|
60
|
+
applicationType,
|
|
52
61
|
}),
|
|
53
62
|
ChatMessageAPI.getCurrentUserId(),
|
|
54
63
|
]);
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
setMessages(mergeMessages([], initialMessages), !history.isEnd);
|
|
65
|
+
if (userIdResult.status === 'fulfilled') {
|
|
66
|
+
setCurrentUserId(userIdResult.value);
|
|
67
|
+
}
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
if (historyResult.status === 'fulfilled') {
|
|
70
|
+
const initialMessages = historyResult.value
|
|
71
|
+
.messageList as DMessageItem[];
|
|
72
|
+
setMessages(
|
|
73
|
+
mergeMessages([], initialMessages),
|
|
74
|
+
!historyResult.value.isEnd
|
|
75
|
+
);
|
|
76
|
+
ChatMessageAPI.markConversationAsRead(conversationId).catch(
|
|
77
|
+
() => undefined
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
// Fallback: try searchLocalMessages to verify local SQLite is accessible offline
|
|
81
|
+
console.warn(
|
|
82
|
+
'[useChatMessages] getAdvancedHistoryMessageList failed, trying searchLocalMessages fallback:',
|
|
83
|
+
historyResult.reason
|
|
84
|
+
);
|
|
85
|
+
const localResult = await ChatMessageAPI.searchLocalMessages(
|
|
86
|
+
conversationId,
|
|
87
|
+
{ count: pageSize }
|
|
88
|
+
);
|
|
89
|
+
const localMessages = (localResult?.searchResultItems ?? []).flatMap(
|
|
90
|
+
(item: any) => item.messageList ?? []
|
|
91
|
+
) as DMessageItem[];
|
|
92
|
+
console.log(
|
|
93
|
+
'[useChatMessages] searchLocalMessages fallback result:',
|
|
94
|
+
localMessages.length,
|
|
95
|
+
'messages'
|
|
96
|
+
);
|
|
97
|
+
if (localMessages.length > 0) {
|
|
98
|
+
setMessages(mergeMessages([], localMessages), false);
|
|
99
|
+
} else {
|
|
100
|
+
throw historyResult.reason;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
64
103
|
} catch (err) {
|
|
65
104
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
66
105
|
} finally {
|
|
67
106
|
setIsLoading(false);
|
|
68
107
|
}
|
|
69
|
-
}, [conversationId, pageSize, setMessages]);
|
|
108
|
+
}, [applicationType, conversationId, pageSize, setMessages]);
|
|
70
109
|
|
|
71
110
|
const onLoadEarlier = useCallback(async () => {
|
|
72
111
|
if (isLoadingEarlier || !hasMoreEarlier || !messages.length) return;
|
|
@@ -148,6 +187,11 @@ export function useChatMessages({
|
|
|
148
187
|
|
|
149
188
|
const appendIncomingMessages = useCallback(
|
|
150
189
|
(incoming: MessageItem[]) => {
|
|
190
|
+
console.log('[Listener] OnRecvNewMessages', {
|
|
191
|
+
total: incoming.length,
|
|
192
|
+
conversationId: conversationIdRef.current,
|
|
193
|
+
clientMsgIDs: incoming.map((m) => m.clientMsgID),
|
|
194
|
+
});
|
|
151
195
|
const relevant = incoming.filter((message) =>
|
|
152
196
|
belongsToConversation(message, conversationIdRef.current)
|
|
153
197
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
2
|
import { ImageUploadAPI } from '../services/imageUpload';
|
|
3
3
|
import DeviceUtils from '../utils/device';
|
|
4
|
-
import UIUtils from '../utils/
|
|
4
|
+
import { UIUtils } from '../utils/UIUtils';
|
|
5
5
|
import { trans } from '../translation';
|
|
6
6
|
import { MAX_FILE_SIZE } from '../services/attachmentHandlers';
|
|
7
7
|
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { Platform, Share } from 'react-native';
|
|
3
3
|
import RNFS from 'react-native-fs';
|
|
4
4
|
import FileViewer from 'react-native-file-viewer';
|
|
5
|
-
import UIUtils from '../utils/
|
|
5
|
+
import { UIUtils } from '../utils/UIUtils';
|
|
6
6
|
import { trans } from '../translation';
|
|
7
7
|
|
|
8
8
|
export type DownloadStatus = 'idle' | 'downloading' | 'done' | 'error';
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
Video as PickedVideo,
|
|
5
5
|
} from 'react-native-image-crop-picker';
|
|
6
6
|
import DeviceUtils from '../utils/device';
|
|
7
|
-
import UIUtils from '../utils/
|
|
7
|
+
import { UIUtils } from '../utils/UIUtils';
|
|
8
8
|
import { trans } from '../translation';
|
|
9
9
|
import { getMimeType, isVideoFromPicker } from '../utils/imageUtils';
|
|
10
10
|
import { UPLOAD_CONCURRENCY, UPLOAD_LIMITS } from '../config/uploadLimits';
|
|
@@ -86,9 +86,14 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
|
|
|
86
86
|
} catch (error: unknown) {
|
|
87
87
|
const msg =
|
|
88
88
|
error instanceof Error
|
|
89
|
-
? error
|
|
89
|
+
? error?.message
|
|
90
90
|
: trans('chat:image_upload_failed');
|
|
91
91
|
updateImageStatus(index, { isUploading: false, error: msg });
|
|
92
|
+
UIUtils.toast.open({
|
|
93
|
+
title: trans('chat:image_upload_failed'),
|
|
94
|
+
message: trans('chat:generic_error'),
|
|
95
|
+
theme: 'danger',
|
|
96
|
+
});
|
|
92
97
|
}
|
|
93
98
|
},
|
|
94
99
|
[updateImageStatus]
|
|
@@ -171,11 +176,14 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
|
|
|
171
176
|
|
|
172
177
|
const oversizedItems = images.filter((img) => !isWithinLimit(img));
|
|
173
178
|
if (oversizedItems.length > 0) {
|
|
179
|
+
const isVideo = oversizedItems.some(isVideoFromPicker);
|
|
180
|
+
const maxSize = Math.floor(
|
|
181
|
+
(isVideo ? UPLOAD_LIMITS.video : UPLOAD_LIMITS.image) / 1024 / 1024
|
|
182
|
+
);
|
|
174
183
|
UIUtils.toast.open({
|
|
175
184
|
title: trans(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: 'chat:image_too_large'
|
|
185
|
+
isVideo ? 'chat:video_too_large' : 'chat:image_too_large',
|
|
186
|
+
{ maxSize: String(maxSize) }
|
|
179
187
|
),
|
|
180
188
|
theme: 'danger',
|
|
181
189
|
});
|
package/src/index.tsx
CHANGED
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
import type { ChatDetailProps } from './types';
|
|
24
24
|
import { ChatDetailProvider } from '../../context/ChatDetailContext';
|
|
25
25
|
import { normalizeUrl } from '../../utils/url';
|
|
26
|
+
// import { ChatMessageAPI } from '../../services/message';
|
|
27
|
+
import { MessageToolboxOverlay } from './messageToolbox/MessageToolboxOverlay';
|
|
28
|
+
import type { MeasuredRect, MessageToolboxRef } from './messageToolbox/types';
|
|
29
|
+
import type { DMessageItem } from '../../types/chat';
|
|
26
30
|
|
|
27
31
|
const ChatDetail = memo(
|
|
28
32
|
({
|
|
@@ -44,6 +48,7 @@ const ChatDetail = memo(
|
|
|
44
48
|
}: ChatDetailProps) => {
|
|
45
49
|
const navigation = useNavigation();
|
|
46
50
|
const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
|
|
51
|
+
const messageToolboxRef = useRef<MessageToolboxRef>(null);
|
|
47
52
|
const conversation = useConversation(conversationId);
|
|
48
53
|
|
|
49
54
|
const handleBackDefault = useCallback(() => {
|
|
@@ -73,6 +78,17 @@ const ChatDetail = memo(
|
|
|
73
78
|
useConversationStore.getState().setCurrentConversation(conversationId);
|
|
74
79
|
}, [conversationId]);
|
|
75
80
|
|
|
81
|
+
// useEffect(() => {
|
|
82
|
+
// if (!conversationId) return;
|
|
83
|
+
// ChatMessageAPI.searchLocalMessages(conversationId, { count: 20 })
|
|
84
|
+
// .then((result) => {
|
|
85
|
+
// console.log('[TEST searchLocalMessages] success:', result);
|
|
86
|
+
// })
|
|
87
|
+
// .catch((err) => {
|
|
88
|
+
// console.log('[TEST searchLocalMessages] error:', err);
|
|
89
|
+
// });
|
|
90
|
+
// }, [conversationId]);
|
|
91
|
+
|
|
76
92
|
const resolvedTitle = title ?? getConversationTitle(conversation);
|
|
77
93
|
const resolvedAvatarUri =
|
|
78
94
|
avatarUri ?? getConversationAvatarUri(conversation);
|
|
@@ -99,6 +115,13 @@ const ChatDetail = memo(
|
|
|
99
115
|
[onPressUrl]
|
|
100
116
|
);
|
|
101
117
|
|
|
118
|
+
const handleMessageLongPress = useCallback(
|
|
119
|
+
(message: DMessageItem, rect: MeasuredRect) => {
|
|
120
|
+
messageToolboxRef.current?.show(message, rect);
|
|
121
|
+
},
|
|
122
|
+
[]
|
|
123
|
+
);
|
|
124
|
+
|
|
102
125
|
return (
|
|
103
126
|
<ChatDetailProvider>
|
|
104
127
|
<KContainer.Page flex edges={['bottom']}>
|
|
@@ -131,12 +154,18 @@ const ChatDetail = memo(
|
|
|
131
154
|
hasMoreEarlier={hasMoreEarlier}
|
|
132
155
|
onMediaPress={handleMediaPress}
|
|
133
156
|
onPressUrl={handlePressUrl}
|
|
157
|
+
onLongPress={handleMessageLongPress}
|
|
134
158
|
/>
|
|
135
159
|
|
|
136
160
|
<ChatComposer isEmptyMessage={messages?.length === 0} />
|
|
137
161
|
</KeyboardAvoidingView>
|
|
138
162
|
|
|
139
163
|
<MediaViewerModal ref={mediaViewerRef} />
|
|
164
|
+
<MessageToolboxOverlay
|
|
165
|
+
ref={messageToolboxRef}
|
|
166
|
+
currentUserId={currentUserId}
|
|
167
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
168
|
+
/>
|
|
140
169
|
</KContainer.Page>
|
|
141
170
|
</ChatDetailProvider>
|
|
142
171
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memo, useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { StyleSheet, type View } from 'react-native';
|
|
3
3
|
import Animated, {
|
|
4
4
|
useSharedValue,
|
|
5
5
|
useAnimatedStyle,
|
|
@@ -18,6 +18,8 @@ import { getFlowInputButtonsNode } from '../../utils/flows';
|
|
|
18
18
|
import { InputButtonFlow } from '../../components/flows/inputButtons';
|
|
19
19
|
import { useResetFlow, useRunFlow } from '../../hooks/flows/useFlow';
|
|
20
20
|
import type { IRunConversationFlowPayload } from '../../types/flows';
|
|
21
|
+
import { useSendMessage } from '../../hooks/message/useSendMessage';
|
|
22
|
+
import { messageStyles } from './legend/messageTypes';
|
|
21
23
|
|
|
22
24
|
export const ChatListLegend = memo(
|
|
23
25
|
({
|
|
@@ -25,7 +27,6 @@ export const ChatListLegend = memo(
|
|
|
25
27
|
currentUserId = '',
|
|
26
28
|
customMessageItemFactory,
|
|
27
29
|
onLoadEarlier,
|
|
28
|
-
isLoading,
|
|
29
30
|
isLoadingEarlier,
|
|
30
31
|
hasMoreEarlier,
|
|
31
32
|
onLoadNewer,
|
|
@@ -33,6 +34,7 @@ export const ChatListLegend = memo(
|
|
|
33
34
|
hasMoreNewer,
|
|
34
35
|
onMediaPress,
|
|
35
36
|
onPressUrl,
|
|
37
|
+
onLongPress,
|
|
36
38
|
}: ChatListProps) => {
|
|
37
39
|
// Precompute all message data in single pass (O(n))
|
|
38
40
|
const messageDataMap = useMemo(
|
|
@@ -41,6 +43,7 @@ export const ChatListLegend = memo(
|
|
|
41
43
|
);
|
|
42
44
|
|
|
43
45
|
const { isBotConversation, conversationFlow } = useChatDetailContext();
|
|
46
|
+
const { retryMessage } = useSendMessage();
|
|
44
47
|
|
|
45
48
|
const lastMessageId = useMemo(() => {
|
|
46
49
|
const last = messages[messages.length - 1];
|
|
@@ -59,6 +62,7 @@ export const ChatListLegend = memo(
|
|
|
59
62
|
const itemHeightsRef = useRef<Map<string, number>>(new Map());
|
|
60
63
|
const avgItemHeightRef = useRef(120);
|
|
61
64
|
const heightDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
65
|
+
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
62
66
|
const [showBadge, setShowBadge] = useState(false);
|
|
63
67
|
|
|
64
68
|
const handleItemLayout = useCallback(
|
|
@@ -85,6 +89,16 @@ export const ChatListLegend = memo(
|
|
|
85
89
|
[]
|
|
86
90
|
);
|
|
87
91
|
|
|
92
|
+
const handleMessageLongPress = useCallback(
|
|
93
|
+
(messageId: string, message: DMessageItem) => {
|
|
94
|
+
const node = rowRefsMap.current.get(messageId);
|
|
95
|
+
node?.measureInWindow((x, y, width, height) => {
|
|
96
|
+
onLongPress?.(message, { x, y, width, height });
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
[onLongPress]
|
|
100
|
+
);
|
|
101
|
+
|
|
88
102
|
const onRunFlow = useCallback(
|
|
89
103
|
(payload: IRunConversationFlowPayload) => {
|
|
90
104
|
runFlow(payload);
|
|
@@ -129,10 +143,15 @@ export const ChatListLegend = memo(
|
|
|
129
143
|
<>
|
|
130
144
|
{dayStart && <LegendChatDay createdAt={createdAt} />}
|
|
131
145
|
<KContainer.View
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
ref={(node: View | null) => {
|
|
147
|
+
if (node) rowRefsMap.current.set(messageId, node);
|
|
148
|
+
else rowRefsMap.current.delete(messageId);
|
|
149
|
+
}}
|
|
150
|
+
style={
|
|
151
|
+
isOutgoing
|
|
152
|
+
? messageStyles.messageRowRight
|
|
153
|
+
: messageStyles.messageRowLeft
|
|
154
|
+
}
|
|
136
155
|
onLayout={(e) =>
|
|
137
156
|
handleItemLayout(messageId, e.nativeEvent.layout.height)
|
|
138
157
|
}
|
|
@@ -143,6 +162,12 @@ export const ChatListLegend = memo(
|
|
|
143
162
|
createdAtTime={createdAt}
|
|
144
163
|
onMediaPress={onMediaPress}
|
|
145
164
|
onPressUrl={onPressUrl}
|
|
165
|
+
onRetry={retryMessage}
|
|
166
|
+
onLongPress={
|
|
167
|
+
onLongPress
|
|
168
|
+
? () => handleMessageLongPress(messageId, message)
|
|
169
|
+
: undefined
|
|
170
|
+
}
|
|
146
171
|
customMessageItemFactory={customMessageItemFactory}
|
|
147
172
|
/>
|
|
148
173
|
{!!flowInputButtonsNode && (
|
|
@@ -174,11 +199,14 @@ export const ChatListLegend = memo(
|
|
|
174
199
|
isBotConversation,
|
|
175
200
|
onMediaPress,
|
|
176
201
|
onPressUrl,
|
|
202
|
+
onLongPress,
|
|
203
|
+
handleMessageLongPress,
|
|
177
204
|
customMessageItemFactory,
|
|
178
205
|
onResetFlow,
|
|
179
206
|
onRunFlow,
|
|
180
207
|
conversationFlow?.flowCurrentStep,
|
|
181
208
|
handleItemLayout,
|
|
209
|
+
retryMessage,
|
|
182
210
|
]
|
|
183
211
|
);
|
|
184
212
|
|
|
@@ -327,16 +355,16 @@ export const ChatListLegend = memo(
|
|
|
327
355
|
};
|
|
328
356
|
}, []);
|
|
329
357
|
|
|
330
|
-
if (isLoading) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
358
|
+
// if (isLoading) {
|
|
359
|
+
// return (
|
|
360
|
+
// <KContainer.View flex center background={KColors.white}>
|
|
361
|
+
// <ActivityIndicator
|
|
362
|
+
// color={KColors.palette.primary.w400}
|
|
363
|
+
// size="small"
|
|
364
|
+
// />
|
|
365
|
+
// </KContainer.View>
|
|
366
|
+
// );
|
|
367
|
+
// }
|
|
340
368
|
|
|
341
369
|
return (
|
|
342
370
|
<KContainer.View flex background={KColors.white} style={styles.container}>
|
|
@@ -346,7 +374,7 @@ export const ChatListLegend = memo(
|
|
|
346
374
|
renderItem={renderItem}
|
|
347
375
|
estimatedItemSize={avgItemHeightRef.current}
|
|
348
376
|
keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
|
|
349
|
-
recycleItems={
|
|
377
|
+
recycleItems={true}
|
|
350
378
|
extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`}
|
|
351
379
|
alignItemsAtEnd
|
|
352
380
|
maintainVisibleContentPosition={maintainVisiblePositionConfig}
|
|
@@ -396,17 +424,6 @@ const styles = StyleSheet.create({
|
|
|
396
424
|
container: {
|
|
397
425
|
position: 'relative',
|
|
398
426
|
},
|
|
399
|
-
messageRow: {
|
|
400
|
-
width: '100%',
|
|
401
|
-
paddingHorizontal: KSpacingValue['0.75rem'],
|
|
402
|
-
marginBottom: KSpacingValue['0.25rem'],
|
|
403
|
-
},
|
|
404
|
-
messageRowLeft: {
|
|
405
|
-
alignItems: 'flex-start',
|
|
406
|
-
},
|
|
407
|
-
messageRowRight: {
|
|
408
|
-
alignItems: 'flex-end',
|
|
409
|
-
},
|
|
410
427
|
content: {
|
|
411
428
|
paddingVertical: KSpacingValue['0.75rem'],
|
|
412
429
|
flexGrow: 1,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { KColors } from '@droppii/libs';
|
|
2
2
|
import type { DChatQuickAction, DChatAttachmentAction } from './types';
|
|
3
|
+
import { opacityToSolid } from '../../utils/ui';
|
|
3
4
|
|
|
4
5
|
export const CHAT_BUBBLE_COLORS = {
|
|
5
|
-
received: '
|
|
6
|
-
sent: '
|
|
6
|
+
received: opacityToSolid('#83899d1a', KColors.white, 0.1),
|
|
7
|
+
sent: opacityToSolid('#0051ff1a', KColors.white, 0.1),
|
|
7
8
|
text: KColors.gray.dark,
|
|
8
9
|
timestamp: KColors.gray.light,
|
|
9
10
|
dayLabel: KColors.gray.light,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { KContainer } from '@droppii/libs';
|
|
2
|
+
import { memo, useMemo } from 'react';
|
|
3
|
+
import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
|
3
4
|
import { MessageType } from '@droppii/openim-rn-client-sdk';
|
|
5
|
+
import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
6
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
7
|
+
import { runOnJS } from 'react-native-reanimated';
|
|
4
8
|
import type { DMessageItem } from '../../../types/chat';
|
|
5
9
|
import type { MediaItem } from '../../../hooks/useMediaViewer';
|
|
6
10
|
import { TextMessage } from '../../../components/messages/textMessage';
|
|
@@ -9,10 +13,13 @@ import { VideoMessage } from '../../../components/messages/videoMessage';
|
|
|
9
13
|
import { FileMessage } from '../../../components/messages/fileMessage';
|
|
10
14
|
import { LinkMessage } from '../../../components/messages/linkMessage';
|
|
11
15
|
import { MergedMessage } from '../../../components/messages/mergedMessage';
|
|
16
|
+
import { MessageStatusIndicator } from '../../../components/messages/MessageStatusIndicator';
|
|
12
17
|
import type { BaseLegendMessageProps } from '../../../components/messages/types';
|
|
18
|
+
import { messageStyles } from './messageTypes';
|
|
13
19
|
|
|
14
20
|
type MessageProps = BaseLegendMessageProps & {
|
|
15
21
|
onPressUrl?: (url: string) => void;
|
|
22
|
+
onLongPress?: () => void;
|
|
16
23
|
};
|
|
17
24
|
|
|
18
25
|
interface LegendChatMessageProps {
|
|
@@ -21,6 +28,8 @@ interface LegendChatMessageProps {
|
|
|
21
28
|
createdAtTime: number;
|
|
22
29
|
onMediaPress?: (items: MediaItem[], index?: number) => void;
|
|
23
30
|
onPressUrl?: (url: string) => void;
|
|
31
|
+
onRetry?: (message: MessageItem) => void;
|
|
32
|
+
onLongPress?: () => void;
|
|
24
33
|
customMessageItemFactory?: (m: DMessageItem) => ReactNode;
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -46,10 +55,6 @@ const MEDIA_TYPES: number[] = [
|
|
|
46
55
|
MERGED_MESSAGE_TYPE,
|
|
47
56
|
];
|
|
48
57
|
|
|
49
|
-
/**
|
|
50
|
-
* Dispatcher component - renders the appropriate message component
|
|
51
|
-
* based on message type. Extensible for new message types.
|
|
52
|
-
*/
|
|
53
58
|
export const LegendChatMessage = memo(
|
|
54
59
|
({
|
|
55
60
|
message,
|
|
@@ -57,12 +62,18 @@ export const LegendChatMessage = memo(
|
|
|
57
62
|
createdAtTime,
|
|
58
63
|
onMediaPress,
|
|
59
64
|
onPressUrl,
|
|
65
|
+
onRetry,
|
|
66
|
+
onLongPress,
|
|
60
67
|
customMessageItemFactory,
|
|
61
68
|
}: LegendChatMessageProps) => {
|
|
62
|
-
console.log('customMessageItemFactory', customMessageItemFactory);
|
|
63
69
|
const customNode = customMessageItemFactory?.(message);
|
|
70
|
+
|
|
64
71
|
if (customNode) {
|
|
65
|
-
return
|
|
72
|
+
return (
|
|
73
|
+
<LongPressWrapper onLongPress={onLongPress}>
|
|
74
|
+
{customNode}
|
|
75
|
+
</LongPressWrapper>
|
|
76
|
+
);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
const MessageComponent: ComponentType<MessageProps> =
|
|
@@ -74,15 +85,49 @@ export const LegendChatMessage = memo(
|
|
|
74
85
|
onPressUrl && message?.contentType === MessageType.UrlTextMessage;
|
|
75
86
|
|
|
76
87
|
return (
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
<KContainer.View style={messageStyles.wrapper} row alignItems>
|
|
89
|
+
<MessageStatusIndicator
|
|
90
|
+
message={message}
|
|
91
|
+
isOutgoing={isOutgoing}
|
|
92
|
+
onRetry={onRetry}
|
|
93
|
+
/>
|
|
94
|
+
<LongPressWrapper onLongPress={onLongPress}>
|
|
95
|
+
<MessageComponent
|
|
96
|
+
message={message}
|
|
97
|
+
isOutgoing={isOutgoing}
|
|
98
|
+
createdAtTime={createdAtTime}
|
|
99
|
+
onMediaPress={hasMediaPress ? onMediaPress : undefined}
|
|
100
|
+
onPressUrl={hasUrlPress ? onPressUrl : undefined}
|
|
101
|
+
/>
|
|
102
|
+
</LongPressWrapper>
|
|
103
|
+
</KContainer.View>
|
|
84
104
|
);
|
|
85
105
|
}
|
|
86
106
|
);
|
|
87
107
|
|
|
88
108
|
LegendChatMessage.displayName = 'LegendChatMessage';
|
|
109
|
+
|
|
110
|
+
const LongPressWrapper = ({
|
|
111
|
+
children,
|
|
112
|
+
onLongPress,
|
|
113
|
+
}: PropsWithChildren<{ onLongPress?: () => void }>) => {
|
|
114
|
+
const longPressGesture = useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
onLongPress
|
|
117
|
+
? Gesture.LongPress().onStart(() => {
|
|
118
|
+
runOnJS(onLongPress)();
|
|
119
|
+
})
|
|
120
|
+
: null,
|
|
121
|
+
[onLongPress]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!longPressGesture) {
|
|
125
|
+
return children;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<GestureDetector gesture={longPressGesture}>
|
|
130
|
+
<KContainer.View>{children}</KContainer.View>
|
|
131
|
+
</GestureDetector>
|
|
132
|
+
);
|
|
133
|
+
};
|