@droppii-org/chat-mobile 0.2.34 → 0.2.37
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/hooks/useFileAttachment.js +3 -1
- package/lib/module/hooks/useFileAttachment.js.map +1 -1
- package/lib/module/hooks/useFileDownload.js +19 -17
- package/lib/module/hooks/useFileDownload.js.map +1 -1
- package/lib/module/hooks/useMuteConversation.js +4 -6
- package/lib/module/hooks/useMuteConversation.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +10 -1
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessageBar.js +8 -3
- package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +173 -0
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +9 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
- package/lib/module/store/conversation.js +11 -0
- package/lib/module/store/conversation.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +4 -1
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/utils/UIUtils.js +6 -0
- package/lib/module/utils/UIUtils.js.map +1 -1
- package/lib/typescript/src/hooks/useFileAttachment.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useFileDownload.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/PinnedMessageBar.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts +10 -0
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +9 -7
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
- package/lib/typescript/src/store/conversation.d.ts +1 -0
- 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/chat.d.ts +3 -0
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useFileAttachment.ts +3 -1
- package/src/hooks/useFileDownload.ts +19 -18
- package/src/hooks/useMuteConversation.ts +6 -6
- package/src/screens/chat-detail/ChatDetail.tsx +10 -0
- package/src/screens/chat-detail/PinnedMessageBar.tsx +9 -3
- package/src/screens/chat-detail/PinnedMessagesSheet.tsx +244 -0
- package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +18 -6
- package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +1 -0
- package/src/store/conversation.ts +12 -0
- package/src/translation/resources/i18n.ts +6 -1
- package/src/types/chat.ts +1 -0
- package/src/utils/UIUtils.ts +5 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
memo,
|
|
4
|
+
useCallback,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
import { Modal, Pressable, StyleSheet, type View } from 'react-native';
|
|
11
|
+
import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
12
|
+
import { KContainer, KLabel, KColors } from '@droppii/libs';
|
|
13
|
+
import { UIUtils } from '../../utils/UIUtils';
|
|
14
|
+
import { trans } from '../../translation';
|
|
15
|
+
import { usePinnedMessageStore } from '../../store/pinnedMessage';
|
|
16
|
+
import { LegendChatMessage } from './legend/LegendChatMessage';
|
|
17
|
+
import { formatMessageTime, messageStyles } from './legend/messageTypes';
|
|
18
|
+
import { MessageToolboxWrapper } from './messageToolbox/MessageToolboxWrapper';
|
|
19
|
+
import { MessageToolboxMenu } from './messageToolbox/MessageToolboxMenu';
|
|
20
|
+
import type { MeasuredRect } from './messageToolbox/types';
|
|
21
|
+
import type { DMessageItem } from '../../types/chat';
|
|
22
|
+
|
|
23
|
+
interface PinnedMessageToolboxRef {
|
|
24
|
+
show: (message: DMessageItem, rect: MeasuredRect) => void;
|
|
25
|
+
hide: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PinnedMessageToolboxOverlayProps {
|
|
29
|
+
currentUserId?: string;
|
|
30
|
+
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
31
|
+
customMessageItemFactory?: (item: DMessageItem) => ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Mirrors MessageToolboxOverlay (used for the main chat list's long-press
|
|
35
|
+
// menu) but restricted to the 2 actions relevant to an already-pinned
|
|
36
|
+
// message: unpin and jump to the original message.
|
|
37
|
+
const PinnedMessageToolboxOverlay = memo(
|
|
38
|
+
forwardRef<PinnedMessageToolboxRef, PinnedMessageToolboxOverlayProps>(
|
|
39
|
+
({ currentUserId, unpinMessage, customMessageItemFactory }, ref) => {
|
|
40
|
+
const [visible, setVisible] = useState(false);
|
|
41
|
+
const [message, setMessage] = useState<DMessageItem | undefined>(
|
|
42
|
+
undefined
|
|
43
|
+
);
|
|
44
|
+
const [rect, setRect] = useState<MeasuredRect | null>(null);
|
|
45
|
+
|
|
46
|
+
const hide = useCallback(() => {
|
|
47
|
+
setVisible(false);
|
|
48
|
+
setMessage(undefined);
|
|
49
|
+
setRect(null);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const show = useCallback((msg: DMessageItem, msgRect: MeasuredRect) => {
|
|
53
|
+
setMessage(msg);
|
|
54
|
+
setRect(msgRect);
|
|
55
|
+
setVisible(true);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
useImperativeHandle(ref, () => ({ show, hide }), [show, hide]);
|
|
59
|
+
|
|
60
|
+
const handleUnpinMessage = useCallback(() => {
|
|
61
|
+
if (!message) return;
|
|
62
|
+
hide();
|
|
63
|
+
unpinMessage(message);
|
|
64
|
+
}, [message, hide, unpinMessage]);
|
|
65
|
+
|
|
66
|
+
const handleViewOriginalMessage = useCallback(() => {
|
|
67
|
+
if (!message) return;
|
|
68
|
+
hide();
|
|
69
|
+
// TODO: implement navigating to / highlighting the original message
|
|
70
|
+
}, [message, hide]);
|
|
71
|
+
|
|
72
|
+
if (!visible || !message || !rect) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isOutgoing = message.sendID === currentUserId;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Modal
|
|
80
|
+
visible={visible}
|
|
81
|
+
transparent
|
|
82
|
+
statusBarTranslucent
|
|
83
|
+
animationType="fade"
|
|
84
|
+
onRequestClose={hide}
|
|
85
|
+
>
|
|
86
|
+
<Pressable style={styles.backdrop} onPress={hide} />
|
|
87
|
+
|
|
88
|
+
<MessageToolboxWrapper
|
|
89
|
+
rect={rect}
|
|
90
|
+
isOutgoing={isOutgoing}
|
|
91
|
+
message={
|
|
92
|
+
<LegendChatMessage
|
|
93
|
+
message={message}
|
|
94
|
+
isOutgoing={isOutgoing}
|
|
95
|
+
createdAtTime={message.sendTime || message.createTime}
|
|
96
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
97
|
+
/>
|
|
98
|
+
}
|
|
99
|
+
toolbox={
|
|
100
|
+
<MessageToolboxMenu
|
|
101
|
+
showUnpinMessage
|
|
102
|
+
showViewOriginalMessage
|
|
103
|
+
onUnpinMessage={handleUnpinMessage}
|
|
104
|
+
onViewOriginalMessage={handleViewOriginalMessage}
|
|
105
|
+
/>
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
</Modal>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
PinnedMessageToolboxOverlay.displayName = 'PinnedMessageToolboxOverlay';
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
backdrop: {
|
|
118
|
+
position: 'absolute',
|
|
119
|
+
top: 0,
|
|
120
|
+
left: 0,
|
|
121
|
+
right: 0,
|
|
122
|
+
bottom: 0,
|
|
123
|
+
backgroundColor: 'rgba(0,0,0,0.75)',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
interface PinnedMessagesSheetContentProps {
|
|
128
|
+
currentUserId?: string;
|
|
129
|
+
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
130
|
+
customMessageItemFactory?: (item: DMessageItem) => ReactNode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const PinnedMessagesSheetContent = memo(
|
|
134
|
+
({
|
|
135
|
+
currentUserId,
|
|
136
|
+
unpinMessage,
|
|
137
|
+
customMessageItemFactory,
|
|
138
|
+
}: PinnedMessagesSheetContentProps) => {
|
|
139
|
+
const messages = usePinnedMessageStore((state) => state.messages);
|
|
140
|
+
const toolboxRef = useRef<PinnedMessageToolboxRef>(null);
|
|
141
|
+
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
142
|
+
|
|
143
|
+
const handleLongPress = useCallback(
|
|
144
|
+
(messageId: string, message: DMessageItem) => {
|
|
145
|
+
const node = rowRefsMap.current.get(messageId);
|
|
146
|
+
node?.measureInWindow((x, y, width, height) => {
|
|
147
|
+
toolboxRef.current?.show(message, { x, y, width, height });
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
[]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<KContainer.View paddingV="0.5rem">
|
|
155
|
+
<KLabel.Text
|
|
156
|
+
typo="TextNmNormal"
|
|
157
|
+
color={KColors.gray.normal}
|
|
158
|
+
paddingH="0.75rem"
|
|
159
|
+
marginB="0.75rem"
|
|
160
|
+
>
|
|
161
|
+
{trans('chat:pinned_messages_hint')}
|
|
162
|
+
</KLabel.Text>
|
|
163
|
+
|
|
164
|
+
{messages.map((message) => {
|
|
165
|
+
const isOutgoing = message.sendID === currentUserId;
|
|
166
|
+
const messageId = message.clientMsgID || message.serverMsgID;
|
|
167
|
+
const createdAtTime = message.sendTime || message.createTime;
|
|
168
|
+
const timeLabel = formatMessageTime(createdAtTime);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<KContainer.View
|
|
172
|
+
key={messageId}
|
|
173
|
+
ref={(node: View | null) => {
|
|
174
|
+
if (node) rowRefsMap.current.set(messageId, node);
|
|
175
|
+
else rowRefsMap.current.delete(messageId);
|
|
176
|
+
}}
|
|
177
|
+
style={
|
|
178
|
+
isOutgoing
|
|
179
|
+
? messageStyles.messageRowRight
|
|
180
|
+
: messageStyles.messageRowLeft
|
|
181
|
+
}
|
|
182
|
+
>
|
|
183
|
+
<LegendChatMessage
|
|
184
|
+
message={message}
|
|
185
|
+
isOutgoing={isOutgoing}
|
|
186
|
+
createdAtTime={createdAtTime}
|
|
187
|
+
onLongPress={() => handleLongPress(messageId, message)}
|
|
188
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
189
|
+
/>
|
|
190
|
+
{!!timeLabel && (
|
|
191
|
+
<KLabel.Text
|
|
192
|
+
typo="TextXsNormal"
|
|
193
|
+
color={KColors.gray.light}
|
|
194
|
+
marginT="0.25rem"
|
|
195
|
+
>
|
|
196
|
+
{timeLabel}
|
|
197
|
+
</KLabel.Text>
|
|
198
|
+
)}
|
|
199
|
+
</KContainer.View>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
|
|
203
|
+
<PinnedMessageToolboxOverlay
|
|
204
|
+
ref={toolboxRef}
|
|
205
|
+
currentUserId={currentUserId}
|
|
206
|
+
unpinMessage={unpinMessage}
|
|
207
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
208
|
+
/>
|
|
209
|
+
</KContainer.View>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
PinnedMessagesSheetContent.displayName = 'PinnedMessagesSheetContent';
|
|
215
|
+
|
|
216
|
+
export interface OpenPinnedMessagesSheetParams {
|
|
217
|
+
currentUserId?: string;
|
|
218
|
+
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
219
|
+
customMessageItemFactory?: (item: DMessageItem) => ReactNode;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const openPinnedMessagesSheet = ({
|
|
223
|
+
currentUserId,
|
|
224
|
+
unpinMessage,
|
|
225
|
+
customMessageItemFactory,
|
|
226
|
+
}: OpenPinnedMessagesSheetParams) => {
|
|
227
|
+
UIUtils.bottomSheet.open({
|
|
228
|
+
header: {
|
|
229
|
+
title: { text: trans('chat:pinned_messages_title') },
|
|
230
|
+
alignment: 'left',
|
|
231
|
+
showCloseButton: true,
|
|
232
|
+
},
|
|
233
|
+
touchOutsideToDismiss: true,
|
|
234
|
+
body: {
|
|
235
|
+
renderContent: () => (
|
|
236
|
+
<PinnedMessagesSheetContent
|
|
237
|
+
currentUserId={currentUserId}
|
|
238
|
+
unpinMessage={unpinMessage}
|
|
239
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
240
|
+
/>
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
};
|
|
@@ -11,12 +11,14 @@ interface MessageToolboxMenuProps {
|
|
|
11
11
|
showUnpinMessage?: boolean;
|
|
12
12
|
showRetryMessage?: boolean;
|
|
13
13
|
showRevokeMessage?: boolean;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
showViewOriginalMessage?: boolean;
|
|
15
|
+
onCopyMessage?: () => void;
|
|
16
|
+
onCopyMessageLink?: () => void;
|
|
17
|
+
onPinMessage?: () => void;
|
|
18
|
+
onUnpinMessage?: () => void;
|
|
19
|
+
onRetryMessage?: () => void;
|
|
20
|
+
onRevokeMessage?: () => void;
|
|
21
|
+
onViewOriginalMessage?: () => void;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export const MessageToolboxMenu = memo(
|
|
@@ -27,12 +29,14 @@ export const MessageToolboxMenu = memo(
|
|
|
27
29
|
showUnpinMessage,
|
|
28
30
|
showRetryMessage,
|
|
29
31
|
showRevokeMessage,
|
|
32
|
+
showViewOriginalMessage,
|
|
30
33
|
onCopyMessage,
|
|
31
34
|
onCopyMessageLink,
|
|
32
35
|
onPinMessage,
|
|
33
36
|
onUnpinMessage,
|
|
34
37
|
onRetryMessage,
|
|
35
38
|
onRevokeMessage,
|
|
39
|
+
onViewOriginalMessage,
|
|
36
40
|
}: MessageToolboxMenuProps) => {
|
|
37
41
|
return (
|
|
38
42
|
<KContainer.View style={styles.card}>
|
|
@@ -68,6 +72,14 @@ export const MessageToolboxMenu = memo(
|
|
|
68
72
|
onPress={onUnpinMessage}
|
|
69
73
|
/>
|
|
70
74
|
|
|
75
|
+
<MessageToolboxItem
|
|
76
|
+
icon="chat-square-o"
|
|
77
|
+
color={KColors.gray.dark}
|
|
78
|
+
label={trans('chat:view_original_message')}
|
|
79
|
+
visible={showViewOriginalMessage}
|
|
80
|
+
onPress={onViewOriginalMessage}
|
|
81
|
+
/>
|
|
82
|
+
|
|
71
83
|
<MessageToolboxItem
|
|
72
84
|
icon="arrow-reload-o"
|
|
73
85
|
color={KColors.danger.normal}
|
|
@@ -12,6 +12,7 @@ export interface ConversationStore {
|
|
|
12
12
|
map: Record<ConversationID, ConversationItem>;
|
|
13
13
|
/** Signal incremented when a truly new conversation arrives and a full refetch is needed. */
|
|
14
14
|
newConversationSignal: number;
|
|
15
|
+
updateConversation(id: ConversationID, data: Partial<ConversationItem>): void;
|
|
15
16
|
updateConversations(conversations: ConversationItem[]): void;
|
|
16
17
|
/**
|
|
17
18
|
* Patches existing conversations with real-time data from OpenIM events.
|
|
@@ -34,6 +35,17 @@ export const useConversationStore = create<ConversationStore>()((set, get) => ({
|
|
|
34
35
|
|
|
35
36
|
return get().map[id];
|
|
36
37
|
},
|
|
38
|
+
updateConversation(id, data) {
|
|
39
|
+
const existing = get().map[id];
|
|
40
|
+
if (!existing) return;
|
|
41
|
+
|
|
42
|
+
set(
|
|
43
|
+
sortConversation({
|
|
44
|
+
...get().map,
|
|
45
|
+
[id]: { ...existing, ...data },
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
},
|
|
37
49
|
updateConversations(conversations) {
|
|
38
50
|
set(
|
|
39
51
|
sortConversation(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const i18nKey: Record<string, string> = {
|
|
2
2
|
'thread_card_bot_crm_name': 'Droppii Hỗ Trợ',
|
|
3
3
|
'thread_card_fallback_name': 'Người dùng',
|
|
4
|
-
'chat:file_too_large':
|
|
4
|
+
'chat:file_too_large':
|
|
5
|
+
'Tệp tải lên không hợp lệ. Kích thước tệp không được vượt quá {{maxSize}}MB',
|
|
5
6
|
'chat:file_max_count': 'Chỉ có thể đính kèm tối đa 3 tệp',
|
|
6
7
|
'chat:file_pick_failed': 'Không thể chọn tệp',
|
|
7
8
|
'chat:file_upload_failed': 'Lỗi tải tệp lên',
|
|
@@ -47,6 +48,10 @@ const i18nKey: Record<string, string> = {
|
|
|
47
48
|
'chat:pin_limit_desc':
|
|
48
49
|
'Chỉ được ghim tối đa 5 tin nhắn. Vui lòng bỏ ghim một tin nhắn hiện có trước khi ghim tin nhắn mới.',
|
|
49
50
|
'chat:pin_limit_primary': 'Đã hiểu',
|
|
51
|
+
'chat:pinned_messages_title': 'Tin nhắn đã ghim',
|
|
52
|
+
'chat:pinned_messages_hint':
|
|
53
|
+
'Nhấn giữ tin nhắn để chọn "Bỏ Ghim" hoặc "Xem tin nhắn gốc"',
|
|
54
|
+
'chat:view_original_message': 'Xem tin nhắn gốc',
|
|
50
55
|
'msg_type_image': '[Hình ảnh]',
|
|
51
56
|
'msg_type_voice': '[Tin nhắn thoại]',
|
|
52
57
|
'msg_type_video': '[Video]',
|
package/src/types/chat.ts
CHANGED
package/src/utils/UIUtils.ts
CHANGED
|
@@ -10,6 +10,7 @@ const defaultAdapter: Required<ChatUIAdapter> = {
|
|
|
10
10
|
toast: { open: openWithAlert },
|
|
11
11
|
alert: { open: openWithAlert },
|
|
12
12
|
popup: { open: openWithAlert },
|
|
13
|
+
bottomSheet: { open: openWithAlert },
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
let _adapter: ChatUIAdapter = {};
|
|
@@ -28,4 +29,8 @@ export const UIUtils: Required<ChatUIAdapter> = {
|
|
|
28
29
|
popup: {
|
|
29
30
|
open: (params) => (_adapter.popup ?? defaultAdapter.popup).open(params),
|
|
30
31
|
},
|
|
32
|
+
bottomSheet: {
|
|
33
|
+
open: (params) =>
|
|
34
|
+
(_adapter.bottomSheet ?? defaultAdapter.bottomSheet).open(params),
|
|
35
|
+
},
|
|
31
36
|
};
|