@droppii-org/chat-mobile 0.2.32 → 0.2.34

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 (77) hide show
  1. package/lib/module/core/useChatListener.js +16 -1
  2. package/lib/module/core/useChatListener.js.map +1 -1
  3. package/lib/module/hooks/conversation/useGetConversationDetail.js +13 -0
  4. package/lib/module/hooks/conversation/useGetConversationDetail.js.map +1 -1
  5. package/lib/module/hooks/message/usePinMessage.js +133 -0
  6. package/lib/module/hooks/message/usePinMessage.js.map +1 -0
  7. package/lib/module/hooks/query-keys.js +1 -0
  8. package/lib/module/hooks/query-keys.js.map +1 -1
  9. package/lib/module/screens/chat-detail/ChatComposer.js +21 -2
  10. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  11. package/lib/module/screens/chat-detail/ChatDetail.js +39 -3
  12. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  13. package/lib/module/screens/chat-detail/PinnedMessageBar.js +158 -0
  14. package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -0
  15. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -10
  16. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  17. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +16 -0
  18. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -1
  19. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +15 -1
  20. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
  21. package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +16 -1
  22. package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -1
  23. package/lib/module/services/endpoints.js +2 -1
  24. package/lib/module/services/endpoints.js.map +1 -1
  25. package/lib/module/store/pinnedMessage.js +34 -0
  26. package/lib/module/store/pinnedMessage.js.map +1 -0
  27. package/lib/module/translation/resources/i18n.js +12 -0
  28. package/lib/module/translation/resources/i18n.js.map +1 -1
  29. package/lib/module/types/chat.js +9 -0
  30. package/lib/module/types/chat.js.map +1 -1
  31. package/lib/module/utils/messageUtils.js +76 -0
  32. package/lib/module/utils/messageUtils.js.map +1 -1
  33. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  34. package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts +2 -0
  35. package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts.map +1 -1
  36. package/lib/typescript/src/hooks/message/usePinMessage.d.ts +9 -0
  37. package/lib/typescript/src/hooks/message/usePinMessage.d.ts.map +1 -0
  38. package/lib/typescript/src/hooks/query-keys.d.ts +1 -0
  39. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  40. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +2 -1
  41. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  42. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  43. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts +8 -0
  44. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -0
  45. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  46. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +5 -1
  47. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -1
  48. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +2 -0
  49. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
  50. package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +4 -0
  51. package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -1
  52. package/lib/typescript/src/services/endpoints.d.ts +1 -0
  53. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  54. package/lib/typescript/src/store/pinnedMessage.d.ts +11 -0
  55. package/lib/typescript/src/store/pinnedMessage.d.ts.map +1 -0
  56. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  57. package/lib/typescript/src/types/chat.d.ts +29 -1
  58. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  59. package/lib/typescript/src/utils/messageUtils.d.ts +8 -0
  60. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  61. package/package.json +2 -2
  62. package/src/core/useChatListener.ts +26 -0
  63. package/src/hooks/conversation/useGetConversationDetail.ts +16 -0
  64. package/src/hooks/message/usePinMessage.ts +153 -0
  65. package/src/hooks/query-keys.ts +1 -0
  66. package/src/screens/chat-detail/ChatComposer.tsx +23 -2
  67. package/src/screens/chat-detail/ChatDetail.tsx +40 -2
  68. package/src/screens/chat-detail/PinnedMessageBar.tsx +177 -0
  69. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +47 -10
  70. package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +24 -0
  71. package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +142 -116
  72. package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +31 -0
  73. package/src/services/endpoints.ts +2 -0
  74. package/src/store/pinnedMessage.ts +38 -0
  75. package/src/translation/resources/i18n.ts +13 -0
  76. package/src/types/chat.ts +33 -1
  77. 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
- >(({ currentUserId = '', customMessageItemFactory }, ref) => {
33
- const [visible, setVisible] = useState(false);
34
- const [message, setMessage] = useState<DMessageItem | undefined>(undefined);
35
- const [rect, setRect] = useState<MeasuredRect | null>(null);
36
- const toolboxItems = useMemo(
37
- () => getMessageToolboxItems(message, currentUserId),
38
- [message, currentUserId]
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
- [currentUserId]
62
- );
63
-
64
- useImperativeHandle(ref, () => ({ show, hide }), [show, hide]);
65
-
66
- const handleCopyMessage = useCallback(() => {
67
- if (!message) return;
68
- const text = getCopyableMessageText(message);
69
- if (text) {
70
- Clipboard.setString(text);
71
- UIUtils.toast.open({
72
- title: trans('chat:copy_message_success'),
73
- theme: 'success',
74
- });
75
- }
76
- hide();
77
- }, [message, hide]);
78
-
79
- const handleCopyMessageLink = useCallback(() => {
80
- if (!message) return;
81
- // UI-only for now: real shareable deeplink format (BR-001) is not decided yet.
82
- const placeholderLink = `${message.recvID || message.groupID}:${message.clientMsgID ?? message.serverMsgID ?? ''}`;
83
- Clipboard.setString(placeholderLink);
84
- UIUtils.toast.open({
85
- title: trans('chat:copy_message_link_success'),
86
- theme: 'success',
87
- });
88
- hide();
89
- }, [message, hide]);
90
-
91
- const handleRetryMessage = useCallback(() => {
92
- if (!message) return;
93
- retryMessage(message);
94
- hide();
95
- }, [message, hide, retryMessage]);
96
-
97
- const handleRevokeMessage = useCallback(async () => {
98
- if (!message) return;
99
- hide();
100
- try {
101
- await revokeMessage(message);
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('msg_type_revoke'),
96
+ title: trans('chat:copy_message_link_success'),
104
97
  theme: 'success',
105
98
  });
106
- } catch (error: any) {
107
- console.error('revokeMessage', error);
108
- UIUtils.toast.open({
109
- title: trans('chat:generic_error'),
110
- theme: 'error',
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
- if (!visible || !message || !rect) {
116
- return null;
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
  };
@@ -26,6 +26,8 @@ export const ENDPOINTS = {
26
26
  `${getApiEndpoint('bot')}/${botId}/conversation`,
27
27
  conversationFlowMainMenu: (id: string) =>
28
28
  `${getApiEndpoint('conversationFlow')}/${id}/bot-flows/main-menu`,
29
+ getConversationStatusByBotId: (botId: string) =>
30
+ `${getApiEndpoint('bot')}/${botId}`,
29
31
  },
30
32
  uploaderService: {
31
33
  get uploadImage() {
@@ -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
@@ -68,6 +68,35 @@ export enum DFlowState {
68
68
  CS = 2,
69
69
  }
70
70
 
71
+ export interface DBotAvatar {
72
+ streamId: string;
73
+ containerName: string;
74
+ contentType: string;
75
+ baseUrl: string;
76
+ fullUrl: string;
77
+ }
78
+
79
+ export enum DBotStatusValue {
80
+ ACTIVE = 'ACTIVE',
81
+ INACTIVE = 'INACTIVE',
82
+ }
83
+
84
+ export enum DBotType {
85
+ CS_TEAM = 'CS_TEAM',
86
+ }
87
+
88
+ export interface DBotStatus {
89
+ id: string;
90
+ name: string;
91
+ avatar: DBotAvatar | null;
92
+ applicationType: string;
93
+ status: `${DBotStatusValue}`;
94
+ type: DBotType;
95
+ botFlow: unknown | null;
96
+ team: unknown | null;
97
+ botSupportPlatform: unknown | null;
98
+ }
99
+
71
100
  export interface DConversationPeerUser {
72
101
  id: string;
73
102
  username?: string;
@@ -94,6 +123,9 @@ export interface DMessageItem extends MessageItem {
94
123
  atUserIDList?: unknown;
95
124
  attachedInfo?: 'null';
96
125
  options?: DMessageOption;
126
+ isPinned?: boolean;
127
+ pinnedByUserID?: string;
128
+ pinnedTime?: number;
97
129
  }
98
130
 
99
131
  export interface DMessageOption {
@@ -141,7 +173,7 @@ export interface MessageStore {
141
173
  setMessages: (messages: DMessageItem[], hasMore: boolean) => void;
142
174
  pushNewMessage: (message: MessageItem) => void;
143
175
  updateOneMessage: (
144
- message: Partial<MessageItem> & Pick<MessageItem, 'clientMsgID'>
176
+ message: Partial<DMessageItem> & Pick<MessageItem, 'clientMsgID'>
145
177
  ) => void;
146
178
  deleteOneMessage: (clientMsgID: string) => void;
147
179
  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,