@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
@@ -0,0 +1,153 @@
1
+ import { useCallback, useEffect } from 'react';
2
+ import OpenIMSDK from '@droppii/openim-rn-client-sdk';
3
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
4
+ import { useConversationStore } from '../../store/conversation';
5
+ import { useMessageStore } from '../../store/message';
6
+ import { usePinnedMessageStore } from '../../store/pinnedMessage';
7
+ import { useUserStore } from '../../store/user';
8
+ import { UIUtils } from '../../utils/UIUtils';
9
+ import { trans } from '../../translation';
10
+
11
+ const MAX_PINNED_MESSAGES = 5;
12
+
13
+ export const fetchPinnedMessages = async (conversationID: string) => {
14
+ const result = await OpenIMSDK.getPinnedMessageList({
15
+ conversationID,
16
+ offset: 0,
17
+ pageSize: MAX_PINNED_MESSAGES,
18
+ });
19
+
20
+ const previouslyPinnedIds = new Set(
21
+ usePinnedMessageStore.getState().messages.map((msg) => msg.clientMsgID)
22
+ );
23
+ const pinnedIds = new Set(result.messageList.map((msg) => msg.clientMsgID));
24
+
25
+ usePinnedMessageStore
26
+ .getState()
27
+ .setMessages(result.messageList, result.totalCount);
28
+
29
+ // Reconcile the per-message `isPinned` flag (used by the toolbox and the
30
+ // pinned badge) with the authoritative list from the server — messages
31
+ // pinned outside this session (before remount, or from another device)
32
+ // never get this flag set otherwise.
33
+ const { updateOneMessage } = useMessageStore.getState();
34
+ pinnedIds.forEach((clientMsgID) => {
35
+ updateOneMessage({ clientMsgID, isPinned: true });
36
+ });
37
+ previouslyPinnedIds.forEach((clientMsgID) => {
38
+ if (!pinnedIds.has(clientMsgID)) {
39
+ updateOneMessage({ clientMsgID, isPinned: false });
40
+ }
41
+ });
42
+ };
43
+
44
+ const showPinLimitReachedModal = () => {
45
+ UIUtils.alert.open({
46
+ title: trans('chat:pin_limit_title'),
47
+ message: trans('chat:pin_limit_desc'),
48
+ primaryIndex: 0,
49
+ buttons: [
50
+ {
51
+ label: trans('chat:pin_limit_primary'),
52
+ kind: 'primary',
53
+ onPress: () => {},
54
+ },
55
+ ],
56
+ });
57
+ };
58
+
59
+ export const usePinMessage = (conversationID?: string) => {
60
+ const pinnedMessages = usePinnedMessageStore((state) => state.messages);
61
+ const addMessage = usePinnedMessageStore((state) => state.addMessage);
62
+ const removeMessage = usePinnedMessageStore((state) => state.removeMessage);
63
+ const reset = usePinnedMessageStore((state) => state.reset);
64
+
65
+ useEffect(() => {
66
+ if (!conversationID) return;
67
+
68
+ fetchPinnedMessages(conversationID).catch((error) => {
69
+ console.error('fetchPinnedMessages', error);
70
+ });
71
+
72
+ return () => {
73
+ reset();
74
+ };
75
+ }, [conversationID, reset]);
76
+
77
+ const pinMessage = useCallback(
78
+ async (message: MessageItem) => {
79
+ if (!conversationID) return;
80
+
81
+ if (pinnedMessages.length >= MAX_PINNED_MESSAGES) {
82
+ showPinLimitReachedModal();
83
+ return;
84
+ }
85
+
86
+ try {
87
+ await OpenIMSDK.pinMsg(conversationID, message.clientMsgID);
88
+ addMessage(message);
89
+ useMessageStore.getState().updateOneMessage({
90
+ clientMsgID: message.clientMsgID,
91
+ isPinned: true,
92
+ pinnedByUserID: useUserStore.getState().user?.userID,
93
+ pinnedTime: Date.now(),
94
+ });
95
+ UIUtils.toast.open({
96
+ title: trans('chat:pin_message_success'),
97
+ theme: 'success',
98
+ });
99
+ } catch (error) {
100
+ console.error('pinMessage', error);
101
+ UIUtils.toast.open({
102
+ title: trans('chat:generic_error'),
103
+ theme: 'error',
104
+ });
105
+ }
106
+ },
107
+ [conversationID, pinnedMessages.length, addMessage]
108
+ );
109
+
110
+ const unpinMessage = useCallback(
111
+ async (message: MessageItem) => {
112
+ if (!conversationID) return;
113
+
114
+ try {
115
+ await OpenIMSDK.unpinMsg(conversationID, message.clientMsgID);
116
+ removeMessage(message.clientMsgID);
117
+ useMessageStore.getState().updateOneMessage({
118
+ clientMsgID: message.clientMsgID,
119
+ isPinned: false,
120
+ pinnedByUserID: undefined,
121
+ pinnedTime: undefined,
122
+ });
123
+ UIUtils.toast.open({
124
+ title: trans('chat:unpin_message_success'),
125
+ theme: 'success',
126
+ });
127
+ } catch (error) {
128
+ console.error('unpinMessage', error);
129
+ UIUtils.toast.open({
130
+ title: trans('chat:generic_error'),
131
+ theme: 'error',
132
+ });
133
+ }
134
+ },
135
+ [conversationID, removeMessage]
136
+ );
137
+
138
+ return {
139
+ pinnedMessages,
140
+ pinMessage,
141
+ unpinMessage,
142
+ };
143
+ };
144
+
145
+ export const syncPinnedMessagesOnEvent = (conversationID: string) => {
146
+ const currentConversationId =
147
+ useConversationStore.getState().currentConversationId;
148
+ if (conversationID !== currentConversationId) return;
149
+
150
+ fetchPinnedMessages(conversationID).catch((error) => {
151
+ console.error('syncPinnedMessagesOnEvent', error);
152
+ });
153
+ };
@@ -11,6 +11,7 @@ export const conversationQueryKeys = {
11
11
  details: () => [...conversationQueryKeys.all, 'detail'] as const,
12
12
  detail: (conversationId: string) =>
13
13
  [...conversationQueryKeys.details(), conversationId] as const,
14
+ getStatusBot: (botId: string) => ['getStatusBot', botId] as const,
14
15
  flow: (conversationId: string) => ['conversationFlow', conversationId],
15
16
  flowMainMenu: (conversationId: string) => [
16
17
  'conversationFlowMainMenu',
@@ -3,7 +3,7 @@ import { Platform, StyleSheet } from 'react-native';
3
3
  import { debounce } from 'lodash';
4
4
  import Reanimated from 'react-native-reanimated';
5
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
- import { KContainer, KColors, KImage, KButton } from '@droppii/libs';
6
+ import { KContainer, KColors, KImage, KButton, KLabel } from '@droppii/libs';
7
7
 
8
8
  const IS_IOS = Platform.OS === 'ios';
9
9
  const INPUT_LINE_HEIGHT = 22.4;
@@ -36,10 +36,15 @@ import { useChatCompose } from '../../hooks/useChatCompose';
36
36
  interface ChatComposerInternalProps {
37
37
  isEmptyMessage?: boolean;
38
38
  pannelActions?: DChatPannelAction[];
39
+ isDisableCompose?: boolean;
39
40
  }
40
41
 
41
42
  export const ChatComposer = memo(
42
- ({ isEmptyMessage = false, pannelActions }: ChatComposerInternalProps) => {
43
+ ({
44
+ isEmptyMessage = false,
45
+ pannelActions,
46
+ isDisableCompose = false,
47
+ }: ChatComposerInternalProps) => {
43
48
  const insets = useSafeAreaInsets();
44
49
 
45
50
  const { isBotConversation, conversationFlow } = useChatDetailContext();
@@ -171,6 +176,22 @@ export const ChatComposer = memo(
171
176
  [compose]
172
177
  );
173
178
 
179
+ if (isDisableCompose) {
180
+ return (
181
+ <KContainer.View row alignItems justifyContent>
182
+ <KContainer.View marginR="0.25rem">
183
+ <KImage.VectorIcons
184
+ name="info-circle-o"
185
+ size={16}
186
+ color={KColors.palette.gray.w600}
187
+ />
188
+ </KContainer.View>
189
+ <KLabel.Text typo="TextXsNormal">
190
+ Người dùng này không còn khả dụng
191
+ </KLabel.Text>
192
+ </KContainer.View>
193
+ );
194
+ }
174
195
  if (
175
196
  isBotConversation &&
176
197
  conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
@@ -6,6 +6,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
6
  import { KContainer } from '@droppii/libs';
7
7
  import { useNavigation } from '@react-navigation/native';
8
8
  import { useChatMessages } from '../../hooks/useChatMessages';
9
+ import { usePinMessage } from '../../hooks/message/usePinMessage';
10
+ import { PinnedMessageBar } from './PinnedMessageBar';
9
11
  import {
10
12
  MediaViewerModal,
11
13
  type MediaViewerModalHandle,
@@ -19,8 +21,12 @@ import type { ChatDetailProps } from './types';
19
21
  import { normalizeUrl } from '../../utils/url';
20
22
  import { MessageToolboxOverlay } from './messageToolbox/MessageToolboxOverlay';
21
23
  import type { MeasuredRect, MessageToolboxRef } from './messageToolbox/types';
22
- import type { DMessageItem } from '../../types/chat';
23
- import { useGetConversationDetail } from '../../hooks/conversation/useGetConversationDetail';
24
+ import { DBotStatusValue, type DMessageItem } from '../../types/chat';
25
+ import {
26
+ useGetConversationDetail,
27
+ useGetStatusConversationBot,
28
+ } from '../../hooks/conversation/useGetConversationDetail';
29
+ import { PeerType } from '@droppii/openim-rn-client-sdk';
24
30
 
25
31
  const ChatDetail = memo(
26
32
  ({
@@ -53,6 +59,27 @@ const ChatDetail = memo(
53
59
 
54
60
  const conversation = useConversation(conversationId);
55
61
 
62
+ const botId = useMemo(() => {
63
+ if (conversation?.peerType === PeerType.Bot) {
64
+ if (targetBotId) {
65
+ return targetBotId;
66
+ }
67
+ try {
68
+ const id = JSON.parse(conversation?.ex ?? '{}')?.sessionInfo?.data
69
+ ?.botId;
70
+ return id;
71
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
72
+ } catch (_: any) {
73
+ return undefined;
74
+ }
75
+ }
76
+ return undefined;
77
+ }, [conversation?.peerType, targetBotId, conversation?.ex]);
78
+
79
+ const { data: botInfo } = useGetStatusConversationBot(botId);
80
+
81
+ const { status: botStatus } = botInfo || {};
82
+
56
83
  const handleBackDefault = useCallback(() => {
57
84
  navigation.goBack();
58
85
  }, [navigation]);
@@ -76,6 +103,9 @@ const ChatDetail = memo(
76
103
  hasMoreEarlier,
77
104
  } = useChatMessages({ conversationId, enabled });
78
105
 
106
+ const { pinnedMessages, pinMessage, unpinMessage } =
107
+ usePinMessage(conversationId);
108
+
79
109
  useEffect(() => {
80
110
  if (conversationId) {
81
111
  useConversationStore.getState().setCurrentConversation(conversationId);
@@ -121,6 +151,11 @@ const ChatDetail = memo(
121
151
  onPressAvatar={onPressAvatar}
122
152
  />
123
153
 
154
+ <PinnedMessageBar
155
+ messages={pinnedMessages}
156
+ currentUserId={currentUserId}
157
+ />
158
+
124
159
  <KeyboardAvoidingView
125
160
  style={styles.keyboardAvoiding}
126
161
  behavior="padding"
@@ -142,6 +177,7 @@ const ChatDetail = memo(
142
177
  <ChatComposer
143
178
  isEmptyMessage={messages?.length === 0}
144
179
  pannelActions={pannelActions}
180
+ isDisableCompose={botStatus === DBotStatusValue.INACTIVE}
145
181
  />
146
182
  </KeyboardAvoidingView>
147
183
 
@@ -150,6 +186,8 @@ const ChatDetail = memo(
150
186
  ref={messageToolboxRef}
151
187
  currentUserId={currentUserId}
152
188
  customMessageItemFactory={customMessageItemFactory}
189
+ onPinMessage={pinMessage}
190
+ onUnpinMessage={unpinMessage}
153
191
  />
154
192
  </KContainer.Page>
155
193
  );
@@ -0,0 +1,177 @@
1
+ import { memo, useMemo } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import {
4
+ KContainer,
5
+ KImage,
6
+ KLabel,
7
+ KColors,
8
+ KSpacingValue,
9
+ KBadge,
10
+ } from '@droppii/libs';
11
+ import { trans } from '../../translation';
12
+ import {
13
+ getMessagePreview,
14
+ getPinnedMessagePreview,
15
+ } from '../../utils/messageUtils';
16
+ import type { DMessageItem } from '../../types/chat';
17
+
18
+ interface PinnedMessageBarProps {
19
+ messages: DMessageItem[];
20
+ currentUserId?: string;
21
+ }
22
+
23
+ const PinnedMessageThumbnail = memo(
24
+ ({ message }: { message: DMessageItem }) => {
25
+ const preview = useMemo(() => getPinnedMessagePreview(message), [message]);
26
+
27
+ if (preview.mediaType === 'image' && preview.thumbnailUrl) {
28
+ return (
29
+ <KImage.Base
30
+ uri={preview.thumbnailUrl}
31
+ style={styles.thumbnail}
32
+ resizeMode="cover"
33
+ />
34
+ );
35
+ }
36
+
37
+ if (preview.mediaType === 'video') {
38
+ return (
39
+ <KContainer.View style={styles.thumbnail}>
40
+ {preview.thumbnailUrl && (
41
+ <KImage.Base
42
+ uri={preview.thumbnailUrl}
43
+ style={styles.thumbnail}
44
+ resizeMode="cover"
45
+ />
46
+ )}
47
+ <KContainer.View style={styles.videoOverlay} center>
48
+ <KImage.VectorIcons
49
+ name="play-circle"
50
+ provider="MaterialCommunityIcons"
51
+ size={16}
52
+ color={KColors.white}
53
+ />
54
+ </KContainer.View>
55
+ </KContainer.View>
56
+ );
57
+ }
58
+
59
+ if (preview.mediaType === 'file' && preview.fileIcon) {
60
+ return (
61
+ <KContainer.View marginL="0.75rem">
62
+ <KImage.Base
63
+ uri={preview.fileIcon}
64
+ style={styles.fileIcon}
65
+ resizeMode="contain"
66
+ />
67
+ </KContainer.View>
68
+ );
69
+ }
70
+
71
+ return null;
72
+ }
73
+ );
74
+
75
+ PinnedMessageThumbnail.displayName = 'PinnedMessageThumbnail';
76
+
77
+ export const PinnedMessageBar = memo(
78
+ ({ messages, currentUserId }: PinnedMessageBarProps) => {
79
+ if (messages.length === 0) {
80
+ return null;
81
+ }
82
+
83
+ const latest = messages[0]!;
84
+ const sender =
85
+ latest.sendID === currentUserId ? 'Bạn' : latest.senderNickname || '';
86
+ const remainingCount = messages.length - 1;
87
+ const { subtitleKey } = getPinnedMessagePreview(latest);
88
+
89
+ return (
90
+ <KContainer.View style={styles.container}>
91
+ <PinnedMessageThumbnail message={latest} />
92
+
93
+ <KContainer.View row alignItems flex paddingR="0.75rem">
94
+ <KContainer.View style={styles.content}>
95
+ <KLabel.Text typo="TextNmMedium" numberOfLines={1}>
96
+ {getMessagePreview(latest)}
97
+ </KLabel.Text>
98
+
99
+ <KContainer.View row alignItems paddingR="0.75rem">
100
+ <KImage.VectorIcons
101
+ name="thumbtack-b"
102
+ size={12}
103
+ color={KColors.primary.normal}
104
+ />
105
+ <KLabel.Text
106
+ typo="TextSmNormal"
107
+ color={KColors.gray.light}
108
+ numberOfLines={1}
109
+ marginL={'0.25rem'}
110
+ >
111
+ {trans(subtitleKey, { sender })}
112
+ </KLabel.Text>
113
+ </KContainer.View>
114
+ </KContainer.View>
115
+
116
+ {remainingCount > 0 && (
117
+ <KBadge.BaseV2
118
+ badge={`+${remainingCount}`}
119
+ skipConvertBadge
120
+ badgeSize={{ width: 26, height: 20 }}
121
+ wrapperLabelTextStyle={{ paddingHorizontal: 3 }}
122
+ />
123
+ )}
124
+ <KImage.VectorIcons
125
+ name="angle-down-o"
126
+ size={20}
127
+ color={KColors.gray.normal}
128
+ />
129
+ </KContainer.View>
130
+ </KContainer.View>
131
+ );
132
+ }
133
+ );
134
+
135
+ PinnedMessageBar.displayName = 'PinnedMessageBar';
136
+
137
+ const styles = StyleSheet.create({
138
+ container: {
139
+ flexDirection: 'row',
140
+ alignItems: 'center',
141
+ backgroundColor: KColors.palette.gray.w25,
142
+ },
143
+ content: {
144
+ flex: 1,
145
+ paddingRight: KSpacingValue['0.75rem'],
146
+ paddingLeft: KSpacingValue['0.75rem'],
147
+ paddingVertical: KSpacingValue['0.25rem'],
148
+ },
149
+ thumbnail: {
150
+ width: 51,
151
+ height: 51,
152
+ overflow: 'hidden',
153
+ backgroundColor: '#000',
154
+ },
155
+ videoOverlay: {
156
+ position: 'absolute',
157
+ top: 0,
158
+ left: 0,
159
+ right: 0,
160
+ bottom: 0,
161
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
162
+ },
163
+ fileIcon: {
164
+ width: 24,
165
+ height: 24,
166
+ },
167
+ badge: {
168
+ minWidth: 32,
169
+ height: 22,
170
+ paddingHorizontal: KSpacingValue['0.25rem'],
171
+ borderRadius: KSpacingValue['1rem'],
172
+ alignItems: 'center',
173
+ justifyContent: 'center',
174
+ backgroundColor: KColors.primary.normal,
175
+ marginRight: KSpacingValue['0.75rem'],
176
+ },
177
+ });
@@ -1,4 +1,5 @@
1
- import { KContainer } from '@droppii/libs';
1
+ import { KContainer, KImage, KColors } from '@droppii/libs';
2
+ import { StyleSheet } from 'react-native';
2
3
  import { memo, useMemo } from 'react';
3
4
  import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
4
5
  import { MessageType } from '@droppii/openim-rn-client-sdk';
@@ -89,15 +90,27 @@ export const LegendChatMessage = memo(
89
90
  isOutgoing={isOutgoing}
90
91
  onRetry={onRetry}
91
92
  />
92
- <LongPressWrapper onLongPress={onLongPress}>
93
- <MessageComponent
94
- message={message}
95
- isOutgoing={isOutgoing}
96
- createdAtTime={createdAtTime}
97
- onMediaPress={hasMediaPress ? onMediaPress : undefined}
98
- onPressUrl={hasUrlPress ? onPressUrl : undefined}
99
- />
100
- </LongPressWrapper>
93
+ <KContainer.View style={styles.content}>
94
+ <LongPressWrapper onLongPress={onLongPress}>
95
+ <MessageComponent
96
+ message={message}
97
+ isOutgoing={isOutgoing}
98
+ createdAtTime={createdAtTime}
99
+ onMediaPress={hasMediaPress ? onMediaPress : undefined}
100
+ onPressUrl={hasUrlPress ? onPressUrl : undefined}
101
+ />
102
+ </LongPressWrapper>
103
+
104
+ {message?.isPinned && (
105
+ <KContainer.View style={styles.pinBadge}>
106
+ <KImage.VectorIcons
107
+ name="thumbtack-b"
108
+ size={12}
109
+ color={KColors.primary.normal}
110
+ />
111
+ </KContainer.View>
112
+ )}
113
+ </KContainer.View>
101
114
  </KContainer.View>
102
115
  );
103
116
  }
@@ -105,6 +118,30 @@ export const LegendChatMessage = memo(
105
118
 
106
119
  LegendChatMessage.displayName = 'LegendChatMessage';
107
120
 
121
+ const styles = StyleSheet.create({
122
+ content: {
123
+ position: 'relative',
124
+ },
125
+ pinBadge: {
126
+ position: 'absolute',
127
+ top: -8,
128
+ right: -8,
129
+ width: 24,
130
+ height: 20,
131
+ borderRadius: 100,
132
+ alignItems: 'center',
133
+ justifyContent: 'center',
134
+ backgroundColor: KColors.pageBackground,
135
+ shadowColor: KColors.black,
136
+ shadowOffset: { width: 0, height: 1 },
137
+ shadowOpacity: 0.15,
138
+ shadowRadius: 2,
139
+ elevation: 3,
140
+ borderWidth: 1,
141
+ borderColor: KColors.white,
142
+ },
143
+ });
144
+
108
145
  const LongPressWrapper = ({
109
146
  children,
110
147
  onLongPress,
@@ -7,10 +7,14 @@ import { MessageToolboxItem } from './MessageToolboxItem';
7
7
  interface MessageToolboxMenuProps {
8
8
  showCopyMessage?: boolean;
9
9
  showCopyMessageLink?: boolean;
10
+ showPinMessage?: boolean;
11
+ showUnpinMessage?: boolean;
10
12
  showRetryMessage?: boolean;
11
13
  showRevokeMessage?: boolean;
12
14
  onCopyMessage: () => void;
13
15
  onCopyMessageLink: () => void;
16
+ onPinMessage: () => void;
17
+ onUnpinMessage: () => void;
14
18
  onRetryMessage: () => void;
15
19
  onRevokeMessage: () => void;
16
20
  }
@@ -19,10 +23,14 @@ export const MessageToolboxMenu = memo(
19
23
  ({
20
24
  showCopyMessage,
21
25
  showCopyMessageLink,
26
+ showPinMessage,
27
+ showUnpinMessage,
22
28
  showRetryMessage,
23
29
  showRevokeMessage,
24
30
  onCopyMessage,
25
31
  onCopyMessageLink,
32
+ onPinMessage,
33
+ onUnpinMessage,
26
34
  onRetryMessage,
27
35
  onRevokeMessage,
28
36
  }: MessageToolboxMenuProps) => {
@@ -44,6 +52,22 @@ export const MessageToolboxMenu = memo(
44
52
  onPress={onCopyMessageLink}
45
53
  />
46
54
 
55
+ <MessageToolboxItem
56
+ icon="thumbtack-o"
57
+ color={KColors.gray.dark}
58
+ label={trans('chat:pin_message')}
59
+ visible={showPinMessage}
60
+ onPress={onPinMessage}
61
+ />
62
+
63
+ <MessageToolboxItem
64
+ icon="thumbtack-off-o"
65
+ color={KColors.gray.dark}
66
+ label={trans('chat:unpin_message')}
67
+ visible={showUnpinMessage}
68
+ onPress={onUnpinMessage}
69
+ />
70
+
47
71
  <MessageToolboxItem
48
72
  icon="arrow-reload-o"
49
73
  color={KColors.danger.normal}