@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.
Files changed (52) hide show
  1. package/lib/module/hooks/useFileAttachment.js +3 -1
  2. package/lib/module/hooks/useFileAttachment.js.map +1 -1
  3. package/lib/module/hooks/useFileDownload.js +19 -17
  4. package/lib/module/hooks/useFileDownload.js.map +1 -1
  5. package/lib/module/hooks/useMuteConversation.js +4 -6
  6. package/lib/module/hooks/useMuteConversation.js.map +1 -1
  7. package/lib/module/screens/chat-detail/ChatDetail.js +10 -1
  8. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  9. package/lib/module/screens/chat-detail/PinnedMessageBar.js +8 -3
  10. package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -1
  11. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +173 -0
  12. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -0
  13. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +9 -1
  14. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -1
  15. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +1 -0
  16. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
  17. package/lib/module/store/conversation.js +11 -0
  18. package/lib/module/store/conversation.js.map +1 -1
  19. package/lib/module/translation/resources/i18n.js +4 -1
  20. package/lib/module/translation/resources/i18n.js.map +1 -1
  21. package/lib/module/types/chat.js.map +1 -1
  22. package/lib/module/utils/UIUtils.js +6 -0
  23. package/lib/module/utils/UIUtils.js.map +1 -1
  24. package/lib/typescript/src/hooks/useFileAttachment.d.ts.map +1 -1
  25. package/lib/typescript/src/hooks/useFileDownload.d.ts.map +1 -1
  26. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  27. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts +2 -1
  28. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -1
  29. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts +10 -0
  30. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -0
  31. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +9 -7
  32. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -1
  33. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
  34. package/lib/typescript/src/store/conversation.d.ts +1 -0
  35. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  36. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  37. package/lib/typescript/src/types/chat.d.ts +3 -0
  38. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  39. package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/hooks/useFileAttachment.ts +3 -1
  42. package/src/hooks/useFileDownload.ts +19 -18
  43. package/src/hooks/useMuteConversation.ts +6 -6
  44. package/src/screens/chat-detail/ChatDetail.tsx +10 -0
  45. package/src/screens/chat-detail/PinnedMessageBar.tsx +9 -3
  46. package/src/screens/chat-detail/PinnedMessagesSheet.tsx +244 -0
  47. package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +18 -6
  48. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +1 -0
  49. package/src/store/conversation.ts +12 -0
  50. package/src/translation/resources/i18n.ts +6 -1
  51. package/src/types/chat.ts +1 -0
  52. 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
- onCopyMessage: () => void;
15
- onCopyMessageLink: () => void;
16
- onPinMessage: () => void;
17
- onUnpinMessage: () => void;
18
- onRetryMessage: () => void;
19
- onRevokeMessage: () => void;
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}
@@ -74,6 +74,7 @@ export const MessageToolboxWrapper = memo(
74
74
  const containerStyles = useAnimatedStyle(() => ({
75
75
  top: Math.max(rect.y, insets.top) + topOffset,
76
76
  left: rect.x,
77
+ width: rect.width,
77
78
  gap: MENU_GAP,
78
79
  transform: [{ translateY: translateY.value }],
79
80
  }));
@@ -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': 'Tệp quá lớn. Tối đa 10MB',
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
@@ -28,6 +28,7 @@ export interface ChatUIAdapter {
28
28
  toast?: { open: (params: any) => void };
29
29
  alert?: { open: (params: any) => void };
30
30
  popup?: { open: (params: any) => void };
31
+ bottomSheet?: { open: (params: any) => void };
31
32
  }
32
33
 
33
34
  export interface ChatContextType {
@@ -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
  };