@droppii-org/chat-mobile 0.2.39 → 0.2.41
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 +3 -10
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/message/useSendMessage.js +0 -3
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessageBar.js +4 -1
- package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +80 -110
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js +179 -0
- package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +18 -6
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/store/message.js +33 -0
- package/lib/module/store/message.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +3 -2
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/utils/UIUtils.js +4 -2
- package/lib/module/utils/UIUtils.js.map +1 -1
- package/lib/module/utils/toastFlashMessage.js +4 -1
- package/lib/module/utils/toastFlashMessage.js.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheetBackup.d.ts +9 -0
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheetBackup.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/store/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 +1 -0
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -1
- package/lib/typescript/src/utils/toastFlashMessage.d.ts +1 -0
- package/lib/typescript/src/utils/toastFlashMessage.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/core/useChatListener.ts +3 -18
- package/src/hooks/message/useSendMessage.ts +0 -2
- package/src/screens/chat-detail/PinnedMessageBar.tsx +17 -8
- package/src/screens/chat-detail/PinnedMessagesSheet.tsx +118 -153
- package/src/screens/chat-detail/PinnedMessagesSheetBackup.tsx +253 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +26 -13
- package/src/store/message.ts +34 -0
- package/src/translation/resources/i18n.ts +4 -2
- package/src/types/chat.ts +1 -1
- package/src/utils/UIUtils.ts +3 -1
- package/src/utils/toastFlashMessage.ts +6 -0
|
@@ -12,14 +12,12 @@ import { useCallback, useEffect } from 'react';
|
|
|
12
12
|
import { useConversationStore } from '../store/conversation';
|
|
13
13
|
import { ToastFlashMessage } from '../utils/toastFlashMessage';
|
|
14
14
|
import { useUserStore } from '../store/user';
|
|
15
|
-
import { getLastMessageText
|
|
15
|
+
import { getLastMessageText } from '../utils/messageUtils';
|
|
16
16
|
import { useChatContext } from '../context';
|
|
17
17
|
import isEmpty from 'lodash/isEmpty';
|
|
18
18
|
import { markMessageRevoked, useMessageStore } from '../store/message';
|
|
19
19
|
import { syncPinnedMessagesOnEvent } from '../hooks/message/usePinMessage';
|
|
20
20
|
|
|
21
|
-
const MAX_TOAST_FILE_NAME_LENGTH = 40;
|
|
22
|
-
|
|
23
21
|
export const useChatListener = () => {
|
|
24
22
|
const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
|
|
25
23
|
const currentUserId = useUserStore((s) => s.user?.userID);
|
|
@@ -55,22 +53,8 @@ export const useChatListener = () => {
|
|
|
55
53
|
const latestMsg: MessageItem = JSON.parse(
|
|
56
54
|
conversation.latestMsg || '{}'
|
|
57
55
|
);
|
|
58
|
-
const messageForToast =
|
|
59
|
-
latestMsg.contentType === MessageType.FileMessage &&
|
|
60
|
-
latestMsg.fileElem?.fileName
|
|
61
|
-
? {
|
|
62
|
-
...latestMsg,
|
|
63
|
-
fileElem: {
|
|
64
|
-
...latestMsg.fileElem,
|
|
65
|
-
fileName: truncateMiddle(
|
|
66
|
-
latestMsg.fileElem.fileName,
|
|
67
|
-
MAX_TOAST_FILE_NAME_LENGTH
|
|
68
|
-
),
|
|
69
|
-
},
|
|
70
|
-
}
|
|
71
|
-
: latestMsg;
|
|
72
56
|
const lastMessageText = getLastMessageText(
|
|
73
|
-
|
|
57
|
+
latestMsg,
|
|
74
58
|
currentUserId,
|
|
75
59
|
conversation
|
|
76
60
|
);
|
|
@@ -78,6 +62,7 @@ export const useChatListener = () => {
|
|
|
78
62
|
key: conversation.conversationID,
|
|
79
63
|
title: conversation.showName || 'New Message',
|
|
80
64
|
body: lastMessageText,
|
|
65
|
+
ellipsizeMiddle: latestMsg.contentType === MessageType.FileMessage,
|
|
81
66
|
onPress: () => {
|
|
82
67
|
ToastFlashMessage.hide(conversation.conversationID);
|
|
83
68
|
onPressFlashMessage?.(conversation);
|
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
getMessagePreviewParts,
|
|
14
14
|
getPinnedMessagePreview,
|
|
15
15
|
} from '../../utils/messageUtils';
|
|
16
|
-
import type
|
|
16
|
+
import { MessageType, type MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
17
|
+
|
|
18
|
+
const HIDDEN_PREVIEW_TEXT_TYPES = new Set([
|
|
19
|
+
MessageType.PictureMessage,
|
|
20
|
+
MessageType.VideoMessage,
|
|
21
|
+
MessageType.MergeMessage,
|
|
22
|
+
]);
|
|
17
23
|
|
|
18
24
|
interface PinnedMessageBarProps {
|
|
19
25
|
messages: MessageItem[];
|
|
@@ -85,6 +91,7 @@ export const PinnedMessageBar = memo(
|
|
|
85
91
|
const remainingCount = messages.length - 1;
|
|
86
92
|
const { subtitleKey } = getPinnedMessagePreview(latest);
|
|
87
93
|
const messagePreview = getMessagePreviewParts(latest);
|
|
94
|
+
const hidePreviewText = HIDDEN_PREVIEW_TEXT_TYPES.has(latest.contentType);
|
|
88
95
|
|
|
89
96
|
return (
|
|
90
97
|
<KContainer.Touchable
|
|
@@ -96,13 +103,15 @@ export const PinnedMessageBar = memo(
|
|
|
96
103
|
|
|
97
104
|
<KContainer.View row alignItems flex paddingR="0.75rem">
|
|
98
105
|
<KContainer.View style={styles.content}>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
{!hidePreviewText && (
|
|
107
|
+
<KLabel.Text
|
|
108
|
+
typo="TextNmMedium"
|
|
109
|
+
numberOfLines={1}
|
|
110
|
+
{...messagePreview.textProps}
|
|
111
|
+
>
|
|
112
|
+
{messagePreview.text}
|
|
113
|
+
</KLabel.Text>
|
|
114
|
+
)}
|
|
106
115
|
|
|
107
116
|
<KContainer.View row alignItems paddingR="0.75rem">
|
|
108
117
|
<KImage.VectorIcons
|
|
@@ -1,127 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
forwardRef,
|
|
3
|
-
memo,
|
|
4
|
-
useCallback,
|
|
5
|
-
useImperativeHandle,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from 'react';
|
|
1
|
+
import { memo, useCallback } from 'react';
|
|
9
2
|
import type { ReactNode } from 'react';
|
|
10
|
-
import {
|
|
11
|
-
import type
|
|
12
|
-
import {
|
|
3
|
+
import { StyleSheet } from 'react-native';
|
|
4
|
+
import { MessageType, type MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
5
|
+
import {
|
|
6
|
+
KContainer,
|
|
7
|
+
KLabel,
|
|
8
|
+
KColors,
|
|
9
|
+
KImage,
|
|
10
|
+
KSpacingValue,
|
|
11
|
+
} from '@droppii/libs';
|
|
13
12
|
import { UIUtils } from '../../utils/UIUtils';
|
|
14
13
|
import { trans } from '../../translation';
|
|
15
14
|
import { usePinnedMessageStore } from '../../store/pinnedMessage';
|
|
16
15
|
import { LegendChatMessage } from './legend/LegendChatMessage';
|
|
17
|
-
import { formatMessageTime
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
currentUserId?: string;
|
|
29
|
-
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
30
|
-
customMessageItemFactory?: (item: MessageItem) => ReactNode;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Mirrors MessageToolboxOverlay (used for the main chat list's long-press
|
|
34
|
-
// menu) but restricted to the 2 actions relevant to an already-pinned
|
|
35
|
-
// message: unpin and jump to the original message.
|
|
36
|
-
const PinnedMessageToolboxOverlay = memo(
|
|
37
|
-
forwardRef<PinnedMessageToolboxRef, PinnedMessageToolboxOverlayProps>(
|
|
38
|
-
({ currentUserId, unpinMessage, customMessageItemFactory }, ref) => {
|
|
39
|
-
const [visible, setVisible] = useState(false);
|
|
40
|
-
const [message, setMessage] = useState<MessageItem | undefined>(
|
|
41
|
-
undefined
|
|
42
|
-
);
|
|
43
|
-
const [rect, setRect] = useState<MeasuredRect | null>(null);
|
|
44
|
-
|
|
45
|
-
const hide = useCallback(() => {
|
|
46
|
-
setVisible(false);
|
|
47
|
-
setMessage(undefined);
|
|
48
|
-
setRect(null);
|
|
49
|
-
}, []);
|
|
50
|
-
|
|
51
|
-
const show = useCallback((msg: MessageItem, msgRect: MeasuredRect) => {
|
|
52
|
-
setMessage(msg);
|
|
53
|
-
setRect(msgRect);
|
|
54
|
-
setVisible(true);
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
|
-
useImperativeHandle(ref, () => ({ show, hide }), [show, hide]);
|
|
58
|
-
|
|
59
|
-
const handleUnpinMessage = useCallback(() => {
|
|
60
|
-
if (!message) return;
|
|
61
|
-
hide();
|
|
62
|
-
unpinMessage(message);
|
|
63
|
-
}, [message, hide, unpinMessage]);
|
|
64
|
-
|
|
65
|
-
const handleViewOriginalMessage = useCallback(() => {
|
|
66
|
-
if (!message) return;
|
|
67
|
-
hide();
|
|
68
|
-
// TODO: implement navigating to / highlighting the original message
|
|
69
|
-
}, [message, hide]);
|
|
70
|
-
|
|
71
|
-
if (!visible || !message || !rect) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const isOutgoing = message.sendID === currentUserId;
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<Modal
|
|
79
|
-
visible={visible}
|
|
80
|
-
transparent
|
|
81
|
-
statusBarTranslucent
|
|
82
|
-
animationType="fade"
|
|
83
|
-
onRequestClose={hide}
|
|
84
|
-
>
|
|
85
|
-
<Pressable style={styles.backdrop} onPress={hide} />
|
|
86
|
-
|
|
87
|
-
<MessageToolboxWrapper
|
|
88
|
-
rect={rect}
|
|
89
|
-
isOutgoing={isOutgoing}
|
|
90
|
-
message={
|
|
91
|
-
<LegendChatMessage
|
|
92
|
-
message={message}
|
|
93
|
-
isOutgoing={isOutgoing}
|
|
94
|
-
createdAtTime={message.sendTime || message.createTime}
|
|
95
|
-
customMessageItemFactory={customMessageItemFactory}
|
|
96
|
-
/>
|
|
97
|
-
}
|
|
98
|
-
toolbox={
|
|
99
|
-
<MessageToolboxMenu
|
|
100
|
-
showUnpinMessage
|
|
101
|
-
showViewOriginalMessage
|
|
102
|
-
onUnpinMessage={handleUnpinMessage}
|
|
103
|
-
onViewOriginalMessage={handleViewOriginalMessage}
|
|
104
|
-
/>
|
|
105
|
-
}
|
|
106
|
-
/>
|
|
107
|
-
</Modal>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
)
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
PinnedMessageToolboxOverlay.displayName = 'PinnedMessageToolboxOverlay';
|
|
114
|
-
|
|
115
|
-
const styles = StyleSheet.create({
|
|
116
|
-
backdrop: {
|
|
117
|
-
position: 'absolute',
|
|
118
|
-
top: 0,
|
|
119
|
-
left: 0,
|
|
120
|
-
right: 0,
|
|
121
|
-
bottom: 0,
|
|
122
|
-
backgroundColor: 'rgba(0,0,0,0.75)',
|
|
123
|
-
},
|
|
124
|
-
});
|
|
16
|
+
import { formatMessageTime } from './legend/messageTypes';
|
|
17
|
+
|
|
18
|
+
// Text/quote/link bubbles already render their own timestamp for incoming
|
|
19
|
+
// messages (see textMessage, quotedMessage, linkMessage) — skip the external
|
|
20
|
+
// label for those to avoid showing the time twice.
|
|
21
|
+
const SELF_RENDERS_TIME_FOR_INCOMING = new Set([
|
|
22
|
+
MessageType.TextMessage,
|
|
23
|
+
MessageType.AtTextMessage,
|
|
24
|
+
MessageType.QuoteMessage,
|
|
25
|
+
MessageType.UrlTextMessage,
|
|
26
|
+
]);
|
|
125
27
|
|
|
126
28
|
interface PinnedMessagesSheetContentProps {
|
|
127
29
|
currentUserId?: string;
|
|
@@ -136,19 +38,25 @@ const PinnedMessagesSheetContent = memo(
|
|
|
136
38
|
customMessageItemFactory,
|
|
137
39
|
}: PinnedMessagesSheetContentProps) => {
|
|
138
40
|
const messages = usePinnedMessageStore((state) => state.messages);
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
toolboxRef.current?.show(message, { x, y, width, height });
|
|
147
|
-
});
|
|
41
|
+
|
|
42
|
+
const handleUnpinMessage = useCallback(
|
|
43
|
+
async (message: MessageItem) => {
|
|
44
|
+
await unpinMessage(message);
|
|
45
|
+
if (usePinnedMessageStore.getState().messages.length === 0) {
|
|
46
|
+
UIUtils.bottomSheet.dismiss();
|
|
47
|
+
}
|
|
148
48
|
},
|
|
149
|
-
[]
|
|
49
|
+
[unpinMessage]
|
|
150
50
|
);
|
|
151
51
|
|
|
52
|
+
const handlePressMessage = useCallback(() => {
|
|
53
|
+
UIUtils.bottomSheet.dismiss();
|
|
54
|
+
UIUtils.alert.open({
|
|
55
|
+
title: trans('chat:view_original_message'),
|
|
56
|
+
message: trans('chat:scroll_to_message_todo'),
|
|
57
|
+
});
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
152
60
|
return (
|
|
153
61
|
<KContainer.View paddingV="0.5rem">
|
|
154
62
|
<KLabel.Text
|
|
@@ -164,28 +72,58 @@ const PinnedMessagesSheetContent = memo(
|
|
|
164
72
|
const isOutgoing = message.sendID === currentUserId;
|
|
165
73
|
const messageId = message.clientMsgID || message.serverMsgID;
|
|
166
74
|
const createdAtTime = message.sendTime || message.createTime;
|
|
167
|
-
const
|
|
75
|
+
const hasInternalTimeLabel =
|
|
76
|
+
!isOutgoing &&
|
|
77
|
+
SELF_RENDERS_TIME_FOR_INCOMING.has(message.contentType);
|
|
78
|
+
const timeLabel = hasInternalTimeLabel
|
|
79
|
+
? ''
|
|
80
|
+
: formatMessageTime(createdAtTime);
|
|
81
|
+
|
|
82
|
+
const unpinButton = (
|
|
83
|
+
<KContainer.Touchable
|
|
84
|
+
center
|
|
85
|
+
br="round"
|
|
86
|
+
width={28}
|
|
87
|
+
height={28}
|
|
88
|
+
background={KColors.background}
|
|
89
|
+
style={
|
|
90
|
+
isOutgoing ? styles.unpinButtonLeft : styles.unpinButtonRight
|
|
91
|
+
}
|
|
92
|
+
onPress={() => handleUnpinMessage(message)}
|
|
93
|
+
>
|
|
94
|
+
<KImage.VectorIcons
|
|
95
|
+
name="thumbtack-off-o"
|
|
96
|
+
provider="DroppiiNew"
|
|
97
|
+
size={14}
|
|
98
|
+
color={KColors.gray.dark}
|
|
99
|
+
/>
|
|
100
|
+
</KContainer.Touchable>
|
|
101
|
+
);
|
|
168
102
|
|
|
169
103
|
return (
|
|
170
104
|
<KContainer.View
|
|
171
105
|
key={messageId}
|
|
172
|
-
|
|
173
|
-
if (node) rowRefsMap.current.set(messageId, node);
|
|
174
|
-
else rowRefsMap.current.delete(messageId);
|
|
175
|
-
}}
|
|
176
|
-
style={
|
|
177
|
-
isOutgoing
|
|
178
|
-
? messageStyles.messageRowRight
|
|
179
|
-
: messageStyles.messageRowLeft
|
|
180
|
-
}
|
|
106
|
+
style={isOutgoing ? styles.rowRight : styles.rowLeft}
|
|
181
107
|
>
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
108
|
+
<KContainer.View
|
|
109
|
+
style={[
|
|
110
|
+
styles.messageRow,
|
|
111
|
+
isOutgoing ? styles.messageRowRight : styles.messageRowLeft,
|
|
112
|
+
]}
|
|
113
|
+
>
|
|
114
|
+
{isOutgoing && unpinButton}
|
|
115
|
+
|
|
116
|
+
<LegendChatMessage
|
|
117
|
+
message={message}
|
|
118
|
+
isOutgoing={isOutgoing}
|
|
119
|
+
createdAtTime={createdAtTime}
|
|
120
|
+
onPress={handlePressMessage}
|
|
121
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
{!isOutgoing && unpinButton}
|
|
125
|
+
</KContainer.View>
|
|
126
|
+
|
|
189
127
|
{!!timeLabel && (
|
|
190
128
|
<KLabel.Text
|
|
191
129
|
typo="TextXsNormal"
|
|
@@ -198,13 +136,6 @@ const PinnedMessagesSheetContent = memo(
|
|
|
198
136
|
</KContainer.View>
|
|
199
137
|
);
|
|
200
138
|
})}
|
|
201
|
-
|
|
202
|
-
<PinnedMessageToolboxOverlay
|
|
203
|
-
ref={toolboxRef}
|
|
204
|
-
currentUserId={currentUserId}
|
|
205
|
-
unpinMessage={unpinMessage}
|
|
206
|
-
customMessageItemFactory={customMessageItemFactory}
|
|
207
|
-
/>
|
|
208
139
|
</KContainer.View>
|
|
209
140
|
);
|
|
210
141
|
}
|
|
@@ -212,6 +143,40 @@ const PinnedMessagesSheetContent = memo(
|
|
|
212
143
|
|
|
213
144
|
PinnedMessagesSheetContent.displayName = 'PinnedMessagesSheetContent';
|
|
214
145
|
|
|
146
|
+
const UNPIN_BUTTON_GAP = KSpacingValue['0.5rem'];
|
|
147
|
+
|
|
148
|
+
const styles = StyleSheet.create({
|
|
149
|
+
rowLeft: {
|
|
150
|
+
width: '100%',
|
|
151
|
+
paddingHorizontal: KSpacingValue['0.75rem'],
|
|
152
|
+
marginBottom: KSpacingValue['0.25rem'],
|
|
153
|
+
alignItems: 'flex-start',
|
|
154
|
+
},
|
|
155
|
+
rowRight: {
|
|
156
|
+
width: '100%',
|
|
157
|
+
paddingHorizontal: KSpacingValue['0.75rem'],
|
|
158
|
+
marginBottom: KSpacingValue['0.25rem'],
|
|
159
|
+
alignItems: 'flex-end',
|
|
160
|
+
},
|
|
161
|
+
messageRow: {
|
|
162
|
+
width: '100%',
|
|
163
|
+
flexDirection: 'row',
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
},
|
|
166
|
+
messageRowLeft: {
|
|
167
|
+
justifyContent: 'flex-start',
|
|
168
|
+
},
|
|
169
|
+
messageRowRight: {
|
|
170
|
+
justifyContent: 'flex-end',
|
|
171
|
+
},
|
|
172
|
+
unpinButtonLeft: {
|
|
173
|
+
marginRight: UNPIN_BUTTON_GAP,
|
|
174
|
+
},
|
|
175
|
+
unpinButtonRight: {
|
|
176
|
+
marginLeft: UNPIN_BUTTON_GAP,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
215
180
|
export interface OpenPinnedMessagesSheetParams {
|
|
216
181
|
currentUserId?: string;
|
|
217
182
|
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
|
|
22
|
+
interface PinnedMessageToolboxRef {
|
|
23
|
+
show: (message: MessageItem, rect: MeasuredRect) => void;
|
|
24
|
+
hide: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PinnedMessageToolboxOverlayProps {
|
|
28
|
+
currentUserId?: string;
|
|
29
|
+
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
30
|
+
customMessageItemFactory?: (item: MessageItem) => ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Mirrors MessageToolboxOverlay (used for the main chat list's long-press
|
|
34
|
+
// menu) but restricted to the 2 actions relevant to an already-pinned
|
|
35
|
+
// message: unpin and jump to the original message.
|
|
36
|
+
const PinnedMessageToolboxOverlay = memo(
|
|
37
|
+
forwardRef<PinnedMessageToolboxRef, PinnedMessageToolboxOverlayProps>(
|
|
38
|
+
({ currentUserId, unpinMessage, customMessageItemFactory }, ref) => {
|
|
39
|
+
const [visible, setVisible] = useState(false);
|
|
40
|
+
const [message, setMessage] = useState<MessageItem | undefined>(
|
|
41
|
+
undefined
|
|
42
|
+
);
|
|
43
|
+
const [rect, setRect] = useState<MeasuredRect | null>(null);
|
|
44
|
+
|
|
45
|
+
const hide = useCallback(() => {
|
|
46
|
+
setVisible(false);
|
|
47
|
+
setMessage(undefined);
|
|
48
|
+
setRect(null);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const show = useCallback((msg: MessageItem, msgRect: MeasuredRect) => {
|
|
52
|
+
setMessage(msg);
|
|
53
|
+
setRect(msgRect);
|
|
54
|
+
setVisible(true);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
useImperativeHandle(ref, () => ({ show, hide }), [show, hide]);
|
|
58
|
+
|
|
59
|
+
const handleUnpinMessage = useCallback(() => {
|
|
60
|
+
if (!message) return;
|
|
61
|
+
hide();
|
|
62
|
+
unpinMessage(message);
|
|
63
|
+
}, [message, hide, unpinMessage]);
|
|
64
|
+
|
|
65
|
+
const handleViewOriginalMessage = useCallback(() => {
|
|
66
|
+
if (!message) return;
|
|
67
|
+
hide();
|
|
68
|
+
// TODO: implement navigating to / highlighting the original message
|
|
69
|
+
}, [message, hide]);
|
|
70
|
+
|
|
71
|
+
if (!visible || !message || !rect) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isOutgoing = message.sendID === currentUserId;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Modal
|
|
79
|
+
visible={visible}
|
|
80
|
+
transparent
|
|
81
|
+
statusBarTranslucent
|
|
82
|
+
animationType="fade"
|
|
83
|
+
onRequestClose={hide}
|
|
84
|
+
>
|
|
85
|
+
<Pressable style={styles.backdrop} onPress={hide} />
|
|
86
|
+
|
|
87
|
+
<MessageToolboxWrapper
|
|
88
|
+
rect={rect}
|
|
89
|
+
isOutgoing={isOutgoing}
|
|
90
|
+
message={
|
|
91
|
+
<LegendChatMessage
|
|
92
|
+
message={message}
|
|
93
|
+
isOutgoing={isOutgoing}
|
|
94
|
+
createdAtTime={message.sendTime || message.createTime}
|
|
95
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
96
|
+
/>
|
|
97
|
+
}
|
|
98
|
+
toolbox={
|
|
99
|
+
<MessageToolboxMenu
|
|
100
|
+
showUnpinMessage
|
|
101
|
+
showViewOriginalMessage
|
|
102
|
+
onUnpinMessage={handleUnpinMessage}
|
|
103
|
+
onViewOriginalMessage={handleViewOriginalMessage}
|
|
104
|
+
/>
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
</Modal>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
PinnedMessageToolboxOverlay.displayName = 'PinnedMessageToolboxOverlay';
|
|
114
|
+
|
|
115
|
+
const styles = StyleSheet.create({
|
|
116
|
+
backdrop: {
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
top: 0,
|
|
119
|
+
left: 0,
|
|
120
|
+
right: 0,
|
|
121
|
+
bottom: 0,
|
|
122
|
+
backgroundColor: 'rgba(0,0,0,0.75)',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
interface PinnedMessagesSheetContentProps {
|
|
127
|
+
currentUserId?: string;
|
|
128
|
+
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
129
|
+
customMessageItemFactory?: (item: MessageItem) => ReactNode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const PinnedMessagesSheetContent = memo(
|
|
133
|
+
({
|
|
134
|
+
currentUserId,
|
|
135
|
+
unpinMessage,
|
|
136
|
+
customMessageItemFactory,
|
|
137
|
+
}: PinnedMessagesSheetContentProps) => {
|
|
138
|
+
const messages = usePinnedMessageStore((state) => state.messages);
|
|
139
|
+
const toolboxRef = useRef<PinnedMessageToolboxRef>(null);
|
|
140
|
+
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
141
|
+
|
|
142
|
+
const handleUnpinMessage = useCallback(
|
|
143
|
+
async (message: MessageItem) => {
|
|
144
|
+
await unpinMessage(message);
|
|
145
|
+
if (usePinnedMessageStore.getState().messages.length === 0) {
|
|
146
|
+
UIUtils.bottomSheet.dismiss();
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
[unpinMessage]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const handleLongPress = useCallback(
|
|
153
|
+
(messageId: string, message: MessageItem) => {
|
|
154
|
+
const node = rowRefsMap.current.get(messageId);
|
|
155
|
+
node?.measureInWindow((x, y, width, height) => {
|
|
156
|
+
toolboxRef.current?.show(message, { x, y, width, height });
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
[]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<KContainer.View paddingV="0.5rem">
|
|
164
|
+
<KLabel.Text
|
|
165
|
+
typo="TextNmNormal"
|
|
166
|
+
color={KColors.gray.normal}
|
|
167
|
+
paddingH="0.75rem"
|
|
168
|
+
marginB="0.75rem"
|
|
169
|
+
>
|
|
170
|
+
{trans('chat:pinned_messages_hint')}
|
|
171
|
+
</KLabel.Text>
|
|
172
|
+
|
|
173
|
+
{messages.map((message) => {
|
|
174
|
+
const isOutgoing = message.sendID === currentUserId;
|
|
175
|
+
const messageId = message.clientMsgID || message.serverMsgID;
|
|
176
|
+
const createdAtTime = message.sendTime || message.createTime;
|
|
177
|
+
const timeLabel = formatMessageTime(createdAtTime);
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<KContainer.View
|
|
181
|
+
key={messageId}
|
|
182
|
+
ref={(node: View | null) => {
|
|
183
|
+
if (node) rowRefsMap.current.set(messageId, node);
|
|
184
|
+
else rowRefsMap.current.delete(messageId);
|
|
185
|
+
}}
|
|
186
|
+
style={
|
|
187
|
+
isOutgoing
|
|
188
|
+
? messageStyles.messageRowRight
|
|
189
|
+
: messageStyles.messageRowLeft
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
<LegendChatMessage
|
|
193
|
+
message={message}
|
|
194
|
+
isOutgoing={isOutgoing}
|
|
195
|
+
createdAtTime={createdAtTime}
|
|
196
|
+
onLongPress={() => handleLongPress(messageId, message)}
|
|
197
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
198
|
+
/>
|
|
199
|
+
{!!timeLabel && (
|
|
200
|
+
<KLabel.Text
|
|
201
|
+
typo="TextXsNormal"
|
|
202
|
+
color={KColors.gray.light}
|
|
203
|
+
marginT="0.25rem"
|
|
204
|
+
>
|
|
205
|
+
{timeLabel}
|
|
206
|
+
</KLabel.Text>
|
|
207
|
+
)}
|
|
208
|
+
</KContainer.View>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
|
|
212
|
+
<PinnedMessageToolboxOverlay
|
|
213
|
+
ref={toolboxRef}
|
|
214
|
+
currentUserId={currentUserId}
|
|
215
|
+
unpinMessage={handleUnpinMessage}
|
|
216
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
217
|
+
/>
|
|
218
|
+
</KContainer.View>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
PinnedMessagesSheetContent.displayName = 'PinnedMessagesSheetContent';
|
|
224
|
+
|
|
225
|
+
export interface OpenPinnedMessagesSheetParams {
|
|
226
|
+
currentUserId?: string;
|
|
227
|
+
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
228
|
+
customMessageItemFactory?: (item: MessageItem) => ReactNode;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export const openPinnedMessagesSheet = ({
|
|
232
|
+
currentUserId,
|
|
233
|
+
unpinMessage,
|
|
234
|
+
customMessageItemFactory,
|
|
235
|
+
}: OpenPinnedMessagesSheetParams) => {
|
|
236
|
+
UIUtils.bottomSheet.open({
|
|
237
|
+
header: {
|
|
238
|
+
title: { text: trans('chat:pinned_messages_title') },
|
|
239
|
+
alignment: 'left',
|
|
240
|
+
showCloseButton: true,
|
|
241
|
+
},
|
|
242
|
+
touchOutsideToDismiss: true,
|
|
243
|
+
body: {
|
|
244
|
+
renderContent: () => (
|
|
245
|
+
<PinnedMessagesSheetContent
|
|
246
|
+
currentUserId={currentUserId}
|
|
247
|
+
unpinMessage={unpinMessage}
|
|
248
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
249
|
+
/>
|
|
250
|
+
),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
};
|