@droppii-org/chat-mobile 0.2.44 → 0.2.46
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/index.js +2 -0
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/core/useChatListener.js +7 -2
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/core/useNetworkStatusSync.js +14 -0
- package/lib/module/core/useNetworkStatusSync.js.map +1 -0
- package/lib/module/hooks/message/usePinMessage.js +12 -8
- package/lib/module/hooks/message/usePinMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +87 -69
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +39 -34
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +16 -8
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +7 -65
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +6 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/store/pinnedMessage.js +10 -5
- package/lib/module/store/pinnedMessage.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +1 -1
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/typescript/src/core/index.d.ts.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/core/useNetworkStatusSync.d.ts +2 -0
- package/lib/typescript/src/core/useNetworkStatusSync.d.ts.map +1 -0
- package/lib/typescript/src/hooks/message/usePinMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +5 -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/ChatListLegend.d.ts +15 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +2 -3
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
- package/lib/typescript/src/store/pinnedMessage.d.ts +2 -1
- package/lib/typescript/src/store/pinnedMessage.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/core/index.ts +2 -0
- package/src/core/useChatListener.ts +14 -1
- package/src/core/useNetworkStatusSync.ts +13 -0
- package/src/hooks/message/usePinMessage.ts +16 -8
- package/src/screens/chat-detail/ChatComposer.tsx +132 -99
- package/src/screens/chat-detail/ChatDetail.tsx +41 -37
- package/src/screens/chat-detail/ChatListLegend.tsx +36 -7
- package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +11 -106
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +6 -1
- package/src/store/pinnedMessage.ts +20 -6
- package/src/translation/resources/i18n.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@droppii-org/chat-mobile",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.46",
|
|
4
4
|
"description": "Droppii chat mobile",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"@growthbook/growthbook-react": "*",
|
|
86
86
|
"@jest/globals": "^30.0.0",
|
|
87
87
|
"@react-native-clipboard/clipboard": "^1.16.3",
|
|
88
|
+
"@react-native-community/netinfo": "11.4.1",
|
|
88
89
|
"@react-native-documents/picker": "^12.0.1",
|
|
89
90
|
"@react-native/babel-preset": "0.85.0",
|
|
90
91
|
"@react-native/eslint-config": "0.85.0",
|
|
@@ -117,7 +118,7 @@
|
|
|
117
118
|
"react-native-file-viewer": "^2.1.5",
|
|
118
119
|
"react-native-gesture-handler": "^3.0.0",
|
|
119
120
|
"react-native-image-crop-picker": "^0.51.1",
|
|
120
|
-
"react-native-keyboard-controller": "1.
|
|
121
|
+
"react-native-keyboard-controller": "^1.21.9",
|
|
121
122
|
"react-native-linear-gradient": "^2.8.3",
|
|
122
123
|
"react-native-mmkv": "4.0.0",
|
|
123
124
|
"react-native-permissions": "^5.6.0",
|
|
@@ -139,6 +140,7 @@
|
|
|
139
140
|
"peerDependencies": {
|
|
140
141
|
"@droppii/libs": "*",
|
|
141
142
|
"@react-native-clipboard/clipboard": "*",
|
|
143
|
+
"@react-native-community/netinfo": "*",
|
|
142
144
|
"@react-native-documents/picker": "*",
|
|
143
145
|
"@tanstack/react-query": "*",
|
|
144
146
|
"react": "*",
|
package/src/core/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { initBaseUrl } from '../config/configuration';
|
|
|
4
4
|
import { useUserListener } from './useUserListener';
|
|
5
5
|
import { useChatListener } from './useChatListener';
|
|
6
6
|
import { useBackgroundStatusSync } from './notification/useBackgroundStatusSync';
|
|
7
|
+
import { useNetworkStatusSync } from './useNetworkStatusSync';
|
|
7
8
|
|
|
8
9
|
export * from './notification';
|
|
9
10
|
|
|
@@ -35,4 +36,5 @@ export const useChatSdk = () => {
|
|
|
35
36
|
useUserListener();
|
|
36
37
|
useChatListener();
|
|
37
38
|
useBackgroundStatusSync();
|
|
39
|
+
useNetworkStatusSync();
|
|
38
40
|
};
|
|
@@ -70,8 +70,8 @@ export const useChatListener = () => {
|
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
-
useConversationStore.getState().updateConversations(conversations);
|
|
74
73
|
}
|
|
74
|
+
useConversationStore.getState().updateConversations(conversations);
|
|
75
75
|
},
|
|
76
76
|
[currentUserId, isShowFLashMessage, onPressFlashMessage]
|
|
77
77
|
);
|
|
@@ -134,6 +134,10 @@ export const useChatListener = () => {
|
|
|
134
134
|
relevant.forEach((msg) => useMessageStore.getState().pushNewMessage(msg));
|
|
135
135
|
}, []);
|
|
136
136
|
|
|
137
|
+
const onRecvOfflineNewMessages = useCallback((messages: MessageItem[]) => {
|
|
138
|
+
console.log('[Listener] onRecvOfflineNewMessages', messages);
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
137
141
|
useEffect(() => {
|
|
138
142
|
// Conversation
|
|
139
143
|
OpenIMSDK.on(OpenIMEvent.OnConversationChanged, onConversationChanged);
|
|
@@ -148,6 +152,10 @@ export const useChatListener = () => {
|
|
|
148
152
|
OpenIMSDK.on(OpenIMEvent.OnNewRecvMessageRevoked, onMessageRevoked);
|
|
149
153
|
OpenIMSDK.on(OpenIMEvent.OnRecvMessagePinned, onMessagePinned);
|
|
150
154
|
OpenIMSDK.on(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
|
|
155
|
+
OpenIMSDK.on(
|
|
156
|
+
OpenIMEvent.OnRecvOfflineNewMessages,
|
|
157
|
+
onRecvOfflineNewMessages
|
|
158
|
+
);
|
|
151
159
|
|
|
152
160
|
return () => {
|
|
153
161
|
// Conversation
|
|
@@ -163,6 +171,10 @@ export const useChatListener = () => {
|
|
|
163
171
|
OpenIMSDK.off(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
|
|
164
172
|
OpenIMSDK.off(OpenIMEvent.OnNewRecvMessageRevoked, onMessageRevoked);
|
|
165
173
|
OpenIMSDK.off(OpenIMEvent.OnRecvMessagePinned, onMessagePinned);
|
|
174
|
+
OpenIMSDK.off(
|
|
175
|
+
OpenIMEvent.OnRecvOfflineNewMessages,
|
|
176
|
+
onRecvOfflineNewMessages
|
|
177
|
+
);
|
|
166
178
|
};
|
|
167
179
|
}, [
|
|
168
180
|
onConversationChanged,
|
|
@@ -173,5 +185,6 @@ export const useChatListener = () => {
|
|
|
173
185
|
onNewConversation,
|
|
174
186
|
onSendMessageProgress,
|
|
175
187
|
onRecvNewMessages,
|
|
188
|
+
onRecvOfflineNewMessages,
|
|
176
189
|
]);
|
|
177
190
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import NetInfo from '@react-native-community/netinfo';
|
|
3
|
+
import OpenIMSDK from '@droppii/openim-rn-client-sdk';
|
|
4
|
+
|
|
5
|
+
export const useNetworkStatusSync = () => {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const unsubscribe = NetInfo.addEventListener(() => {
|
|
8
|
+
OpenIMSDK.networkStatusChanged();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return unsubscribe;
|
|
12
|
+
}, []);
|
|
13
|
+
};
|
|
@@ -8,13 +8,13 @@ import { useUserStore } from '../../store/user';
|
|
|
8
8
|
import { UIUtils } from '../../utils/UIUtils';
|
|
9
9
|
import { trans } from '../../translation';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const PINNED_MESSAGES_PAGE_SIZE = 5;
|
|
12
12
|
|
|
13
13
|
export const fetchPinnedMessages = async (conversationID: string) => {
|
|
14
14
|
const result = await OpenIMSDK.getPinnedMessageList({
|
|
15
15
|
conversationID,
|
|
16
16
|
offset: 0,
|
|
17
|
-
pageSize:
|
|
17
|
+
pageSize: PINNED_MESSAGES_PAGE_SIZE,
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const previouslyPinnedIds = new Set(
|
|
@@ -24,7 +24,11 @@ export const fetchPinnedMessages = async (conversationID: string) => {
|
|
|
24
24
|
|
|
25
25
|
usePinnedMessageStore
|
|
26
26
|
.getState()
|
|
27
|
-
.setMessages(
|
|
27
|
+
.setMessages(
|
|
28
|
+
result.messageList,
|
|
29
|
+
result.totalCount,
|
|
30
|
+
result.remainingPinCount
|
|
31
|
+
);
|
|
28
32
|
|
|
29
33
|
// Reconcile the per-message `isPinned` flag (used by the toolbox and the
|
|
30
34
|
// pinned badge) with the authoritative list from the server — messages
|
|
@@ -41,10 +45,10 @@ export const fetchPinnedMessages = async (conversationID: string) => {
|
|
|
41
45
|
});
|
|
42
46
|
};
|
|
43
47
|
|
|
44
|
-
const showPinLimitReachedModal = () => {
|
|
48
|
+
const showPinLimitReachedModal = (maxCount: number) => {
|
|
45
49
|
UIUtils.alert.open({
|
|
46
50
|
title: trans('chat:pin_limit_title'),
|
|
47
|
-
message: trans('chat:pin_limit_desc'),
|
|
51
|
+
message: trans('chat:pin_limit_desc', { maxCount }),
|
|
48
52
|
primaryIndex: 0,
|
|
49
53
|
buttons: [
|
|
50
54
|
{
|
|
@@ -58,6 +62,10 @@ const showPinLimitReachedModal = () => {
|
|
|
58
62
|
|
|
59
63
|
export const usePinMessage = (conversationID?: string) => {
|
|
60
64
|
const pinnedMessages = usePinnedMessageStore((state) => state.messages);
|
|
65
|
+
const totalCount = usePinnedMessageStore((state) => state.totalCount);
|
|
66
|
+
const remainingPinCount = usePinnedMessageStore(
|
|
67
|
+
(state) => state.remainingPinCount
|
|
68
|
+
);
|
|
61
69
|
const addMessage = usePinnedMessageStore((state) => state.addMessage);
|
|
62
70
|
const removeMessage = usePinnedMessageStore((state) => state.removeMessage);
|
|
63
71
|
const reset = usePinnedMessageStore((state) => state.reset);
|
|
@@ -78,8 +86,8 @@ export const usePinMessage = (conversationID?: string) => {
|
|
|
78
86
|
async (message: MessageItem) => {
|
|
79
87
|
if (!conversationID) return;
|
|
80
88
|
|
|
81
|
-
if (
|
|
82
|
-
showPinLimitReachedModal();
|
|
89
|
+
if (remainingPinCount <= 0) {
|
|
90
|
+
showPinLimitReachedModal(totalCount + remainingPinCount);
|
|
83
91
|
return;
|
|
84
92
|
}
|
|
85
93
|
|
|
@@ -104,7 +112,7 @@ export const usePinMessage = (conversationID?: string) => {
|
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
114
|
},
|
|
107
|
-
[conversationID,
|
|
115
|
+
[conversationID, remainingPinCount, totalCount, addMessage]
|
|
108
116
|
);
|
|
109
117
|
|
|
110
118
|
const unpinMessage = useCallback(
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type RefObject,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Platform,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
type LayoutChangeEvent,
|
|
14
|
+
type View,
|
|
15
|
+
} from 'react-native';
|
|
3
16
|
import { debounce } from 'lodash';
|
|
4
17
|
import Reanimated from 'react-native-reanimated';
|
|
18
|
+
import { KeyboardStickyView } from 'react-native-keyboard-controller';
|
|
5
19
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
20
|
import { KContainer, KColors, KImage, KButton, KLabel } from '@droppii/libs';
|
|
7
21
|
|
|
@@ -38,6 +52,8 @@ interface ChatComposerInternalProps {
|
|
|
38
52
|
isEmptyMessage?: boolean;
|
|
39
53
|
pannelActions?: DChatPannelAction[];
|
|
40
54
|
isDisableCompose?: boolean;
|
|
55
|
+
composerRef?: RefObject<View | null>;
|
|
56
|
+
onComposerLayout?: (event: LayoutChangeEvent) => void;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
export const ChatComposer = memo(
|
|
@@ -45,6 +61,8 @@ export const ChatComposer = memo(
|
|
|
45
61
|
isEmptyMessage = false,
|
|
46
62
|
pannelActions,
|
|
47
63
|
isDisableCompose = false,
|
|
64
|
+
composerRef,
|
|
65
|
+
onComposerLayout,
|
|
48
66
|
}: ChatComposerInternalProps) => {
|
|
49
67
|
const insets = useSafeAreaInsets();
|
|
50
68
|
|
|
@@ -70,7 +88,6 @@ export const ChatComposer = memo(
|
|
|
70
88
|
|
|
71
89
|
const { accessorySlotStyle, toggleAttachmentVisibility, closeAttachment } =
|
|
72
90
|
useChatComposerAnimation({
|
|
73
|
-
isAttachmentOpen,
|
|
74
91
|
panelHeight,
|
|
75
92
|
onAttachmentOpenChange: setIsAttachmentOpen,
|
|
76
93
|
});
|
|
@@ -86,7 +103,7 @@ export const ChatComposer = memo(
|
|
|
86
103
|
|
|
87
104
|
const handleInputFocus = useCallback(() => {
|
|
88
105
|
setIsInputFocused(true);
|
|
89
|
-
if (isAttachmentOpen) closeAttachment();
|
|
106
|
+
if (isAttachmentOpen) closeAttachment(true);
|
|
90
107
|
}, [closeAttachment, isAttachmentOpen, setIsInputFocused]);
|
|
91
108
|
|
|
92
109
|
const handleInputBlur = useCallback(() => {
|
|
@@ -177,8 +194,14 @@ export const ChatComposer = memo(
|
|
|
177
194
|
[compose]
|
|
178
195
|
);
|
|
179
196
|
|
|
197
|
+
let content: ReactNode;
|
|
198
|
+
const isNewBotConversation =
|
|
199
|
+
isBotConversation &&
|
|
200
|
+
conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
|
|
201
|
+
isEmptyMessage;
|
|
202
|
+
|
|
180
203
|
if (isDisableCompose) {
|
|
181
|
-
|
|
204
|
+
content = (
|
|
182
205
|
<KContainer.View row alignItems justifyContent>
|
|
183
206
|
<KContainer.View marginR="0.25rem">
|
|
184
207
|
<KImage.VectorIcons
|
|
@@ -192,13 +215,8 @@ export const ChatComposer = memo(
|
|
|
192
215
|
</KLabel.Text>
|
|
193
216
|
</KContainer.View>
|
|
194
217
|
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
isBotConversation &&
|
|
198
|
-
conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
|
|
199
|
-
isEmptyMessage
|
|
200
|
-
) {
|
|
201
|
-
return (
|
|
218
|
+
} else if (isNewBotConversation) {
|
|
219
|
+
content = (
|
|
202
220
|
<KContainer.View padding={'1.5rem'} paddingB={0}>
|
|
203
221
|
<KButton.Solid
|
|
204
222
|
size="md"
|
|
@@ -210,102 +228,117 @@ export const ChatComposer = memo(
|
|
|
210
228
|
/>
|
|
211
229
|
</KContainer.View>
|
|
212
230
|
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
/>
|
|
225
|
-
|
|
226
|
-
{compose.showLinkPreview && (
|
|
227
|
-
<ChatLinkPreview
|
|
228
|
-
metadata={compose.urlMetadata}
|
|
229
|
-
isLoading={compose.isMetadataLoading}
|
|
230
|
-
onDismiss={compose.dismissPreview}
|
|
231
|
+
} else {
|
|
232
|
+
content = (
|
|
233
|
+
<KContainer.View background={'#F9F9FA'}>
|
|
234
|
+
<ChatCurrentQuotedMessage />
|
|
235
|
+
<ChatQuickActions
|
|
236
|
+
visible
|
|
237
|
+
actions={DEFAULT_QUICK_ACTIONS}
|
|
238
|
+
onActionPress={(action) =>
|
|
239
|
+
console.log('[ChatComposer] quick action:', action.id)
|
|
240
|
+
}
|
|
231
241
|
/>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
attachments={attachments}
|
|
239
|
-
onRemove={onRemove}
|
|
240
|
-
/>
|
|
241
|
-
|
|
242
|
-
<KContainer.View
|
|
243
|
-
row
|
|
244
|
-
alignItems
|
|
245
|
-
paddingH="0.75rem"
|
|
246
|
-
paddingT="0.75rem"
|
|
247
|
-
paddingB="0.75rem"
|
|
248
|
-
style={inputRowStyle}
|
|
249
|
-
>
|
|
250
|
-
<KContainer.Touchable
|
|
251
|
-
onPress={toggleAttachmentVisibility}
|
|
252
|
-
center
|
|
253
|
-
br="round"
|
|
254
|
-
>
|
|
255
|
-
<KImage.VectorIcons
|
|
256
|
-
name={isAttachmentOpen ? 'close-circle-o' : 'plus-circle-o'}
|
|
257
|
-
size={24}
|
|
258
|
-
color={
|
|
259
|
-
isAttachmentOpen
|
|
260
|
-
? KColors.palette.primary.w400
|
|
261
|
-
: KColors.palette.gray.w600
|
|
262
|
-
}
|
|
242
|
+
|
|
243
|
+
{compose.showLinkPreview && (
|
|
244
|
+
<ChatLinkPreview
|
|
245
|
+
metadata={compose.urlMetadata}
|
|
246
|
+
isLoading={compose.isMetadataLoading}
|
|
247
|
+
onDismiss={compose.dismissPreview}
|
|
263
248
|
/>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<ChatMessageInput
|
|
267
|
-
value={compose.text}
|
|
268
|
-
placeholder="Nhập tin nhắn..."
|
|
269
|
-
onChangeText={handleChangeText}
|
|
270
|
-
onFocus={handleInputFocus}
|
|
271
|
-
onBlur={handleInputBlur}
|
|
272
|
-
onContentSizeChange={handleInputContentSizeChange}
|
|
273
|
-
isInputFocused={isInputFocused}
|
|
274
|
-
isMultilineInput={isMultilineInput}
|
|
275
|
-
inputRef={inputRef}
|
|
276
|
-
/>
|
|
249
|
+
)}
|
|
277
250
|
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
width={40}
|
|
282
|
-
height={40}
|
|
283
|
-
center
|
|
284
|
-
br="round"
|
|
285
|
-
background={
|
|
286
|
-
compose.isUploading
|
|
287
|
-
? KColors.palette.gray.w400
|
|
288
|
-
: KColors.palette.primary.w400
|
|
251
|
+
<AttachmentPreview
|
|
252
|
+
visible={
|
|
253
|
+
compose.isImagePreviewVisible || compose.isFilePreviewVisible
|
|
289
254
|
}
|
|
290
|
-
|
|
291
|
-
|
|
255
|
+
attachments={attachments}
|
|
256
|
+
onRemove={onRemove}
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
<KContainer.View
|
|
260
|
+
row
|
|
261
|
+
alignItems
|
|
262
|
+
paddingH="0.75rem"
|
|
263
|
+
paddingT="0.75rem"
|
|
264
|
+
paddingB="0.75rem"
|
|
265
|
+
style={inputRowStyle}
|
|
292
266
|
>
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
267
|
+
<KContainer.Touchable
|
|
268
|
+
onPress={toggleAttachmentVisibility}
|
|
269
|
+
center
|
|
270
|
+
br="round"
|
|
271
|
+
>
|
|
272
|
+
<KImage.VectorIcons
|
|
273
|
+
name={isAttachmentOpen ? 'close-circle-o' : 'plus-circle-o'}
|
|
274
|
+
size={24}
|
|
275
|
+
color={
|
|
276
|
+
isAttachmentOpen
|
|
277
|
+
? KColors.palette.primary.w400
|
|
278
|
+
: KColors.palette.gray.w600
|
|
279
|
+
}
|
|
280
|
+
/>
|
|
281
|
+
</KContainer.Touchable>
|
|
282
|
+
|
|
283
|
+
<ChatMessageInput
|
|
284
|
+
value={compose.text}
|
|
285
|
+
placeholder="Nhập tin nhắn..."
|
|
286
|
+
onChangeText={handleChangeText}
|
|
287
|
+
onFocus={handleInputFocus}
|
|
288
|
+
onBlur={handleInputBlur}
|
|
289
|
+
onContentSizeChange={handleInputContentSizeChange}
|
|
290
|
+
isInputFocused={isInputFocused}
|
|
291
|
+
isMultilineInput={isMultilineInput}
|
|
292
|
+
inputRef={inputRef}
|
|
298
293
|
/>
|
|
299
|
-
|
|
294
|
+
|
|
295
|
+
<KContainer.Touchable
|
|
296
|
+
onPress={compose.isUploading ? undefined : handleSend}
|
|
297
|
+
pointerEvents={compose.isUploading ? 'none' : 'auto'}
|
|
298
|
+
width={40}
|
|
299
|
+
height={40}
|
|
300
|
+
center
|
|
301
|
+
br="round"
|
|
302
|
+
background={
|
|
303
|
+
compose.isUploading
|
|
304
|
+
? KColors.palette.gray.w400
|
|
305
|
+
: KColors.palette.primary.w400
|
|
306
|
+
}
|
|
307
|
+
marginL="0.75rem"
|
|
308
|
+
opacity={compose.isUploading ? 0.6 : 1}
|
|
309
|
+
>
|
|
310
|
+
<KImage.VectorIcons
|
|
311
|
+
name="send"
|
|
312
|
+
provider="MaterialCommunityIcons"
|
|
313
|
+
size={20}
|
|
314
|
+
color={KColors.white}
|
|
315
|
+
/>
|
|
316
|
+
</KContainer.Touchable>
|
|
317
|
+
</KContainer.View>
|
|
300
318
|
</KContainer.View>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
301
321
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
return (
|
|
323
|
+
<>
|
|
324
|
+
<KeyboardStickyView
|
|
325
|
+
ref={composerRef}
|
|
326
|
+
onLayout={onComposerLayout}
|
|
327
|
+
offset={{ closed: 0, opened: insets.bottom }}
|
|
328
|
+
>
|
|
329
|
+
{content}
|
|
330
|
+
</KeyboardStickyView>
|
|
331
|
+
<KContainer.VisibleView
|
|
332
|
+
visible={!isDisableCompose && !isNewBotConversation}
|
|
305
333
|
>
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
334
|
+
<Reanimated.View
|
|
335
|
+
style={[styles.accessorySlot, accessorySlotStyle]}
|
|
336
|
+
pointerEvents={isAttachmentOpen ? 'auto' : 'none'}
|
|
337
|
+
>
|
|
338
|
+
<ChatAttachmentPanel actions={resolvedPannelActons} />
|
|
339
|
+
</Reanimated.View>
|
|
340
|
+
</KContainer.VisibleView>
|
|
341
|
+
</>
|
|
309
342
|
);
|
|
310
343
|
}
|
|
311
344
|
);
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import { DeviceEventEmitter,
|
|
2
|
+
import { DeviceEventEmitter, View } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useKeyboardChatComposerInset,
|
|
6
|
+
useKeyboardScrollToEnd,
|
|
7
|
+
} from '@legendapp/list/keyboard';
|
|
8
|
+
import type { LegendListRef } from '@legendapp/list/react-native';
|
|
5
9
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
10
|
import { KContainer } from '@droppii/libs';
|
|
7
11
|
import { useNavigation } from '@react-navigation/native';
|
|
@@ -123,10 +127,13 @@ const ChatDetail = memo(
|
|
|
123
127
|
|
|
124
128
|
const insets = useSafeAreaInsets();
|
|
125
129
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
const listRef = useRef<LegendListRef>(null);
|
|
131
|
+
const composerRef = useRef<View>(null);
|
|
132
|
+
const { contentInsetEndAdjustment, onComposerLayout } =
|
|
133
|
+
useKeyboardChatComposerInset(listRef, composerRef);
|
|
134
|
+
const { freeze, scrollMessageToEnd } = useKeyboardScrollToEnd({
|
|
135
|
+
listRef,
|
|
136
|
+
});
|
|
130
137
|
|
|
131
138
|
const handlePressUrl = useCallback(
|
|
132
139
|
(url: string) => {
|
|
@@ -173,33 +180,34 @@ const ChatDetail = memo(
|
|
|
173
180
|
onPress={handlePressPinnedMessageBar}
|
|
174
181
|
/>
|
|
175
182
|
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
183
|
+
<ChatListLegend
|
|
184
|
+
messages={messages}
|
|
185
|
+
conversationId={conversationId}
|
|
186
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
187
|
+
isLoading={initLoading || isLoadingConversation}
|
|
188
|
+
onLoadMoreOld={getMoreOldMessages}
|
|
189
|
+
hasMoreOld={hasMoreOld}
|
|
190
|
+
moreOldLoading={moreOldLoading}
|
|
191
|
+
onLoadMoreNew={getMoreNewMessages}
|
|
192
|
+
hasMoreNew={hasMoreNew}
|
|
193
|
+
moreNewLoading={moreNewLoading}
|
|
194
|
+
onMediaPress={handleMediaPress}
|
|
195
|
+
onPressUrl={handlePressUrl}
|
|
196
|
+
onLongPress={handleMessageLongPress}
|
|
197
|
+
listRef={listRef}
|
|
198
|
+
contentInsetEndAdjustment={contentInsetEndAdjustment}
|
|
199
|
+
freeze={freeze}
|
|
200
|
+
scrollMessageToEnd={scrollMessageToEnd}
|
|
201
|
+
keyboardOffset={insets.bottom}
|
|
202
|
+
/>
|
|
203
|
+
|
|
204
|
+
<ChatComposer
|
|
205
|
+
isEmptyMessage={messages?.length === 0}
|
|
206
|
+
pannelActions={pannelActions}
|
|
207
|
+
isDisableCompose={botStatus === DBotStatusValue.INACTIVE}
|
|
208
|
+
composerRef={composerRef}
|
|
209
|
+
onComposerLayout={onComposerLayout}
|
|
210
|
+
/>
|
|
203
211
|
|
|
204
212
|
<MediaViewerModal ref={mediaViewerRef} />
|
|
205
213
|
<MessageToolboxOverlay
|
|
@@ -216,8 +224,4 @@ const ChatDetail = memo(
|
|
|
216
224
|
|
|
217
225
|
ChatDetail.displayName = 'ChatDetail';
|
|
218
226
|
|
|
219
|
-
const styles = StyleSheet.create({
|
|
220
|
-
keyboardAvoiding: { flex: 1 },
|
|
221
|
-
});
|
|
222
|
-
|
|
223
227
|
export default ChatDetail;
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useCallback,
|
|
4
|
+
useMemo,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type RefObject,
|
|
9
|
+
} from 'react';
|
|
2
10
|
import { DeviceEventEmitter, StyleSheet, type View } from 'react-native';
|
|
3
11
|
import Animated, {
|
|
4
12
|
useSharedValue,
|
|
5
13
|
useAnimatedStyle,
|
|
6
14
|
withSpring,
|
|
15
|
+
type SharedValue,
|
|
7
16
|
} from 'react-native-reanimated';
|
|
8
|
-
import {
|
|
17
|
+
import { KeyboardAwareLegendList } from '@legendapp/list/keyboard';
|
|
18
|
+
import type { LegendListRef } from '@legendapp/list/react-native';
|
|
9
19
|
import { KContainer, KColors, KSpacingValue, KLabel } from '@droppii/libs';
|
|
10
20
|
import { precomputeLegendListData } from '../../utils/legendListMessage';
|
|
11
21
|
import type { ChatListProps } from './types';
|
|
@@ -26,6 +36,17 @@ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
|
26
36
|
import { useUserStore } from '../../store';
|
|
27
37
|
import { ChatMessageAPI } from '../../services';
|
|
28
38
|
|
|
39
|
+
interface ChatListLegendInternalProps extends ChatListProps {
|
|
40
|
+
listRef: RefObject<LegendListRef | null>;
|
|
41
|
+
contentInsetEndAdjustment: SharedValue<number>;
|
|
42
|
+
freeze: SharedValue<boolean>;
|
|
43
|
+
scrollMessageToEnd: (opts: {
|
|
44
|
+
animated: boolean;
|
|
45
|
+
closeKeyboard: boolean;
|
|
46
|
+
}) => Promise<void>;
|
|
47
|
+
keyboardOffset: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
export const ChatListLegend = memo(
|
|
30
51
|
({
|
|
31
52
|
messages = [],
|
|
@@ -40,7 +61,12 @@ export const ChatListLegend = memo(
|
|
|
40
61
|
onMediaPress,
|
|
41
62
|
onPressUrl,
|
|
42
63
|
onLongPress,
|
|
43
|
-
|
|
64
|
+
listRef,
|
|
65
|
+
contentInsetEndAdjustment,
|
|
66
|
+
freeze,
|
|
67
|
+
scrollMessageToEnd,
|
|
68
|
+
keyboardOffset,
|
|
69
|
+
}: ChatListLegendInternalProps) => {
|
|
44
70
|
const currentUserId = useUserStore((s) => s.user?.userID);
|
|
45
71
|
// Precompute all message data in single pass (O(n))
|
|
46
72
|
const messageDataMap = useMemo(
|
|
@@ -58,7 +84,6 @@ export const ChatListLegend = memo(
|
|
|
58
84
|
const { mutate: runFlow } = useRunFlow();
|
|
59
85
|
const { mutate: resetFlow } = useResetFlow();
|
|
60
86
|
|
|
61
|
-
const listRef = useRef<LegendListRef>(null);
|
|
62
87
|
const buttonOpacity = useSharedValue(0);
|
|
63
88
|
const isAtBottomRef = useRef(true);
|
|
64
89
|
const scrollDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
@@ -285,12 +310,12 @@ export const ChatListLegend = memo(
|
|
|
285
310
|
if (hasMoreNew) {
|
|
286
311
|
onLoadMoreOld?.(false);
|
|
287
312
|
} else {
|
|
288
|
-
|
|
313
|
+
scrollMessageToEnd({ animated: true, closeKeyboard: false });
|
|
289
314
|
buttonOpacity.value = withSpring(0, { duration: 300 });
|
|
290
315
|
isAtBottomRef.current = true;
|
|
291
316
|
setShowBadge(false);
|
|
292
317
|
}
|
|
293
|
-
}, [
|
|
318
|
+
}, [hasMoreNew, onLoadMoreOld, scrollMessageToEnd, buttonOpacity]);
|
|
294
319
|
|
|
295
320
|
const animatedButtonStyle = useAnimatedStyle(() => ({
|
|
296
321
|
opacity: buttonOpacity.value,
|
|
@@ -359,6 +384,7 @@ export const ChatListLegend = memo(
|
|
|
359
384
|
conversationId,
|
|
360
385
|
buttonOpacity,
|
|
361
386
|
handleScrollToBottom,
|
|
387
|
+
listRef,
|
|
362
388
|
]);
|
|
363
389
|
|
|
364
390
|
// if (isLoading) {
|
|
@@ -374,7 +400,7 @@ export const ChatListLegend = memo(
|
|
|
374
400
|
|
|
375
401
|
return (
|
|
376
402
|
<KContainer.View flex background={KColors.white} style={styles.container}>
|
|
377
|
-
<
|
|
403
|
+
<KeyboardAwareLegendList
|
|
378
404
|
ref={listRef}
|
|
379
405
|
data={messages}
|
|
380
406
|
renderItem={renderItem}
|
|
@@ -385,6 +411,9 @@ export const ChatListLegend = memo(
|
|
|
385
411
|
alignItemsAtEnd
|
|
386
412
|
maintainVisibleContentPosition={maintainVisiblePositionConfig}
|
|
387
413
|
maintainScrollAtEnd={maintainScrollConfig}
|
|
414
|
+
contentInsetEndAdjustment={contentInsetEndAdjustment}
|
|
415
|
+
freeze={freeze}
|
|
416
|
+
keyboardOffset={keyboardOffset}
|
|
388
417
|
scrollEventThrottle={16}
|
|
389
418
|
onScroll={handleScroll}
|
|
390
419
|
contentContainerStyle={styles.content}
|