@droppii-org/chat-mobile 0.2.87 → 0.2.89
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/components/ThreadCard/ThreadCard.js +0 -1
- package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
- package/lib/module/components/messages/LinkifiedText.js +34 -0
- package/lib/module/components/messages/LinkifiedText.js.map +1 -0
- package/lib/module/components/messages/quotedMessage/index.js +14 -2
- package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
- package/lib/module/components/messages/textMessage/index.js +15 -22
- package/lib/module/components/messages/textMessage/index.js.map +1 -1
- package/lib/module/core/useChatListener.js +2 -2
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/contacts/useSearchContacts.js +3 -2
- package/lib/module/hooks/contacts/useSearchContacts.js.map +1 -1
- package/lib/module/hooks/groups/useFetchPublicGroup.js +3 -2
- package/lib/module/hooks/groups/useFetchPublicGroup.js.map +1 -1
- package/lib/module/hooks/message/useMessages.js +6 -0
- package/lib/module/hooks/message/useMessages.js.map +1 -1
- package/lib/module/hooks/message/useSearchMessage.js +3 -2
- package/lib/module/hooks/message/useSearchMessage.js.map +1 -1
- package/lib/module/hooks/message/useSendMessage.js +10 -3
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchAllResult.js +29 -22
- package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchCommunityResult.js +3 -2
- package/lib/module/screens/SearchConversation/SearchCommunityResult.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchContactsResult.js +4 -2
- package/lib/module/screens/SearchConversation/SearchContactsResult.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchMessagesResult.js +4 -2
- package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
- package/lib/module/screens/SearchConversation/TabResult.js +10 -5
- package/lib/module/screens/SearchConversation/TabResult.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +2 -1
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +259 -117
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +6 -5
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +36 -4
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
- package/lib/module/services/resourceCenter.js +45 -22
- package/lib/module/services/resourceCenter.js.map +1 -1
- package/lib/module/store/message.js +8 -1
- package/lib/module/store/message.js.map +1 -1
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/LinkifiedText.d.ts +11 -0
- package/lib/typescript/src/components/messages/LinkifiedText.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts +1 -0
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts +2 -1
- package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts.map +1 -1
- package/lib/typescript/src/hooks/groups/useFetchPublicGroup.d.ts +1 -1
- package/lib/typescript/src/hooks/groups/useFetchPublicGroup.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useMessages.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useSearchMessage.d.ts +2 -1
- package/lib/typescript/src/hooks/message/useSearchMessage.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts +2 -1
- package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts +2 -1
- package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts +2 -1
- package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts +2 -1
- package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/TabResult.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 +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.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/services/resourceCenter.d.ts.map +1 -1
- package/lib/typescript/src/store/message.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/ThreadCard/ThreadCard.tsx +0 -2
- package/src/components/messages/LinkifiedText.tsx +37 -0
- package/src/components/messages/quotedMessage/index.tsx +18 -2
- package/src/components/messages/textMessage/index.tsx +18 -27
- package/src/core/useChatListener.ts +2 -2
- package/src/hooks/contacts/useSearchContacts.ts +3 -1
- package/src/hooks/groups/useFetchPublicGroup.ts +2 -1
- package/src/hooks/message/useMessages.ts +6 -0
- package/src/hooks/message/useSearchMessage.ts +3 -1
- package/src/hooks/message/useSendMessage.ts +7 -0
- package/src/screens/ResourceCenter/ResourceCenterTabScene.tsx +0 -1
- package/src/screens/SearchConversation/SearchAllResult.tsx +25 -15
- package/src/screens/SearchConversation/SearchCommunityResult.tsx +7 -2
- package/src/screens/SearchConversation/SearchContactsResult.tsx +7 -1
- package/src/screens/SearchConversation/SearchMessagesResult.tsx +7 -1
- package/src/screens/SearchConversation/TabResult.tsx +7 -0
- package/src/screens/chat-detail/ChatDetail.tsx +1 -0
- package/src/screens/chat-detail/ChatListLegend.tsx +292 -130
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +7 -5
- package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +46 -2
- package/src/services/resourceCenter.ts +77 -36
- package/src/store/message.ts +7 -1
|
@@ -4,7 +4,7 @@ import { memo, useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { ActivityIndicator, DeviceEventEmitter, Platform, StyleSheet, useWindowDimensions } from 'react-native';
|
|
5
5
|
import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
|
|
6
6
|
import { KeyboardAwareLegendList } from '@legendapp/list/keyboard';
|
|
7
|
-
import {
|
|
7
|
+
import { KeyboardEvents } from 'react-native-keyboard-controller';
|
|
8
8
|
import { KContainer, KColors, KSpacingValue, KLabel, KRadiusValue, KImage } from '@droppii/libs';
|
|
9
9
|
import { precomputeLegendListData } from "../../utils/legendListMessage.js";
|
|
10
10
|
import { getMessageRenderId } from "../../utils/message.js";
|
|
@@ -35,11 +35,15 @@ import ChatImageDimens, { CHAT_IMAGE_VIEW_SOURCE } from "../../utils/chatImageDi
|
|
|
35
35
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
36
36
|
const ROW_MARGIN_CLUSTER_END = KSpacingValue['0.5rem'];
|
|
37
37
|
const ROW_MARGIN_IN_CLUSTER = KSpacingValue['0.25rem'];
|
|
38
|
-
const getRowMarginBottom = position => position === 'last' || position === 'solo' ? ROW_MARGIN_CLUSTER_END : ROW_MARGIN_IN_CLUSTER;
|
|
39
38
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
const
|
|
39
|
+
// Tags every __DEV__ diagnostic log below with the platform, so logs from an
|
|
40
|
+
// iOS and an Android run can be told apart when compared side by side.
|
|
41
|
+
const LOG_TAG = `[ChatListLegend:${Platform.OS}]`;
|
|
42
|
+
const BUTTON_FADE_CONFIG = {
|
|
43
|
+
duration: 200,
|
|
44
|
+
easing: Easing.out(Easing.ease)
|
|
45
|
+
};
|
|
46
|
+
const getRowMarginBottom = position => position === 'last' || position === 'solo' ? ROW_MARGIN_CLUSTER_END : ROW_MARGIN_IN_CLUSTER;
|
|
43
47
|
export const ChatListLegend = /*#__PURE__*/memo(({
|
|
44
48
|
messages = [],
|
|
45
49
|
conversationId,
|
|
@@ -48,7 +52,6 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
48
52
|
moreOldLoading,
|
|
49
53
|
hasMoreOld,
|
|
50
54
|
onLoadMoreNew,
|
|
51
|
-
moreNewLoading,
|
|
52
55
|
hasMoreNew,
|
|
53
56
|
onMediaPress,
|
|
54
57
|
onPressUrl,
|
|
@@ -82,6 +85,16 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
82
85
|
const last = messages[messages.length - 1];
|
|
83
86
|
return last?.clientMsgID || last?.serverMsgID || null;
|
|
84
87
|
}, [messages]);
|
|
88
|
+
// Stopgap for the keyboard-open blank-gap bug (unresolved upstream —
|
|
89
|
+
// see project_chatlistlegend_scroll_debugging memory): below this
|
|
90
|
+
// threshold, disable `alignItemsAtEnd` and let plain CSS `flex-end`
|
|
91
|
+
// anchor short content to the bottom instead. Known trade-off (same
|
|
92
|
+
// memory, bug #17/#19): disabling `alignItemsAtEnd` removes the extra
|
|
93
|
+
// scrollable room `clampScrollOffset` needs, so sending a message while
|
|
94
|
+
// the keyboard is open on one of these same short conversations can
|
|
95
|
+
// land the new message away from the composer instead of scrolling to
|
|
96
|
+
// it — verify that scenario specifically after any change here.
|
|
97
|
+
const isVeryShortConversation = messages.length < 15;
|
|
85
98
|
const {
|
|
86
99
|
mutate: runFlow
|
|
87
100
|
} = useRunFlow();
|
|
@@ -91,11 +104,42 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
91
104
|
const buttonOpacity = useSharedValue(0);
|
|
92
105
|
const isAtBottomRef = useRef(true);
|
|
93
106
|
const scrollDebounceRef = useRef(undefined);
|
|
107
|
+
// Diagnostic only (__DEV__): the keyboard-open bottom inset `useChatKeyboard`
|
|
108
|
+
// computes internally (raw keyboard height minus `keyboardOffset`, matching
|
|
109
|
+
// how much the composer's KeyboardStickyView actually rises). Compared
|
|
110
|
+
// against the list's real, native-reported `contentInset.bottom` in
|
|
111
|
+
// `handleScroll` below to auto-detect the known issue where that native
|
|
112
|
+
// inset silently fails to apply (confirmed via manual trace: contentOffset
|
|
113
|
+
// scrolls correctly but contentInset stays {bottom:0} on this RN/Reanimated/
|
|
114
|
+
// Old-Architecture combo, leaving a blank gap past the real content end).
|
|
115
|
+
const expectedKeyboardInsetRef = useRef(0);
|
|
94
116
|
const rowRefsMap = useRef(new Map());
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
117
|
+
// Set on a real touch-drag (`onScrollBeginDrag`), before onStartReached/
|
|
118
|
+
// onEndReached's threshold could fire. `onMomentumScrollBegin` was tried
|
|
119
|
+
// and reverted: it only fires after the finger lifts, so the first
|
|
120
|
+
// gesture's mid-drag threshold-crossing beats it, needing a wasted
|
|
121
|
+
// second gesture to actually unlock. See
|
|
122
|
+
// project_chatlistlegend_scroll_debugging for the full history.
|
|
123
|
+
const hasUserScrolledRef = useRef(false);
|
|
124
|
+
// Old Architecture defers recomputing `alignItemsAtEndPadding` when
|
|
125
|
+
// content shrinks while `initialScrollAtEnd` is active. `estimatedItemSize`
|
|
126
|
+
// below is a deliberate lower bound to mostly avoid that; this ref
|
|
127
|
+
// backstops the rest via one forced `scrollToEnd` on mount (`handleListLoad`).
|
|
128
|
+
const hasCorrectedInitialPaddingRef = useRef(false);
|
|
129
|
+
// `messages` starts empty, so `lastMessageId`'s first real value looks
|
|
130
|
+
// identical to a new message — tracking the previous id (not a one-shot
|
|
131
|
+
// flag) lets the effect below tell the two apart.
|
|
132
|
+
const previousLastMessageIdRef = useRef(null);
|
|
133
|
+
// Read via ref (not as a dependency) by the `lastMessageId` effect below —
|
|
134
|
+
// a loading-state flip alone shouldn't re-run it.
|
|
135
|
+
const moreOldLoadingRef = useRef(moreOldLoading);
|
|
136
|
+
moreOldLoadingRef.current = moreOldLoading;
|
|
137
|
+
// Read via ref (not as a dependency) by the ScrollToMessage subscription
|
|
138
|
+
// below — the whole array changes on every new message and every old-
|
|
139
|
+
// history page, which would otherwise tear down and re-add both
|
|
140
|
+
// DeviceEventEmitter listeners on every one of those.
|
|
141
|
+
const messagesRef = useRef(messages);
|
|
142
|
+
messagesRef.current = messages;
|
|
99
143
|
const [showBadge, setShowBadge] = useState(false);
|
|
100
144
|
const [highlightedId, setHighlightedId] = useState(null);
|
|
101
145
|
const highlightTimerRef = useRef(null);
|
|
@@ -201,48 +245,33 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
201
245
|
}
|
|
202
246
|
}, [currentUserId, lastMessageId, messageDataMap, isGroupConversation, isBotConversation, onMediaPress, onPressUrl, onLongPress, handleMessageLongPress, customMessageItemFactory, onResetFlow, onRunFlow, conversationFlow?.flowCurrentStep, conversationFlow?.havingBotMessage, retryMessage, highlightedId, revealedMessageId, handleMessagePress]);
|
|
203
247
|
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// itemLayout: !moreOldLoading,
|
|
213
|
-
// },
|
|
214
|
-
// }),
|
|
215
|
-
// [moreOldLoading]
|
|
216
|
-
// );
|
|
217
|
-
|
|
218
|
-
// Stable config so the native maintainVisibleContentPosition prop isn't toggled
|
|
219
|
-
// mid-prepend (toggling forces an extra layout recompute). `data` anchoring keeps
|
|
220
|
-
// the viewport pinned to the item being read when older messages are added on top.
|
|
248
|
+
// `maintainVisibleContentPosition` anchors a prepend to whatever native
|
|
249
|
+
// view sits at the top. Default `drawDistance` (250px) doesn't survive a
|
|
250
|
+
// ~1-1.5s history fetch if the user keeps scrolling away — the anchor
|
|
251
|
+
// unmounts and the native compensation silently drops (the "list drifts
|
|
252
|
+
// after loading old history" bug). `drawDistance` below is raised to
|
|
253
|
+
// outlast a realistic scroll during that wait (measured ~300-350px).
|
|
254
|
+
// `recycleItems` is safe again since @legendapp/list 3.3.7 fixed
|
|
255
|
+
// recycled rows flashing wrong content on prepend.
|
|
221
256
|
const maintainVisiblePositionConfig = useMemo(() => ({
|
|
222
257
|
data: true
|
|
223
258
|
}), []);
|
|
224
259
|
|
|
225
|
-
// Buckets averaging
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// content type with an arbitrary custom node — see `LegendChatMessage`'s
|
|
230
|
-
// `customNode` short-circuit, checked before its own contentType switch)
|
|
231
|
-
// get their own bucket per content type, so a custom node's real measured
|
|
232
|
-
// height never pollutes the native component's average for that same type.
|
|
260
|
+
// Buckets LegendList's size-averaging per content type so a batch of
|
|
261
|
+
// images landing early doesn't pollute the estimate for still-unmeasured
|
|
262
|
+
// text rows. `customMessageItemFactory` nodes get their own bucket per
|
|
263
|
+
// type too, so a custom node's real height doesn't pollute that type's average.
|
|
233
264
|
const getItemType = useCallback(item => {
|
|
234
265
|
const isCustom = !!customMessageItemFactory?.(item);
|
|
235
266
|
return `${item?.contentType ?? 'default'}${isCustom ? '-custom' : ''}`;
|
|
236
267
|
}, [customMessageItemFactory]);
|
|
237
268
|
|
|
238
|
-
// Exact size for content types
|
|
239
|
-
// `plans/260728-1400-chatlist-legend-fixed-item-size/plan.md
|
|
240
|
-
// `undefined` (defer to
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// and once it resolves a number that item is never measured again, so
|
|
245
|
-
// guessing wrong here is a permanent regression, not just an approximation.
|
|
269
|
+
// Exact size for content types knowable ahead of render (see
|
|
270
|
+
// `plans/260728-1400-chatlist-legend-fixed-item-size/plan.md`). Returns
|
|
271
|
+
// `undefined` (defer to measurement) for anything unpredictable, or
|
|
272
|
+
// carrying chrome this function doesn't account for (day-divider, flow-
|
|
273
|
+
// input-buttons row) — a resolved number is never re-measured, so
|
|
274
|
+
// guessing wrong here is permanent, not just approximate.
|
|
246
275
|
const getFixedItemSize = useCallback((item, index) => {
|
|
247
276
|
const message = item;
|
|
248
277
|
const messageId = getMessageRenderId(message, index);
|
|
@@ -294,13 +323,10 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
294
323
|
return candidateSize;
|
|
295
324
|
}, [messageDataMap, customMessageItemFactory, currentUserId, isGroupConversation, lastMessageId, isBotConversation, conversationFlow?.flowCurrentStep, windowWidth]);
|
|
296
325
|
|
|
297
|
-
// LegendList
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
// message itself
|
|
301
|
-
// 'first' into a 'middle'. messageDataMap is memoised on exactly the inputs
|
|
302
|
-
// that can move those boundaries, so including it here is what keeps a
|
|
303
|
-
// stale tail from being left behind.
|
|
326
|
+
// LegendList only re-renders an item when [itemKey, item, extraData]
|
|
327
|
+
// changes. Cluster position depends on a message's NEIGHBOURS, which can
|
|
328
|
+
// shift (prepending history can turn a 'first' into a 'middle') without
|
|
329
|
+
// the message itself changing — messageDataMap here is what catches that.
|
|
304
330
|
const listExtraData = useMemo(() => ({
|
|
305
331
|
messageDataMap,
|
|
306
332
|
flowStepId: conversationFlow?.flowCurrentStep?.id ?? '',
|
|
@@ -309,46 +335,75 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
309
335
|
revealedMessageId
|
|
310
336
|
}), [messageDataMap, conversationFlow?.flowCurrentStep?.id, lastMessageId, highlightedId, revealedMessageId]);
|
|
311
337
|
const handleStartReached = useCallback(() => {
|
|
312
|
-
const willLoad = hasMoreOld && !moreOldLoading;
|
|
313
338
|
if (__DEV__) {
|
|
314
|
-
console.log(
|
|
339
|
+
console.log(`${LOG_TAG} onStartReached fired`, {
|
|
315
340
|
conversationId,
|
|
316
|
-
|
|
317
|
-
firstMessageId: messages[0]?.clientMsgID || messages[0]?.serverMsgID,
|
|
341
|
+
hasUserScrolled: hasUserScrolledRef.current,
|
|
318
342
|
hasMoreOld,
|
|
319
|
-
moreOldLoading
|
|
320
|
-
willLoad,
|
|
321
|
-
duringInitialSettle: !hasSettledRef.current
|
|
343
|
+
moreOldLoading: useMessageStore.getState().moreOldLoading
|
|
322
344
|
});
|
|
323
345
|
}
|
|
324
|
-
if (
|
|
346
|
+
if (!hasUserScrolledRef.current) {
|
|
347
|
+
const wouldHaveLoaded = __DEV__ && hasMoreOld && !useMessageStore.getState().moreOldLoading;
|
|
348
|
+
if (wouldHaveLoaded) {
|
|
349
|
+
console.warn(`${LOG_TAG} onStartReached fired before user scroll — suppressed`, {
|
|
350
|
+
conversationId,
|
|
351
|
+
hasMoreOld
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (hasMoreOld && !useMessageStore.getState().moreOldLoading) {
|
|
325
357
|
onLoadMoreOld?.(true);
|
|
326
358
|
}
|
|
327
|
-
}, [conversationId,
|
|
359
|
+
}, [conversationId, hasMoreOld, onLoadMoreOld]);
|
|
328
360
|
const handleEndReached = useCallback(() => {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
duringInitialSettle: !hasSettledRef.current
|
|
339
|
-
});
|
|
361
|
+
if (!hasUserScrolledRef.current) {
|
|
362
|
+
const wouldHaveLoaded = __DEV__ && hasMoreNew && !useMessageStore.getState().moreNewLoading;
|
|
363
|
+
if (wouldHaveLoaded) {
|
|
364
|
+
console.warn(`${LOG_TAG} onEndReached fired before user scroll — suppressed`, {
|
|
365
|
+
conversationId,
|
|
366
|
+
hasMoreNew
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
340
370
|
}
|
|
341
|
-
if (
|
|
371
|
+
if (hasMoreNew && !useMessageStore.getState().moreNewLoading) {
|
|
372
|
+
if (__DEV__) console.log(`${LOG_TAG} handleEndReached`);
|
|
342
373
|
onLoadMoreNew?.(true);
|
|
343
374
|
}
|
|
344
|
-
}, [conversationId,
|
|
375
|
+
}, [conversationId, hasMoreNew, onLoadMoreNew]);
|
|
376
|
+
const handleScrollBeginDrag = useCallback(() => {
|
|
377
|
+
hasUserScrolledRef.current = true;
|
|
378
|
+
}, []);
|
|
345
379
|
const handleScroll = useCallback(e => {
|
|
346
380
|
const {
|
|
347
381
|
contentOffset,
|
|
348
382
|
contentSize,
|
|
349
|
-
layoutMeasurement
|
|
383
|
+
layoutMeasurement,
|
|
384
|
+
contentInset
|
|
350
385
|
} = e.nativeEvent;
|
|
351
386
|
const isAtBottom = contentOffset.y + layoutMeasurement.height >= contentSize.height - 100;
|
|
387
|
+
if (__DEV__) {
|
|
388
|
+
const expected = expectedKeyboardInsetRef.current;
|
|
389
|
+
const actual = contentInset?.bottom ?? 0;
|
|
390
|
+
// `contentInset` failing to apply is a constant, systemic mismatch
|
|
391
|
+
// (present regardless of conversation length) — but it only shows
|
|
392
|
+
// up as a visible gap once the scroll position has actually moved
|
|
393
|
+
// past the real end of content. Gate on that so the warning fires
|
|
394
|
+
// only when the bug is actually visible, not on every keyboard
|
|
395
|
+
// open in every conversation.
|
|
396
|
+
const scrolledPastRealContentEnd = contentOffset.y + layoutMeasurement.height > contentSize.height;
|
|
397
|
+
if (expected > 0 && Math.abs(actual - expected) > 5 && scrolledPastRealContentEnd) {
|
|
398
|
+
console.warn(`${LOG_TAG} keyboard contentInset mismatch — expected bottom inset ~${expected} (keyboard open) but native reports ${actual}, and scroll position is past the real content end. This reproduces the known "blank gap" issue: contentOffset scrolls correctly but contentInset silently fails to apply.`, {
|
|
399
|
+
expected,
|
|
400
|
+
actual,
|
|
401
|
+
contentOffsetY: contentOffset.y,
|
|
402
|
+
contentSizeHeight: contentSize.height,
|
|
403
|
+
layoutHeight: layoutMeasurement.height
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
352
407
|
|
|
353
408
|
// Debounce animation updates
|
|
354
409
|
if (scrollDebounceRef.current) {
|
|
@@ -357,10 +412,7 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
357
412
|
scrollDebounceRef.current = setTimeout(() => {
|
|
358
413
|
if (isAtBottomRef.current !== isAtBottom) {
|
|
359
414
|
isAtBottomRef.current = isAtBottom;
|
|
360
|
-
buttonOpacity.value = withTiming(isAtBottom ? 0 : 1,
|
|
361
|
-
duration: 200,
|
|
362
|
-
easing: Easing.out(Easing.ease)
|
|
363
|
-
});
|
|
415
|
+
buttonOpacity.value = withTiming(isAtBottom ? 0 : 1, BUTTON_FADE_CONFIG);
|
|
364
416
|
}
|
|
365
417
|
if (isAtBottom) {
|
|
366
418
|
ChatMessageAPI.markConversationAsRead(conversationId || '');
|
|
@@ -370,18 +422,20 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
370
422
|
const handleScrollToBottom = useCallback(() => {
|
|
371
423
|
if (hasMoreNew) {
|
|
372
424
|
onLoadMoreOld?.(false);
|
|
373
|
-
|
|
374
|
-
scrollMessageToEnd({
|
|
375
|
-
animated: true,
|
|
376
|
-
closeKeyboard: false
|
|
377
|
-
});
|
|
378
|
-
buttonOpacity.value = withTiming(0, {
|
|
379
|
-
duration: 200,
|
|
380
|
-
easing: Easing.out(Easing.ease)
|
|
381
|
-
});
|
|
382
|
-
isAtBottomRef.current = true;
|
|
383
|
-
setShowBadge(false);
|
|
425
|
+
return;
|
|
384
426
|
}
|
|
427
|
+
// `ChatEvent.ScrollToBottom` fires after every self-sent message, but
|
|
428
|
+
// the `lastMessageId` effect below already animates to it — re-scrolling
|
|
429
|
+
// here raced it and flickered. Only act when not already at bottom
|
|
430
|
+
// (FAB tap, or a send while scrolled away).
|
|
431
|
+
if (isAtBottomRef.current) return;
|
|
432
|
+
scrollMessageToEnd({
|
|
433
|
+
animated: true,
|
|
434
|
+
closeKeyboard: false
|
|
435
|
+
});
|
|
436
|
+
buttonOpacity.value = withTiming(0, BUTTON_FADE_CONFIG);
|
|
437
|
+
isAtBottomRef.current = true;
|
|
438
|
+
setShowBadge(false);
|
|
385
439
|
}, [hasMoreNew, onLoadMoreOld, scrollMessageToEnd, buttonOpacity]);
|
|
386
440
|
const animatedButtonStyle = useAnimatedStyle(() => ({
|
|
387
441
|
opacity: buttonOpacity.value,
|
|
@@ -397,19 +451,94 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
397
451
|
};
|
|
398
452
|
}, []);
|
|
399
453
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
454
|
+
// Diagnostic only (__DEV__) — keeps `expectedKeyboardInsetRef` in sync so
|
|
455
|
+
// `handleScroll` can detect the known contentInset-not-applying issue.
|
|
456
|
+
// JS-thread only (no worklet involved), so this carries none of the
|
|
457
|
+
// reentrant-lock crash risk `useKeyboardHandler`'s own worklet hit.
|
|
404
458
|
useEffect(() => {
|
|
405
|
-
|
|
459
|
+
if (!__DEV__) return;
|
|
460
|
+
const willShow = KeyboardEvents.addListener('keyboardWillShow', e => {
|
|
461
|
+
expectedKeyboardInsetRef.current = Math.max(e.height - keyboardOffset, 0);
|
|
462
|
+
});
|
|
463
|
+
const willHide = KeyboardEvents.addListener('keyboardWillHide', () => {
|
|
464
|
+
expectedKeyboardInsetRef.current = 0;
|
|
465
|
+
});
|
|
466
|
+
return () => {
|
|
467
|
+
willShow.remove();
|
|
468
|
+
willHide.remove();
|
|
469
|
+
};
|
|
470
|
+
}, [keyboardOffset]);
|
|
471
|
+
|
|
472
|
+
// This component can stay mounted across a conversationId change (e.g.
|
|
473
|
+
// navigation.replace reusing the same screen instance), so this ref must
|
|
474
|
+
// reset per-conversation — otherwise a leftover "user scrolled" flag from
|
|
475
|
+
// the previous conversation would wrongly let load-more fire on the
|
|
476
|
+
// newly opened one before any real scroll there.
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
hasUserScrolledRef.current = false;
|
|
479
|
+
hasCorrectedInitialPaddingRef.current = false;
|
|
480
|
+
previousLastMessageIdRef.current = null;
|
|
406
481
|
}, [conversationId]);
|
|
482
|
+
|
|
483
|
+
// Single source of truth for "scroll to bottom on a new message" (self-
|
|
484
|
+
// sent or received). Skipped while prepending old history or scrolled
|
|
485
|
+
// away from the bottom. `moreOldLoading` is read via ref, not a
|
|
486
|
+
// dependency, so its own start/finish toggle doesn't re-run this.
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
const previousLastMessageId = previousLastMessageIdRef.current;
|
|
489
|
+
previousLastMessageIdRef.current = lastMessageId;
|
|
490
|
+
|
|
491
|
+
// null → a real id is the initial fetch landing (initialScrollAtEnd +
|
|
492
|
+
// handleListLoad's job), not a new message; a real id → a different
|
|
493
|
+
// one is.
|
|
494
|
+
const shouldFire = previousLastMessageId !== null && !!lastMessageId && !moreOldLoadingRef.current && isAtBottomRef.current;
|
|
495
|
+
if (__DEV__) {
|
|
496
|
+
console.log(`${LOG_TAG} lastMessageId effect — ${shouldFire ? 'firing animated scroll' : 'skipped'}`, {
|
|
497
|
+
conversationId,
|
|
498
|
+
lastMessageId,
|
|
499
|
+
initialPopulation: previousLastMessageId === null,
|
|
500
|
+
moreOldLoading: moreOldLoadingRef.current,
|
|
501
|
+
isAtBottom: isAtBottomRef.current
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (!shouldFire) return;
|
|
505
|
+
scrollMessageToEnd({
|
|
506
|
+
animated: true,
|
|
507
|
+
closeKeyboard: false
|
|
508
|
+
});
|
|
509
|
+
}, [lastMessageId, scrollMessageToEnd, conversationId]);
|
|
510
|
+
const handleListLoad = useCallback(info => {
|
|
511
|
+
if (hasCorrectedInitialPaddingRef.current) return;
|
|
512
|
+
hasCorrectedInitialPaddingRef.current = true;
|
|
513
|
+
|
|
514
|
+
// Diagnostic: how long bootstrap + the flush below each take.
|
|
515
|
+
if (__DEV__) {
|
|
516
|
+
console.log(`${LOG_TAG} onLoad (bootstrap settle done)`, {
|
|
517
|
+
conversationId,
|
|
518
|
+
messagesLength: messages.length,
|
|
519
|
+
elapsedTimeInMs: info.elapsedTimeInMs
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const startedAt = __DEV__ ? Date.now() : 0;
|
|
523
|
+
const flushPromise = scrollMessageToEnd({
|
|
524
|
+
animated: false,
|
|
525
|
+
closeKeyboard: false
|
|
526
|
+
});
|
|
527
|
+
if (__DEV__) {
|
|
528
|
+
flushPromise.then(() => {
|
|
529
|
+
console.log(`${LOG_TAG} handleListLoad flush resolved`, {
|
|
530
|
+
conversationId,
|
|
531
|
+
tookMs: Date.now() - startedAt
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}, [scrollMessageToEnd, conversationId, messages.length]);
|
|
407
536
|
useEffect(() => {
|
|
408
537
|
const bottomSubscription = DeviceEventEmitter.addListener(ChatEvent.ScrollToBottom, handleScrollToBottom);
|
|
409
538
|
const subscription = DeviceEventEmitter.addListener(ChatEvent.ScrollToMessage, ({
|
|
410
539
|
clientMessageId
|
|
411
540
|
}) => {
|
|
412
|
-
const index =
|
|
541
|
+
const index = messagesRef.current.findIndex(m => m.clientMsgID === clientMessageId);
|
|
413
542
|
if (index >= 0) {
|
|
414
543
|
try {
|
|
415
544
|
listRef.current?.scrollToIndex({
|
|
@@ -441,7 +570,7 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
441
570
|
bottomSubscription.remove();
|
|
442
571
|
subscription.remove();
|
|
443
572
|
};
|
|
444
|
-
}, [
|
|
573
|
+
}, [applicationType, searchMessages, conversationId, handleScrollToBottom, listRef]);
|
|
445
574
|
if (isLoading) {
|
|
446
575
|
return /*#__PURE__*/_jsx(KContainer.View, {
|
|
447
576
|
flex: true,
|
|
@@ -453,50 +582,60 @@ export const ChatListLegend = /*#__PURE__*/memo(({
|
|
|
453
582
|
})
|
|
454
583
|
});
|
|
455
584
|
}
|
|
456
|
-
console.log('messages length: ', +messages.length);
|
|
457
585
|
return /*#__PURE__*/_jsxs(KContainer.View, {
|
|
458
586
|
flex: true,
|
|
459
587
|
background: KColors.white,
|
|
460
588
|
style: styles.container,
|
|
461
|
-
children: [/*#__PURE__*/_jsx(
|
|
589
|
+
children: [/*#__PURE__*/_jsx(KeyboardAwareLegendList, {
|
|
462
590
|
ref: listRef,
|
|
463
591
|
data: messages,
|
|
464
|
-
renderItem: renderItem
|
|
465
|
-
|
|
592
|
+
renderItem: renderItem
|
|
593
|
+
// Deliberate lower bound (not a best-guess average) — see the
|
|
594
|
+
// comment above `hasCorrectedInitialPaddingRef`. A single-line
|
|
595
|
+
// bubble floors out around 38-44px, so this stays safely under it.
|
|
596
|
+
,
|
|
597
|
+
estimatedItemSize: 36,
|
|
466
598
|
getItemType: getItemType,
|
|
467
599
|
getFixedItemSize: getFixedItemSize,
|
|
468
600
|
keyExtractor: item => String(item.clientMsgID || item.serverMsgID),
|
|
469
|
-
recycleItems:
|
|
601
|
+
recycleItems: true,
|
|
470
602
|
extraData: listExtraData,
|
|
471
|
-
alignItemsAtEnd:
|
|
472
|
-
|
|
603
|
+
alignItemsAtEnd: !isVeryShortConversation,
|
|
604
|
+
alwaysRender: {
|
|
605
|
+
top: 10
|
|
606
|
+
},
|
|
607
|
+
maintainScrollAtEnd: true,
|
|
608
|
+
maintainVisibleContentPosition: maintainVisiblePositionConfig
|
|
609
|
+
// See the comment above `maintainVisiblePositionConfig` — this is
|
|
610
|
+
// what keeps a history prepend's anchor item mounted through a slow
|
|
611
|
+
// fetch instead of the default 250px window losing it.
|
|
612
|
+
,
|
|
613
|
+
drawDistance: 800,
|
|
473
614
|
freeze: freeze,
|
|
474
615
|
keyboardOffset: keyboardOffset,
|
|
475
616
|
scrollEventThrottle: 16,
|
|
476
617
|
onScroll: handleScroll,
|
|
477
|
-
|
|
618
|
+
onScrollBeginDrag: handleScrollBeginDrag,
|
|
619
|
+
onLoad: handleListLoad,
|
|
620
|
+
contentContainerStyle: isVeryShortConversation ? [styles.content, styles.contentBottomAnchored] : styles.content,
|
|
478
621
|
scrollIndicatorInsets: {
|
|
479
622
|
right: 1
|
|
480
623
|
},
|
|
481
624
|
ListHeaderComponent: /*#__PURE__*/_jsx(LegendChatLoadEarlier, {
|
|
482
625
|
isLoading: moreOldLoading
|
|
483
626
|
}),
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
},
|
|
494
|
-
onStartReached: handleStartReached,
|
|
495
|
-
onStartReachedThreshold: 0.5,
|
|
627
|
+
onStartReached: handleStartReached
|
|
628
|
+
// LegendList only re-fires onStartReached after scrolling away from
|
|
629
|
+
// the threshold by 1.3x and back. At 0.5 that distance exceeded a
|
|
630
|
+
// short conversation's whole scrollable range, so the mount-time
|
|
631
|
+
// firing we suppress in handleStartReached "used up" the only
|
|
632
|
+
// trigger. 0.3 keeps that reset distance safely under a
|
|
633
|
+
// ~1.5-screen conversation; shorter ones can still hit it.
|
|
634
|
+
,
|
|
635
|
+
onStartReachedThreshold: 0.2,
|
|
496
636
|
onEndReached: handleEndReached,
|
|
497
637
|
onEndReachedThreshold: 0.2,
|
|
498
|
-
|
|
499
|
-
initialScrollIndex: messages.length - 1
|
|
638
|
+
initialScrollAtEnd: true
|
|
500
639
|
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
501
640
|
style: [styles.scrollToBottomButton, animatedButtonStyle],
|
|
502
641
|
children: /*#__PURE__*/_jsxs(KContainer.Touchable, {
|
|
@@ -526,6 +665,9 @@ const styles = StyleSheet.create({
|
|
|
526
665
|
paddingVertical: KSpacingValue['0.75rem'],
|
|
527
666
|
flexGrow: 1
|
|
528
667
|
},
|
|
668
|
+
contentBottomAnchored: {
|
|
669
|
+
justifyContent: 'flex-end'
|
|
670
|
+
},
|
|
529
671
|
scrollToBottomButton: {
|
|
530
672
|
position: 'absolute',
|
|
531
673
|
bottom: 20,
|