@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.
- package/lib/module/core/useChatListener.js +16 -1
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/conversation/useGetConversationDetail.js +13 -0
- package/lib/module/hooks/conversation/useGetConversationDetail.js.map +1 -1
- package/lib/module/hooks/message/usePinMessage.js +133 -0
- package/lib/module/hooks/message/usePinMessage.js.map +1 -0
- package/lib/module/hooks/query-keys.js +1 -0
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +21 -2
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +39 -3
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessageBar.js +158 -0
- package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -10
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +16 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +15 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +16 -1
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -1
- package/lib/module/services/endpoints.js +2 -1
- package/lib/module/services/endpoints.js.map +1 -1
- package/lib/module/store/pinnedMessage.js +34 -0
- package/lib/module/store/pinnedMessage.js.map +1 -0
- package/lib/module/translation/resources/i18n.js +12 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js +9 -0
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/utils/messageUtils.js +76 -0
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts +2 -0
- package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/usePinMessage.d.ts +9 -0
- package/lib/typescript/src/hooks/message/usePinMessage.d.ts.map +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts +8 -0
- package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +5 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +2 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +4 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -1
- package/lib/typescript/src/services/endpoints.d.ts +1 -0
- package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
- package/lib/typescript/src/store/pinnedMessage.d.ts +11 -0
- package/lib/typescript/src/store/pinnedMessage.d.ts.map +1 -0
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +29 -1
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/utils/messageUtils.d.ts +8 -0
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/core/useChatListener.ts +26 -0
- package/src/hooks/conversation/useGetConversationDetail.ts +16 -0
- package/src/hooks/message/usePinMessage.ts +153 -0
- package/src/hooks/query-keys.ts +1 -0
- package/src/screens/chat-detail/ChatComposer.tsx +23 -2
- package/src/screens/chat-detail/ChatDetail.tsx +40 -2
- package/src/screens/chat-detail/PinnedMessageBar.tsx +177 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +47 -10
- package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +24 -0
- package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +142 -116
- package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +31 -0
- package/src/services/endpoints.ts +2 -0
- package/src/store/pinnedMessage.ts +38 -0
- package/src/translation/resources/i18n.ts +13 -0
- package/src/types/chat.ts +33 -1
- 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
|
+
};
|
package/src/hooks/query-keys.ts
CHANGED
|
@@ -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
|
-
({
|
|
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
|
|
23
|
-
import {
|
|
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
|
-
<
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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}
|