@droppii-org/chat-mobile 0.2.43 → 0.2.45
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/context/ChatContext.js +6 -0
- package/lib/module/context/ChatContext.js.map +1 -1
- 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/module/types/chat.js.map +1 -1
- package/lib/typescript/src/context/ChatContext.d.ts +1 -1
- package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
- 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/lib/typescript/src/types/chat.d.ts +1 -0
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/context/ChatContext.tsx +6 -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/src/types/chat.ts +1 -0
|
@@ -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}
|
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
import { useCallback, useEffect
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
2
|
import { Keyboard } from 'react-native';
|
|
3
3
|
import {
|
|
4
|
-
runOnJS,
|
|
5
|
-
useAnimatedReaction,
|
|
6
4
|
useAnimatedStyle,
|
|
7
5
|
useSharedValue,
|
|
8
6
|
withTiming,
|
|
9
7
|
} from 'react-native-reanimated';
|
|
10
|
-
import {
|
|
11
|
-
useKeyboardState,
|
|
12
|
-
useReanimatedKeyboardAnimation,
|
|
13
|
-
} from 'react-native-keyboard-controller';
|
|
8
|
+
import { useKeyboardState } from 'react-native-keyboard-controller';
|
|
14
9
|
|
|
15
10
|
const ACCESSORY_ANIMATION_MS = 250;
|
|
16
|
-
const ACCESSORY_SETTLE_MS = 150;
|
|
17
11
|
|
|
18
12
|
interface UseChatComposerAnimationProps {
|
|
19
|
-
isAttachmentOpen: boolean;
|
|
20
13
|
panelHeight: number;
|
|
21
14
|
onAttachmentOpenChange: (isOpen: boolean) => void;
|
|
22
15
|
}
|
|
@@ -26,23 +19,17 @@ interface UseChatComposerAnimationReturn {
|
|
|
26
19
|
accessorySlotStyle: any;
|
|
27
20
|
toggleAttachmentVisibility: () => void;
|
|
28
21
|
openAttachment: () => void;
|
|
29
|
-
closeAttachment: () => void;
|
|
22
|
+
closeAttachment: (speedUp?: boolean) => void;
|
|
30
23
|
}
|
|
31
24
|
|
|
32
25
|
export const useChatComposerAnimation = ({
|
|
33
|
-
isAttachmentOpen,
|
|
34
26
|
panelHeight,
|
|
35
27
|
onAttachmentOpenChange,
|
|
36
28
|
}: UseChatComposerAnimationProps): UseChatComposerAnimationReturn => {
|
|
37
|
-
const isSwitchingToAttachmentRef = useRef(false);
|
|
38
29
|
const panelHeightSV = useSharedValue(panelHeight);
|
|
39
30
|
const attachmentHeightSV = useSharedValue(0);
|
|
40
|
-
const switchKeyboardHeightSV = useSharedValue(0);
|
|
41
|
-
const isKeyboardToAttachmentSwitchSV = useSharedValue(false);
|
|
42
31
|
|
|
43
32
|
const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
|
|
44
|
-
const keyboardHeight = useKeyboardState((state) => state.height);
|
|
45
|
-
const { progress: keyboardProgress } = useReanimatedKeyboardAnimation();
|
|
46
33
|
|
|
47
34
|
useEffect(() => {
|
|
48
35
|
panelHeightSV.value = panelHeight;
|
|
@@ -55,92 +42,18 @@ export const useChatComposerAnimation = ({
|
|
|
55
42
|
onAttachmentOpenChange(true);
|
|
56
43
|
}, [attachmentHeightSV, panelHeight, onAttachmentOpenChange]);
|
|
57
44
|
|
|
58
|
-
const closeAttachment = useCallback(
|
|
59
|
-
|
|
60
|
-
duration: ACCESSORY_ANIMATION_MS,
|
|
61
|
-
});
|
|
62
|
-
onAttachmentOpenChange(false);
|
|
63
|
-
}, [attachmentHeightSV, onAttachmentOpenChange]);
|
|
64
|
-
|
|
65
|
-
const finishKeyboardToAttachmentSwitch = useCallback(() => {
|
|
66
|
-
if (!isSwitchingToAttachmentRef.current) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
isSwitchingToAttachmentRef.current = false;
|
|
71
|
-
isKeyboardToAttachmentSwitchSV.value = false;
|
|
72
|
-
|
|
73
|
-
const targetHeight = panelHeightSV.value;
|
|
74
|
-
const startHeight = switchKeyboardHeightSV.value;
|
|
75
|
-
|
|
76
|
-
if (Math.abs(startHeight - targetHeight) < 1) {
|
|
77
|
-
attachmentHeightSV.value = targetHeight;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
attachmentHeightSV.value = withTiming(targetHeight, {
|
|
82
|
-
duration: ACCESSORY_SETTLE_MS,
|
|
83
|
-
});
|
|
84
|
-
}, [
|
|
85
|
-
attachmentHeightSV,
|
|
86
|
-
isKeyboardToAttachmentSwitchSV,
|
|
87
|
-
panelHeightSV,
|
|
88
|
-
switchKeyboardHeightSV,
|
|
89
|
-
]);
|
|
90
|
-
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (!isKeyboardVisible && isSwitchingToAttachmentRef.current) {
|
|
93
|
-
finishKeyboardToAttachmentSwitch();
|
|
94
|
-
}
|
|
95
|
-
}, [finishKeyboardToAttachmentSwitch, isKeyboardVisible]);
|
|
96
|
-
|
|
97
|
-
useAnimatedReaction(
|
|
98
|
-
() => ({
|
|
99
|
-
switching: isKeyboardToAttachmentSwitchSV.value,
|
|
100
|
-
progress: keyboardProgress.value,
|
|
101
|
-
}),
|
|
102
|
-
(current, previous) => {
|
|
103
|
-
if (
|
|
104
|
-
current.switching &&
|
|
105
|
-
current.progress === 0 &&
|
|
106
|
-
(previous?.progress ?? 1) > 0
|
|
107
|
-
) {
|
|
108
|
-
runOnJS(finishKeyboardToAttachmentSwitch)();
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
[finishKeyboardToAttachmentSwitch]
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
// Close panel when keyboard becomes visible (unless doing keyboard↔panel switch)
|
|
116
|
-
// Don't depend on closeAttachment to avoid triggering effect when callback changes
|
|
117
|
-
if (
|
|
118
|
-
!isSwitchingToAttachmentRef.current &&
|
|
119
|
-
isKeyboardVisible &&
|
|
120
|
-
isAttachmentOpen
|
|
121
|
-
) {
|
|
45
|
+
const closeAttachment = useCallback(
|
|
46
|
+
(speedUp = false) => {
|
|
122
47
|
attachmentHeightSV.value = withTiming(0, {
|
|
123
|
-
duration: ACCESSORY_ANIMATION_MS,
|
|
48
|
+
duration: ACCESSORY_ANIMATION_MS / (speedUp ? 2 : 1),
|
|
124
49
|
});
|
|
125
50
|
onAttachmentOpenChange(false);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
isKeyboardVisible,
|
|
130
|
-
attachmentHeightSV,
|
|
131
|
-
onAttachmentOpenChange,
|
|
132
|
-
]);
|
|
51
|
+
},
|
|
52
|
+
[attachmentHeightSV, onAttachmentOpenChange]
|
|
53
|
+
);
|
|
133
54
|
|
|
134
55
|
const accessorySlotStyle = useAnimatedStyle(() => {
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
height: switchKeyboardHeightSV.value * (1 - keyboardProgress.value),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
height: attachmentHeightSV.value,
|
|
143
|
-
};
|
|
56
|
+
return { height: attachmentHeightSV.value };
|
|
144
57
|
});
|
|
145
58
|
|
|
146
59
|
const toggleAttachmentVisibility = useCallback(() => {
|
|
@@ -152,24 +65,16 @@ export const useChatComposerAnimation = ({
|
|
|
152
65
|
return;
|
|
153
66
|
}
|
|
154
67
|
|
|
155
|
-
if (isKeyboardVisible
|
|
156
|
-
// Dismiss keyboard first, then open attachment
|
|
68
|
+
if (isKeyboardVisible) {
|
|
157
69
|
Keyboard.dismiss();
|
|
158
|
-
isSwitchingToAttachmentRef.current = true;
|
|
159
|
-
switchKeyboardHeightSV.value = keyboardHeight;
|
|
160
|
-
isKeyboardToAttachmentSwitchSV.value = true;
|
|
161
70
|
onAttachmentOpenChange(true);
|
|
162
|
-
return;
|
|
163
71
|
}
|
|
164
72
|
|
|
165
73
|
openAttachment();
|
|
166
74
|
}, [
|
|
167
75
|
closeAttachment,
|
|
168
76
|
isKeyboardVisible,
|
|
169
|
-
keyboardHeight,
|
|
170
|
-
isKeyboardToAttachmentSwitchSV,
|
|
171
77
|
onAttachmentOpenChange,
|
|
172
|
-
switchKeyboardHeightSV,
|
|
173
78
|
openAttachment,
|
|
174
79
|
attachmentHeightSV,
|
|
175
80
|
]);
|
|
@@ -134,7 +134,12 @@ const styles = StyleSheet.create({
|
|
|
134
134
|
},
|
|
135
135
|
pinBadge: {
|
|
136
136
|
position: 'absolute',
|
|
137
|
-
|
|
137
|
+
// Capped at the inter-row gap (messageRowLeft/Right's marginBottom) so the
|
|
138
|
+
// badge never overlaps the row above: LegendList's non-sticky item cells
|
|
139
|
+
// don't carry a zIndex, so cross-row paint order follows recycled view-slot
|
|
140
|
+
// order, not visual position — any real overlap can end up hidden behind
|
|
141
|
+
// the row above depending on scroll history.
|
|
142
|
+
top: -4,
|
|
138
143
|
right: -8,
|
|
139
144
|
width: 24,
|
|
140
145
|
height: 20,
|