@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.
Files changed (49) hide show
  1. package/lib/module/core/useChatListener.js +3 -10
  2. package/lib/module/core/useChatListener.js.map +1 -1
  3. package/lib/module/hooks/message/useSendMessage.js +0 -3
  4. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  5. package/lib/module/screens/chat-detail/PinnedMessageBar.js +4 -1
  6. package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -1
  7. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +80 -110
  8. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
  9. package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js +179 -0
  10. package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js.map +1 -0
  11. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +18 -6
  12. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  13. package/lib/module/store/message.js +33 -0
  14. package/lib/module/store/message.js.map +1 -1
  15. package/lib/module/translation/resources/i18n.js +3 -2
  16. package/lib/module/translation/resources/i18n.js.map +1 -1
  17. package/lib/module/utils/UIUtils.js +4 -2
  18. package/lib/module/utils/UIUtils.js.map +1 -1
  19. package/lib/module/utils/toastFlashMessage.js +4 -1
  20. package/lib/module/utils/toastFlashMessage.js.map +1 -1
  21. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  22. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  23. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts +1 -1
  24. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -1
  25. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts +1 -1
  26. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
  27. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheetBackup.d.ts +9 -0
  28. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheetBackup.d.ts.map +1 -0
  29. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
  30. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  31. package/lib/typescript/src/store/message.d.ts.map +1 -1
  32. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  33. package/lib/typescript/src/types/chat.d.ts +1 -0
  34. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -1
  36. package/lib/typescript/src/utils/toastFlashMessage.d.ts +1 -0
  37. package/lib/typescript/src/utils/toastFlashMessage.d.ts.map +1 -1
  38. package/package.json +4 -2
  39. package/src/core/useChatListener.ts +3 -18
  40. package/src/hooks/message/useSendMessage.ts +0 -2
  41. package/src/screens/chat-detail/PinnedMessageBar.tsx +17 -8
  42. package/src/screens/chat-detail/PinnedMessagesSheet.tsx +118 -153
  43. package/src/screens/chat-detail/PinnedMessagesSheetBackup.tsx +253 -0
  44. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +26 -13
  45. package/src/store/message.ts +34 -0
  46. package/src/translation/resources/i18n.ts +4 -2
  47. package/src/types/chat.ts +1 -1
  48. package/src/utils/UIUtils.ts +3 -1
  49. 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, truncateMiddle } from '../utils/messageUtils';
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
- messageForToast,
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);
@@ -207,8 +207,6 @@ export const useSendMessage = () => {
207
207
  clientMsgID: message.clientMsgID,
208
208
  });
209
209
 
210
- // TODO: Handle revoked quoted message
211
-
212
210
  markMessageRevoked(message.clientMsgID);
213
211
  }, []);
214
212
 
@@ -13,7 +13,13 @@ import {
13
13
  getMessagePreviewParts,
14
14
  getPinnedMessagePreview,
15
15
  } from '../../utils/messageUtils';
16
- import type { MessageItem } from '@droppii/openim-rn-client-sdk';
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
- <KLabel.Text
100
- typo="TextNmMedium"
101
- numberOfLines={1}
102
- {...messagePreview.textProps}
103
- >
104
- {messagePreview.text}
105
- </KLabel.Text>
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 { 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';
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, 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
- });
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
- const toolboxRef = useRef<PinnedMessageToolboxRef>(null);
140
- const rowRefsMap = useRef<Map<string, View | null>>(new Map());
141
-
142
- const handleLongPress = useCallback(
143
- (messageId: string, message: MessageItem) => {
144
- const node = rowRefsMap.current.get(messageId);
145
- node?.measureInWindow((x, y, width, height) => {
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 timeLabel = formatMessageTime(createdAtTime);
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
- ref={(node: View | null) => {
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
- <LegendChatMessage
183
- message={message}
184
- isOutgoing={isOutgoing}
185
- createdAtTime={createdAtTime}
186
- onLongPress={() => handleLongPress(messageId, message)}
187
- customMessageItemFactory={customMessageItemFactory}
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
+ };