@droppii-org/chat-mobile 0.2.32 → 0.2.33
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/core/useChatListener.js +16 -1
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/message/usePinMessage.js +133 -0
- package/lib/module/hooks/message/usePinMessage.js.map +1 -0
- package/lib/module/screens/chat-detail/ChatDetail.js +13 -1
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessageBar.js +158 -0
- package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -10
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +16 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +15 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +16 -1
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -1
- package/lib/module/store/pinnedMessage.js +34 -0
- package/lib/module/store/pinnedMessage.js.map +1 -0
- package/lib/module/translation/resources/i18n.js +12 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/utils/messageUtils.js +76 -0
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/usePinMessage.d.ts +9 -0
- package/lib/typescript/src/hooks/message/usePinMessage.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts +8 -0
- package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -0
- 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 +5 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +2 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +4 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -1
- package/lib/typescript/src/store/pinnedMessage.d.ts +11 -0
- package/lib/typescript/src/store/pinnedMessage.d.ts.map +1 -0
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +4 -1
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/utils/messageUtils.d.ts +8 -0
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/core/useChatListener.ts +26 -0
- package/src/hooks/message/usePinMessage.ts +153 -0
- package/src/screens/chat-detail/ChatDetail.tsx +12 -0
- package/src/screens/chat-detail/PinnedMessageBar.tsx +177 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +47 -10
- package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +24 -0
- package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +142 -116
- package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +31 -0
- package/src/store/pinnedMessage.ts +38 -0
- package/src/translation/resources/i18n.ts +13 -0
- package/src/types/chat.ts +4 -1
- package/src/utils/messageUtils.ts +90 -0
|
@@ -24,134 +24,160 @@ import { useSendMessage } from '../../../hooks/message/useSendMessage';
|
|
|
24
24
|
interface MessageToolboxOverlayProps {
|
|
25
25
|
currentUserId?: string;
|
|
26
26
|
customMessageItemFactory?: (item: DMessageItem) => ReactNode;
|
|
27
|
+
onPinMessage?: (message: DMessageItem) => void;
|
|
28
|
+
onUnpinMessage?: (message: DMessageItem) => void;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export const MessageToolboxOverlay = forwardRef<
|
|
30
32
|
MessageToolboxRef,
|
|
31
33
|
MessageToolboxOverlayProps
|
|
32
|
-
>(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
const { retryMessage, revokeMessage } = useSendMessage();
|
|
41
|
-
|
|
42
|
-
const hide = useCallback(() => {
|
|
43
|
-
setVisible(false);
|
|
44
|
-
setMessage(undefined);
|
|
45
|
-
setRect(null);
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
const show = useCallback(
|
|
49
|
-
(msg: DMessageItem, msgRect: MeasuredRect) => {
|
|
50
|
-
const toolboxNotAvailable =
|
|
51
|
-
Object.values(getMessageToolboxItems(msg, currentUserId)).filter(
|
|
52
|
-
Boolean
|
|
53
|
-
).length === 0;
|
|
54
|
-
|
|
55
|
-
if (toolboxNotAvailable) return;
|
|
56
|
-
|
|
57
|
-
setMessage(msg);
|
|
58
|
-
setRect(msgRect);
|
|
59
|
-
setVisible(true);
|
|
34
|
+
>(
|
|
35
|
+
(
|
|
36
|
+
{
|
|
37
|
+
currentUserId = '',
|
|
38
|
+
customMessageItemFactory,
|
|
39
|
+
onPinMessage,
|
|
40
|
+
onUnpinMessage,
|
|
60
41
|
},
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
42
|
+
ref
|
|
43
|
+
) => {
|
|
44
|
+
const [visible, setVisible] = useState(false);
|
|
45
|
+
const [message, setMessage] = useState<DMessageItem | undefined>(undefined);
|
|
46
|
+
const [rect, setRect] = useState<MeasuredRect | null>(null);
|
|
47
|
+
const toolboxItems = useMemo(
|
|
48
|
+
() => getMessageToolboxItems(message, currentUserId),
|
|
49
|
+
[message, currentUserId]
|
|
50
|
+
);
|
|
51
|
+
const { retryMessage, revokeMessage } = useSendMessage();
|
|
52
|
+
|
|
53
|
+
const hide = useCallback(() => {
|
|
54
|
+
setVisible(false);
|
|
55
|
+
setMessage(undefined);
|
|
56
|
+
setRect(null);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const show = useCallback(
|
|
60
|
+
(msg: DMessageItem, msgRect: MeasuredRect) => {
|
|
61
|
+
const toolboxNotAvailable =
|
|
62
|
+
Object.values(getMessageToolboxItems(msg, currentUserId)).filter(
|
|
63
|
+
Boolean
|
|
64
|
+
).length === 0;
|
|
65
|
+
|
|
66
|
+
if (toolboxNotAvailable) return;
|
|
67
|
+
|
|
68
|
+
setMessage(msg);
|
|
69
|
+
setRect(msgRect);
|
|
70
|
+
setVisible(true);
|
|
71
|
+
},
|
|
72
|
+
[currentUserId]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
useImperativeHandle(ref, () => ({ show, hide }), [show, hide]);
|
|
76
|
+
|
|
77
|
+
const handleCopyMessage = useCallback(() => {
|
|
78
|
+
if (!message) return;
|
|
79
|
+
const text = getCopyableMessageText(message);
|
|
80
|
+
if (text) {
|
|
81
|
+
Clipboard.setString(text);
|
|
82
|
+
UIUtils.toast.open({
|
|
83
|
+
title: trans('chat:copy_message_success'),
|
|
84
|
+
theme: 'success',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
hide();
|
|
88
|
+
}, [message, hide]);
|
|
89
|
+
|
|
90
|
+
const handleCopyMessageLink = useCallback(() => {
|
|
91
|
+
if (!message) return;
|
|
92
|
+
// UI-only for now: real shareable deeplink format (BR-001) is not decided yet.
|
|
93
|
+
const placeholderLink = `${message.recvID || message.groupID}:${message.clientMsgID ?? message.serverMsgID ?? ''}`;
|
|
94
|
+
Clipboard.setString(placeholderLink);
|
|
102
95
|
UIUtils.toast.open({
|
|
103
|
-
title: trans('
|
|
96
|
+
title: trans('chat:copy_message_link_success'),
|
|
104
97
|
theme: 'success',
|
|
105
98
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
99
|
+
hide();
|
|
100
|
+
}, [message, hide]);
|
|
101
|
+
|
|
102
|
+
const handleRetryMessage = useCallback(() => {
|
|
103
|
+
if (!message) return;
|
|
104
|
+
retryMessage(message);
|
|
105
|
+
hide();
|
|
106
|
+
}, [message, hide, retryMessage]);
|
|
107
|
+
|
|
108
|
+
const handleRevokeMessage = useCallback(async () => {
|
|
109
|
+
if (!message) return;
|
|
110
|
+
hide();
|
|
111
|
+
try {
|
|
112
|
+
await revokeMessage(message);
|
|
113
|
+
UIUtils.toast.open({
|
|
114
|
+
title: trans('msg_type_revoke'),
|
|
115
|
+
theme: 'success',
|
|
116
|
+
});
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
console.error('revokeMessage', error);
|
|
119
|
+
UIUtils.toast.open({
|
|
120
|
+
title: trans('chat:generic_error'),
|
|
121
|
+
theme: 'error',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}, [message, hide, revokeMessage]);
|
|
125
|
+
|
|
126
|
+
const handlePinMessage = useCallback(() => {
|
|
127
|
+
if (!message) return;
|
|
128
|
+
hide();
|
|
129
|
+
onPinMessage?.(message);
|
|
130
|
+
}, [message, hide, onPinMessage]);
|
|
131
|
+
|
|
132
|
+
const handleUnpinMessage = useCallback(() => {
|
|
133
|
+
if (!message) return;
|
|
134
|
+
hide();
|
|
135
|
+
onUnpinMessage?.(message);
|
|
136
|
+
}, [message, hide, onUnpinMessage]);
|
|
137
|
+
|
|
138
|
+
if (!visible || !message || !rect) {
|
|
139
|
+
return null;
|
|
112
140
|
}
|
|
113
|
-
}, [message, hide, revokeMessage]);
|
|
114
141
|
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
const isOutgoing = message.sendID === currentUserId;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Modal
|
|
146
|
+
visible={visible}
|
|
147
|
+
transparent
|
|
148
|
+
statusBarTranslucent
|
|
149
|
+
animationType="fade"
|
|
150
|
+
onRequestClose={hide}
|
|
151
|
+
>
|
|
152
|
+
<Pressable style={styles.backdrop} onPress={hide} />
|
|
153
|
+
|
|
154
|
+
<MessageToolboxWrapper
|
|
155
|
+
rect={rect}
|
|
156
|
+
isOutgoing={isOutgoing}
|
|
157
|
+
message={
|
|
158
|
+
<LegendChatMessage
|
|
159
|
+
message={message}
|
|
160
|
+
isOutgoing={isOutgoing}
|
|
161
|
+
createdAtTime={message.sendTime || message.createTime}
|
|
162
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
163
|
+
/>
|
|
164
|
+
}
|
|
165
|
+
toolbox={
|
|
166
|
+
<MessageToolboxMenu
|
|
167
|
+
{...toolboxItems}
|
|
168
|
+
onCopyMessage={handleCopyMessage}
|
|
169
|
+
onCopyMessageLink={handleCopyMessageLink}
|
|
170
|
+
onPinMessage={handlePinMessage}
|
|
171
|
+
onUnpinMessage={handleUnpinMessage}
|
|
172
|
+
onRetryMessage={handleRetryMessage}
|
|
173
|
+
onRevokeMessage={handleRevokeMessage}
|
|
174
|
+
/>
|
|
175
|
+
}
|
|
176
|
+
/>
|
|
177
|
+
</Modal>
|
|
178
|
+
);
|
|
117
179
|
}
|
|
118
|
-
|
|
119
|
-
const isOutgoing = message.sendID === currentUserId;
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<Modal
|
|
123
|
-
visible={visible}
|
|
124
|
-
transparent
|
|
125
|
-
statusBarTranslucent
|
|
126
|
-
animationType="fade"
|
|
127
|
-
onRequestClose={hide}
|
|
128
|
-
>
|
|
129
|
-
<Pressable style={styles.backdrop} onPress={hide} />
|
|
130
|
-
|
|
131
|
-
<MessageToolboxWrapper
|
|
132
|
-
rect={rect}
|
|
133
|
-
isOutgoing={isOutgoing}
|
|
134
|
-
message={
|
|
135
|
-
<LegendChatMessage
|
|
136
|
-
message={message}
|
|
137
|
-
isOutgoing={isOutgoing}
|
|
138
|
-
createdAtTime={message.sendTime || message.createTime}
|
|
139
|
-
customMessageItemFactory={customMessageItemFactory}
|
|
140
|
-
/>
|
|
141
|
-
}
|
|
142
|
-
toolbox={
|
|
143
|
-
<MessageToolboxMenu
|
|
144
|
-
{...toolboxItems}
|
|
145
|
-
onCopyMessage={handleCopyMessage}
|
|
146
|
-
onCopyMessageLink={handleCopyMessageLink}
|
|
147
|
-
onRetryMessage={handleRetryMessage}
|
|
148
|
-
onRevokeMessage={handleRevokeMessage}
|
|
149
|
-
/>
|
|
150
|
-
}
|
|
151
|
-
/>
|
|
152
|
-
</Modal>
|
|
153
|
-
);
|
|
154
|
-
});
|
|
180
|
+
);
|
|
155
181
|
|
|
156
182
|
MessageToolboxOverlay.displayName = 'MessageToolboxOverlay';
|
|
157
183
|
|
|
@@ -11,6 +11,17 @@ const COPYABLE_TEXT_TYPES: number[] = [
|
|
|
11
11
|
MessageType.UrlTextMessage,
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
// BR-001: Pin message supports text, link, image, video, file.
|
|
15
|
+
const PIN_SUPPORTED_TYPES: number[] = [
|
|
16
|
+
MessageType.TextMessage,
|
|
17
|
+
MessageType.AtTextMessage,
|
|
18
|
+
MessageType.QuoteMessage,
|
|
19
|
+
MessageType.UrlTextMessage,
|
|
20
|
+
MessageType.PictureMessage,
|
|
21
|
+
MessageType.VideoMessage,
|
|
22
|
+
MessageType.FileMessage,
|
|
23
|
+
];
|
|
24
|
+
|
|
14
25
|
const isMessageSucceeded = (message: DMessageItem): boolean =>
|
|
15
26
|
message.status !== MessageStatus.Failed &&
|
|
16
27
|
message.status !== MessageStatus.Sending;
|
|
@@ -30,6 +41,20 @@ const isCopyMessageSupported = (message: DMessageItem): boolean =>
|
|
|
30
41
|
const isRetrySupported = (message: DMessageItem): boolean =>
|
|
31
42
|
message.status === MessageStatus.Failed;
|
|
32
43
|
|
|
44
|
+
// Merged/album messages (grouped photos) count as "image" for pinning
|
|
45
|
+
// purposes, mirroring the preview logic in utils/messageUtils.ts.
|
|
46
|
+
const isMergedImageMessage = (message: DMessageItem): boolean =>
|
|
47
|
+
message.contentType === MessageType.MergeMessage &&
|
|
48
|
+
!!message.mergeElem?.multiMessage?.some(
|
|
49
|
+
(msg) => msg.contentType === MessageType.PictureMessage
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// BR-002: Pin message supports only successfully sent messages.
|
|
53
|
+
const isPinMessageSupported = (message: DMessageItem): boolean =>
|
|
54
|
+
isEligibleForToolbox(message) &&
|
|
55
|
+
(PIN_SUPPORTED_TYPES.includes(message.contentType) ||
|
|
56
|
+
isMergedImageMessage(message));
|
|
57
|
+
|
|
33
58
|
// Revoke is only available within 24h of a successful send.
|
|
34
59
|
const REVOKE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
35
60
|
|
|
@@ -64,10 +89,16 @@ export const getMessageToolboxItems = (
|
|
|
64
89
|
const showRetryMessage = isRetrySupported(message);
|
|
65
90
|
const showRevokeMessage = isRevokeMessageSupported(message, currentUserId);
|
|
66
91
|
|
|
92
|
+
const isPinSupported = isPinMessageSupported(message);
|
|
93
|
+
const showPinMessage = isPinSupported && !message.isPinned;
|
|
94
|
+
const showUnpinMessage = isPinSupported && !!message.isPinned;
|
|
95
|
+
|
|
67
96
|
return {
|
|
68
97
|
showCopyMessage,
|
|
69
98
|
// showCopyMessageLink,
|
|
70
99
|
showRetryMessage,
|
|
71
100
|
showRevokeMessage,
|
|
101
|
+
showPinMessage,
|
|
102
|
+
showUnpinMessage,
|
|
72
103
|
};
|
|
73
104
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { DMessageItem } from '../types/chat';
|
|
3
|
+
|
|
4
|
+
export interface PinnedMessageStore {
|
|
5
|
+
messages: DMessageItem[];
|
|
6
|
+
totalCount: number;
|
|
7
|
+
setMessages: (messages: DMessageItem[], totalCount: number) => void;
|
|
8
|
+
addMessage: (message: DMessageItem) => void;
|
|
9
|
+
removeMessage: (clientMsgID: string) => void;
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const usePinnedMessageStore = create<PinnedMessageStore>()(
|
|
14
|
+
(set, get) => ({
|
|
15
|
+
messages: [],
|
|
16
|
+
totalCount: 0,
|
|
17
|
+
|
|
18
|
+
setMessages: (messages, totalCount) => {
|
|
19
|
+
set({ messages, totalCount });
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
addMessage: (message) => {
|
|
23
|
+
const messages = [message, ...get().messages];
|
|
24
|
+
set({ messages, totalCount: messages.length });
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
removeMessage: (clientMsgID) => {
|
|
28
|
+
const messages = get().messages.filter(
|
|
29
|
+
(msg) => msg.clientMsgID !== clientMsgID
|
|
30
|
+
);
|
|
31
|
+
set({ messages, totalCount: messages.length });
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
reset: () => {
|
|
35
|
+
set({ messages: [], totalCount: 0 });
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
);
|
|
@@ -34,6 +34,19 @@ const i18nKey: Record<string, string> = {
|
|
|
34
34
|
'chat:copy_message_link_success': 'Sao chép đường dẫn tin nhắn thành công',
|
|
35
35
|
'chat:retry_message': 'Gửi lại',
|
|
36
36
|
'chat:revoke_message': 'Thu hồi',
|
|
37
|
+
'chat:pin_message': 'Ghim',
|
|
38
|
+
'chat:unpin_message': 'Bỏ ghim',
|
|
39
|
+
'chat:pin_message_success': 'Đã ghim tin nhắn',
|
|
40
|
+
'chat:unpin_message_success': 'Đã bỏ ghim tin nhắn',
|
|
41
|
+
'chat:pinned_message_subtitle': 'Tin nhắn từ {{sender}}',
|
|
42
|
+
'chat:pinned_image_subtitle': 'Hình ảnh từ {{sender}}',
|
|
43
|
+
'chat:pinned_video_subtitle': 'Video từ {{sender}}',
|
|
44
|
+
'chat:pinned_file_subtitle': 'Tài liệu từ {{sender}}',
|
|
45
|
+
'chat:pinned_link_subtitle': 'Liên kết từ {{sender}}',
|
|
46
|
+
'chat:pin_limit_title': 'Không thể ghim thêm tin nhắn',
|
|
47
|
+
'chat:pin_limit_desc':
|
|
48
|
+
'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
|
+
'chat:pin_limit_primary': 'Đã hiểu',
|
|
37
50
|
'msg_type_image': '[Hình ảnh]',
|
|
38
51
|
'msg_type_voice': '[Tin nhắn thoại]',
|
|
39
52
|
'msg_type_video': '[Video]',
|
package/src/types/chat.ts
CHANGED
|
@@ -94,6 +94,9 @@ export interface DMessageItem extends MessageItem {
|
|
|
94
94
|
atUserIDList?: unknown;
|
|
95
95
|
attachedInfo?: 'null';
|
|
96
96
|
options?: DMessageOption;
|
|
97
|
+
isPinned?: boolean;
|
|
98
|
+
pinnedByUserID?: string;
|
|
99
|
+
pinnedTime?: number;
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
export interface DMessageOption {
|
|
@@ -141,7 +144,7 @@ export interface MessageStore {
|
|
|
141
144
|
setMessages: (messages: DMessageItem[], hasMore: boolean) => void;
|
|
142
145
|
pushNewMessage: (message: MessageItem) => void;
|
|
143
146
|
updateOneMessage: (
|
|
144
|
-
message: Partial<
|
|
147
|
+
message: Partial<DMessageItem> & Pick<MessageItem, 'clientMsgID'>
|
|
145
148
|
) => void;
|
|
146
149
|
deleteOneMessage: (clientMsgID: string) => void;
|
|
147
150
|
reset: () => void;
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
type ConversationItem,
|
|
9
9
|
} from '@droppii/openim-rn-client-sdk';
|
|
10
10
|
import { type DMessageItem } from '../types/chat';
|
|
11
|
+
import type { IUrlMetadata } from '../types/common';
|
|
11
12
|
import { formatDuration } from './dateTimeUtils';
|
|
13
|
+
import { getFileIcon, getMimeType } from './imageUtils';
|
|
12
14
|
import { trans } from '../translation';
|
|
13
15
|
|
|
14
16
|
export const isMessageRevoked = (message: DMessageItem): boolean =>
|
|
@@ -92,6 +94,94 @@ export const getMessagePreview = (message: DMessageItem): string => {
|
|
|
92
94
|
}
|
|
93
95
|
};
|
|
94
96
|
|
|
97
|
+
export type PinnedMessageMediaType = 'image' | 'video' | 'file' | 'text';
|
|
98
|
+
|
|
99
|
+
export interface PinnedMessagePreview {
|
|
100
|
+
mediaType: PinnedMessageMediaType;
|
|
101
|
+
subtitleKey: string;
|
|
102
|
+
thumbnailUrl?: string;
|
|
103
|
+
fileIcon?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const getImageUrl = (pictureElem: any): string | undefined =>
|
|
107
|
+
pictureElem?.snapshotPicture?.url ||
|
|
108
|
+
pictureElem?.bigPicture?.url ||
|
|
109
|
+
pictureElem?.sourcePicture?.url;
|
|
110
|
+
|
|
111
|
+
// Link messages carry their rich-preview metadata (title/description/image)
|
|
112
|
+
// as a JSON string in `ex`, set by useSendMessage's IMessageItemEx payload.
|
|
113
|
+
const getUrlMetadata = (message: DMessageItem): IUrlMetadata | undefined => {
|
|
114
|
+
if (!message.ex) return undefined;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(message.ex) as { urlMetadata?: IUrlMetadata };
|
|
117
|
+
return parsed.urlMetadata;
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const getPinnedMessagePreview = (
|
|
124
|
+
message: DMessageItem
|
|
125
|
+
): PinnedMessagePreview => {
|
|
126
|
+
switch (message.contentType) {
|
|
127
|
+
case MessageType.PictureMessage: {
|
|
128
|
+
const thumbnailUrl = getImageUrl((message as any).pictureElem);
|
|
129
|
+
return {
|
|
130
|
+
mediaType: 'image',
|
|
131
|
+
subtitleKey: 'chat:pinned_image_subtitle',
|
|
132
|
+
thumbnailUrl,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case MessageType.VideoMessage:
|
|
137
|
+
return {
|
|
138
|
+
mediaType: 'video',
|
|
139
|
+
subtitleKey: 'chat:pinned_video_subtitle',
|
|
140
|
+
thumbnailUrl: message.videoElem?.snapshotUrl,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
case MessageType.FileMessage: {
|
|
144
|
+
const fileName = message.fileElem?.fileName || '';
|
|
145
|
+
return {
|
|
146
|
+
mediaType: 'file',
|
|
147
|
+
subtitleKey: 'chat:pinned_file_subtitle',
|
|
148
|
+
fileIcon: getFileIcon(getMimeType(fileName)),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case MessageType.UrlTextMessage: {
|
|
153
|
+
const thumbnailUrl = getUrlMetadata(message)?.image;
|
|
154
|
+
return {
|
|
155
|
+
mediaType: thumbnailUrl ? 'image' : 'text',
|
|
156
|
+
subtitleKey: 'chat:pinned_link_subtitle',
|
|
157
|
+
thumbnailUrl,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case MessageType.MergeMessage: {
|
|
162
|
+
try {
|
|
163
|
+
const firstImage = message.mergeElem?.multiMessage?.find(
|
|
164
|
+
(msg: any) => msg.contentType === MessageType.PictureMessage
|
|
165
|
+
);
|
|
166
|
+
const thumbnailUrl = getImageUrl(firstImage?.pictureElem);
|
|
167
|
+
if (thumbnailUrl) {
|
|
168
|
+
return {
|
|
169
|
+
mediaType: 'image',
|
|
170
|
+
subtitleKey: 'chat:pinned_image_subtitle',
|
|
171
|
+
thumbnailUrl,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Fall through to default
|
|
176
|
+
}
|
|
177
|
+
return { mediaType: 'text', subtitleKey: 'chat:pinned_message_subtitle' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
default:
|
|
181
|
+
return { mediaType: 'text', subtitleKey: 'chat:pinned_message_subtitle' };
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
95
185
|
export const getLastMessageText = (
|
|
96
186
|
message: DMessageItem | undefined,
|
|
97
187
|
currentUserId?: string,
|