@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
|
@@ -23,7 +23,11 @@ import Animated, {
|
|
|
23
23
|
type SharedValue,
|
|
24
24
|
} from 'react-native-reanimated';
|
|
25
25
|
import { KeyboardAwareLegendList } from '@legendapp/list/keyboard';
|
|
26
|
-
import {
|
|
26
|
+
import { KeyboardEvents } from 'react-native-keyboard-controller';
|
|
27
|
+
import {
|
|
28
|
+
type LegendListRef,
|
|
29
|
+
type MaintainVisibleContentPositionConfig,
|
|
30
|
+
} from '@legendapp/list/react-native';
|
|
27
31
|
import {
|
|
28
32
|
KContainer,
|
|
29
33
|
KColors,
|
|
@@ -72,16 +76,17 @@ import ChatImageDimens, {
|
|
|
72
76
|
const ROW_MARGIN_CLUSTER_END = KSpacingValue['0.5rem'];
|
|
73
77
|
const ROW_MARGIN_IN_CLUSTER = KSpacingValue['0.25rem'];
|
|
74
78
|
|
|
79
|
+
// Tags every __DEV__ diagnostic log below with the platform, so logs from an
|
|
80
|
+
// iOS and an Android run can be told apart when compared side by side.
|
|
81
|
+
const LOG_TAG = `[ChatListLegend:${Platform.OS}]`;
|
|
82
|
+
|
|
83
|
+
const BUTTON_FADE_CONFIG = { duration: 200, easing: Easing.out(Easing.ease) };
|
|
84
|
+
|
|
75
85
|
const getRowMarginBottom = (position: GroupPosition): number =>
|
|
76
86
|
position === 'last' || position === 'solo'
|
|
77
87
|
? ROW_MARGIN_CLUSTER_END
|
|
78
88
|
: ROW_MARGIN_IN_CLUSTER;
|
|
79
89
|
|
|
80
|
-
// KeyboardAwareLegendList auto scroll when modal opened
|
|
81
|
-
// TODO: Convert toolbox to absolute view
|
|
82
|
-
const ListComponent =
|
|
83
|
-
Platform.OS === 'ios' ? KeyboardAwareLegendList : LegendList;
|
|
84
|
-
|
|
85
90
|
interface ChatListLegendInternalProps extends ChatListProps {
|
|
86
91
|
listRef: RefObject<LegendListRef | null>;
|
|
87
92
|
freeze: SharedValue<boolean>;
|
|
@@ -101,7 +106,6 @@ export const ChatListLegend = memo(
|
|
|
101
106
|
moreOldLoading,
|
|
102
107
|
hasMoreOld,
|
|
103
108
|
onLoadMoreNew,
|
|
104
|
-
moreNewLoading,
|
|
105
109
|
hasMoreNew,
|
|
106
110
|
onMediaPress,
|
|
107
111
|
onPressUrl,
|
|
@@ -138,17 +142,58 @@ export const ChatListLegend = memo(
|
|
|
138
142
|
const last = messages[messages.length - 1];
|
|
139
143
|
return last?.clientMsgID || last?.serverMsgID || null;
|
|
140
144
|
}, [messages]);
|
|
145
|
+
// Stopgap for the keyboard-open blank-gap bug (unresolved upstream —
|
|
146
|
+
// see project_chatlistlegend_scroll_debugging memory): below this
|
|
147
|
+
// threshold, disable `alignItemsAtEnd` and let plain CSS `flex-end`
|
|
148
|
+
// anchor short content to the bottom instead. Known trade-off (same
|
|
149
|
+
// memory, bug #17/#19): disabling `alignItemsAtEnd` removes the extra
|
|
150
|
+
// scrollable room `clampScrollOffset` needs, so sending a message while
|
|
151
|
+
// the keyboard is open on one of these same short conversations can
|
|
152
|
+
// land the new message away from the composer instead of scrolling to
|
|
153
|
+
// it — verify that scenario specifically after any change here.
|
|
154
|
+
const isVeryShortConversation = messages.length < 15;
|
|
141
155
|
const { mutate: runFlow } = useRunFlow();
|
|
142
156
|
const { mutate: resetFlow } = useResetFlow();
|
|
143
157
|
|
|
144
158
|
const buttonOpacity = useSharedValue(0);
|
|
145
159
|
const isAtBottomRef = useRef(true);
|
|
146
160
|
const scrollDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
161
|
+
// Diagnostic only (__DEV__): the keyboard-open bottom inset `useChatKeyboard`
|
|
162
|
+
// computes internally (raw keyboard height minus `keyboardOffset`, matching
|
|
163
|
+
// how much the composer's KeyboardStickyView actually rises). Compared
|
|
164
|
+
// against the list's real, native-reported `contentInset.bottom` in
|
|
165
|
+
// `handleScroll` below to auto-detect the known issue where that native
|
|
166
|
+
// inset silently fails to apply (confirmed via manual trace: contentOffset
|
|
167
|
+
// scrolls correctly but contentInset stays {bottom:0} on this RN/Reanimated/
|
|
168
|
+
// Old-Architecture combo, leaving a blank gap past the real content end).
|
|
169
|
+
const expectedKeyboardInsetRef = useRef(0);
|
|
147
170
|
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
171
|
+
// Set on a real touch-drag (`onScrollBeginDrag`), before onStartReached/
|
|
172
|
+
// onEndReached's threshold could fire. `onMomentumScrollBegin` was tried
|
|
173
|
+
// and reverted: it only fires after the finger lifts, so the first
|
|
174
|
+
// gesture's mid-drag threshold-crossing beats it, needing a wasted
|
|
175
|
+
// second gesture to actually unlock. See
|
|
176
|
+
// project_chatlistlegend_scroll_debugging for the full history.
|
|
177
|
+
const hasUserScrolledRef = useRef(false);
|
|
178
|
+
// Old Architecture defers recomputing `alignItemsAtEndPadding` when
|
|
179
|
+
// content shrinks while `initialScrollAtEnd` is active. `estimatedItemSize`
|
|
180
|
+
// below is a deliberate lower bound to mostly avoid that; this ref
|
|
181
|
+
// backstops the rest via one forced `scrollToEnd` on mount (`handleListLoad`).
|
|
182
|
+
const hasCorrectedInitialPaddingRef = useRef(false);
|
|
183
|
+
// `messages` starts empty, so `lastMessageId`'s first real value looks
|
|
184
|
+
// identical to a new message — tracking the previous id (not a one-shot
|
|
185
|
+
// flag) lets the effect below tell the two apart.
|
|
186
|
+
const previousLastMessageIdRef = useRef<string | null>(null);
|
|
187
|
+
// Read via ref (not as a dependency) by the `lastMessageId` effect below —
|
|
188
|
+
// a loading-state flip alone shouldn't re-run it.
|
|
189
|
+
const moreOldLoadingRef = useRef(moreOldLoading);
|
|
190
|
+
moreOldLoadingRef.current = moreOldLoading;
|
|
191
|
+
// Read via ref (not as a dependency) by the ScrollToMessage subscription
|
|
192
|
+
// below — the whole array changes on every new message and every old-
|
|
193
|
+
// history page, which would otherwise tear down and re-add both
|
|
194
|
+
// DeviceEventEmitter listeners on every one of those.
|
|
195
|
+
const messagesRef = useRef(messages);
|
|
196
|
+
messagesRef.current = messages;
|
|
152
197
|
const [showBadge, setShowBadge] = useState(false);
|
|
153
198
|
const [highlightedId, setHighlightedId] = useState<string | null>(null);
|
|
154
199
|
const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
@@ -190,7 +235,7 @@ export const ChatListLegend = memo(
|
|
|
190
235
|
}, []);
|
|
191
236
|
|
|
192
237
|
const renderItem = useCallback(
|
|
193
|
-
({ item, index }:
|
|
238
|
+
({ item, index }: { item: MessageItem; index: number }) => {
|
|
194
239
|
try {
|
|
195
240
|
if (!item) return null;
|
|
196
241
|
|
|
@@ -310,33 +355,21 @@ export const ChatListLegend = memo(
|
|
|
310
355
|
]
|
|
311
356
|
);
|
|
312
357
|
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
// mid-prepend (toggling forces an extra layout recompute). `data` anchoring keeps
|
|
329
|
-
// the viewport pinned to the item being read when older messages are added on top.
|
|
330
|
-
const maintainVisiblePositionConfig = useMemo(() => ({ data: true }), []);
|
|
331
|
-
|
|
332
|
-
// Buckets averaging (`averageSizes[itemType]`, internal to LegendList) per
|
|
333
|
-
// content type, so a batch of images landing early doesn't pollute the
|
|
334
|
-
// estimate used for still-unmeasured text rows and vice versa. Items
|
|
335
|
-
// rendered via `customMessageItemFactory` (host apps can override ANY
|
|
336
|
-
// content type with an arbitrary custom node — see `LegendChatMessage`'s
|
|
337
|
-
// `customNode` short-circuit, checked before its own contentType switch)
|
|
338
|
-
// get their own bucket per content type, so a custom node's real measured
|
|
339
|
-
// height never pollutes the native component's average for that same type.
|
|
358
|
+
// `maintainVisibleContentPosition` anchors a prepend to whatever native
|
|
359
|
+
// view sits at the top. Default `drawDistance` (250px) doesn't survive a
|
|
360
|
+
// ~1-1.5s history fetch if the user keeps scrolling away — the anchor
|
|
361
|
+
// unmounts and the native compensation silently drops (the "list drifts
|
|
362
|
+
// after loading old history" bug). `drawDistance` below is raised to
|
|
363
|
+
// outlast a realistic scroll during that wait (measured ~300-350px).
|
|
364
|
+
// `recycleItems` is safe again since @legendapp/list 3.3.7 fixed
|
|
365
|
+
// recycled rows flashing wrong content on prepend.
|
|
366
|
+
const maintainVisiblePositionConfig: MaintainVisibleContentPositionConfig =
|
|
367
|
+
useMemo(() => ({ data: true }), []);
|
|
368
|
+
|
|
369
|
+
// Buckets LegendList's size-averaging per content type so a batch of
|
|
370
|
+
// images landing early doesn't pollute the estimate for still-unmeasured
|
|
371
|
+
// text rows. `customMessageItemFactory` nodes get their own bucket per
|
|
372
|
+
// type too, so a custom node's real height doesn't pollute that type's average.
|
|
340
373
|
const getItemType = useCallback(
|
|
341
374
|
(item: MessageItem) => {
|
|
342
375
|
const isCustom = !!customMessageItemFactory?.(item);
|
|
@@ -345,14 +378,12 @@ export const ChatListLegend = memo(
|
|
|
345
378
|
[customMessageItemFactory]
|
|
346
379
|
);
|
|
347
380
|
|
|
348
|
-
// Exact size for content types
|
|
349
|
-
// `plans/260728-1400-chatlist-legend-fixed-item-size/plan.md
|
|
350
|
-
// `undefined` (defer to
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
// and once it resolves a number that item is never measured again, so
|
|
355
|
-
// guessing wrong here is a permanent regression, not just an approximation.
|
|
381
|
+
// Exact size for content types knowable ahead of render (see
|
|
382
|
+
// `plans/260728-1400-chatlist-legend-fixed-item-size/plan.md`). Returns
|
|
383
|
+
// `undefined` (defer to measurement) for anything unpredictable, or
|
|
384
|
+
// carrying chrome this function doesn't account for (day-divider, flow-
|
|
385
|
+
// input-buttons row) — a resolved number is never re-measured, so
|
|
386
|
+
// guessing wrong here is permanent, not just approximate.
|
|
356
387
|
const getFixedItemSize = useCallback(
|
|
357
388
|
(item: MessageItem, index: number): number | undefined => {
|
|
358
389
|
const message = item;
|
|
@@ -432,13 +463,10 @@ export const ChatListLegend = memo(
|
|
|
432
463
|
]
|
|
433
464
|
);
|
|
434
465
|
|
|
435
|
-
// LegendList
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// message itself
|
|
439
|
-
// 'first' into a 'middle'. messageDataMap is memoised on exactly the inputs
|
|
440
|
-
// that can move those boundaries, so including it here is what keeps a
|
|
441
|
-
// stale tail from being left behind.
|
|
466
|
+
// LegendList only re-renders an item when [itemKey, item, extraData]
|
|
467
|
+
// changes. Cluster position depends on a message's NEIGHBOURS, which can
|
|
468
|
+
// shift (prepending history can turn a 'first' into a 'middle') without
|
|
469
|
+
// the message itself changing — messageDataMap here is what catches that.
|
|
442
470
|
const listExtraData = useMemo(
|
|
443
471
|
() => ({
|
|
444
472
|
messageDataMap,
|
|
@@ -457,59 +485,89 @@ export const ChatListLegend = memo(
|
|
|
457
485
|
);
|
|
458
486
|
|
|
459
487
|
const handleStartReached = useCallback(() => {
|
|
460
|
-
const willLoad = hasMoreOld && !moreOldLoading;
|
|
461
488
|
if (__DEV__) {
|
|
462
|
-
console.log(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
489
|
+
console.log(`${LOG_TAG} onStartReached fired`, {
|
|
490
|
+
conversationId,
|
|
491
|
+
hasUserScrolled: hasUserScrolledRef.current,
|
|
492
|
+
hasMoreOld,
|
|
493
|
+
moreOldLoading: useMessageStore.getState().moreOldLoading,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (!hasUserScrolledRef.current) {
|
|
497
|
+
const wouldHaveLoaded =
|
|
498
|
+
__DEV__ && hasMoreOld && !useMessageStore.getState().moreOldLoading;
|
|
499
|
+
if (wouldHaveLoaded) {
|
|
500
|
+
console.warn(
|
|
501
|
+
`${LOG_TAG} onStartReached fired before user scroll — suppressed`,
|
|
502
|
+
{ conversationId, hasMoreOld }
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return;
|
|
477
506
|
}
|
|
478
|
-
if (
|
|
507
|
+
if (hasMoreOld && !useMessageStore.getState().moreOldLoading) {
|
|
479
508
|
onLoadMoreOld?.(true);
|
|
480
509
|
}
|
|
481
|
-
}, [conversationId,
|
|
510
|
+
}, [conversationId, hasMoreOld, onLoadMoreOld]);
|
|
482
511
|
|
|
483
512
|
const handleEndReached = useCallback(() => {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
hasMoreNew,
|
|
495
|
-
moreNewLoading,
|
|
496
|
-
willLoad,
|
|
497
|
-
duringInitialSettle: !hasSettledRef.current,
|
|
498
|
-
}
|
|
499
|
-
);
|
|
513
|
+
if (!hasUserScrolledRef.current) {
|
|
514
|
+
const wouldHaveLoaded =
|
|
515
|
+
__DEV__ && hasMoreNew && !useMessageStore.getState().moreNewLoading;
|
|
516
|
+
if (wouldHaveLoaded) {
|
|
517
|
+
console.warn(
|
|
518
|
+
`${LOG_TAG} onEndReached fired before user scroll — suppressed`,
|
|
519
|
+
{ conversationId, hasMoreNew }
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
500
523
|
}
|
|
501
|
-
if (
|
|
524
|
+
if (hasMoreNew && !useMessageStore.getState().moreNewLoading) {
|
|
525
|
+
if (__DEV__) console.log(`${LOG_TAG} handleEndReached`);
|
|
502
526
|
onLoadMoreNew?.(true);
|
|
503
527
|
}
|
|
504
|
-
}, [conversationId,
|
|
528
|
+
}, [conversationId, hasMoreNew, onLoadMoreNew]);
|
|
529
|
+
|
|
530
|
+
const handleScrollBeginDrag = useCallback(() => {
|
|
531
|
+
hasUserScrolledRef.current = true;
|
|
532
|
+
}, []);
|
|
505
533
|
|
|
506
534
|
const handleScroll = useCallback(
|
|
507
535
|
(e: any) => {
|
|
508
|
-
const { contentOffset, contentSize, layoutMeasurement } =
|
|
536
|
+
const { contentOffset, contentSize, layoutMeasurement, contentInset } =
|
|
537
|
+
e.nativeEvent;
|
|
509
538
|
const isAtBottom =
|
|
510
539
|
contentOffset.y + layoutMeasurement.height >=
|
|
511
540
|
contentSize.height - 100;
|
|
512
541
|
|
|
542
|
+
if (__DEV__) {
|
|
543
|
+
const expected = expectedKeyboardInsetRef.current;
|
|
544
|
+
const actual = contentInset?.bottom ?? 0;
|
|
545
|
+
// `contentInset` failing to apply is a constant, systemic mismatch
|
|
546
|
+
// (present regardless of conversation length) — but it only shows
|
|
547
|
+
// up as a visible gap once the scroll position has actually moved
|
|
548
|
+
// past the real end of content. Gate on that so the warning fires
|
|
549
|
+
// only when the bug is actually visible, not on every keyboard
|
|
550
|
+
// open in every conversation.
|
|
551
|
+
const scrolledPastRealContentEnd =
|
|
552
|
+
contentOffset.y + layoutMeasurement.height > contentSize.height;
|
|
553
|
+
if (
|
|
554
|
+
expected > 0 &&
|
|
555
|
+
Math.abs(actual - expected) > 5 &&
|
|
556
|
+
scrolledPastRealContentEnd
|
|
557
|
+
) {
|
|
558
|
+
console.warn(
|
|
559
|
+
`${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.`,
|
|
560
|
+
{
|
|
561
|
+
expected,
|
|
562
|
+
actual,
|
|
563
|
+
contentOffsetY: contentOffset.y,
|
|
564
|
+
contentSizeHeight: contentSize.height,
|
|
565
|
+
layoutHeight: layoutMeasurement.height,
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
513
571
|
// Debounce animation updates
|
|
514
572
|
if (scrollDebounceRef.current) {
|
|
515
573
|
clearTimeout(scrollDebounceRef.current);
|
|
@@ -518,10 +576,10 @@ export const ChatListLegend = memo(
|
|
|
518
576
|
scrollDebounceRef.current = setTimeout(() => {
|
|
519
577
|
if (isAtBottomRef.current !== isAtBottom) {
|
|
520
578
|
isAtBottomRef.current = isAtBottom;
|
|
521
|
-
buttonOpacity.value = withTiming(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
579
|
+
buttonOpacity.value = withTiming(
|
|
580
|
+
isAtBottom ? 0 : 1,
|
|
581
|
+
BUTTON_FADE_CONFIG
|
|
582
|
+
);
|
|
525
583
|
}
|
|
526
584
|
if (isAtBottom) {
|
|
527
585
|
ChatMessageAPI.markConversationAsRead(conversationId || '');
|
|
@@ -534,15 +592,17 @@ export const ChatListLegend = memo(
|
|
|
534
592
|
const handleScrollToBottom = useCallback(() => {
|
|
535
593
|
if (hasMoreNew) {
|
|
536
594
|
onLoadMoreOld?.(false);
|
|
537
|
-
|
|
538
|
-
scrollMessageToEnd({ animated: true, closeKeyboard: false });
|
|
539
|
-
buttonOpacity.value = withTiming(0, {
|
|
540
|
-
duration: 200,
|
|
541
|
-
easing: Easing.out(Easing.ease),
|
|
542
|
-
});
|
|
543
|
-
isAtBottomRef.current = true;
|
|
544
|
-
setShowBadge(false);
|
|
595
|
+
return;
|
|
545
596
|
}
|
|
597
|
+
// `ChatEvent.ScrollToBottom` fires after every self-sent message, but
|
|
598
|
+
// the `lastMessageId` effect below already animates to it — re-scrolling
|
|
599
|
+
// here raced it and flickered. Only act when not already at bottom
|
|
600
|
+
// (FAB tap, or a send while scrolled away).
|
|
601
|
+
if (isAtBottomRef.current) return;
|
|
602
|
+
scrollMessageToEnd({ animated: true, closeKeyboard: false });
|
|
603
|
+
buttonOpacity.value = withTiming(0, BUTTON_FADE_CONFIG);
|
|
604
|
+
isAtBottomRef.current = true;
|
|
605
|
+
setShowBadge(false);
|
|
546
606
|
}, [hasMoreNew, onLoadMoreOld, scrollMessageToEnd, buttonOpacity]);
|
|
547
607
|
|
|
548
608
|
const animatedButtonStyle = useAnimatedStyle(() => ({
|
|
@@ -559,14 +619,104 @@ export const ChatListLegend = memo(
|
|
|
559
619
|
};
|
|
560
620
|
}, []);
|
|
561
621
|
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
622
|
+
// Diagnostic only (__DEV__) — keeps `expectedKeyboardInsetRef` in sync so
|
|
623
|
+
// `handleScroll` can detect the known contentInset-not-applying issue.
|
|
624
|
+
// JS-thread only (no worklet involved), so this carries none of the
|
|
625
|
+
// reentrant-lock crash risk `useKeyboardHandler`'s own worklet hit.
|
|
566
626
|
useEffect(() => {
|
|
567
|
-
|
|
627
|
+
if (!__DEV__) return;
|
|
628
|
+
const willShow = KeyboardEvents.addListener('keyboardWillShow', (e) => {
|
|
629
|
+
expectedKeyboardInsetRef.current = Math.max(
|
|
630
|
+
e.height - keyboardOffset,
|
|
631
|
+
0
|
|
632
|
+
);
|
|
633
|
+
});
|
|
634
|
+
const willHide = KeyboardEvents.addListener('keyboardWillHide', () => {
|
|
635
|
+
expectedKeyboardInsetRef.current = 0;
|
|
636
|
+
});
|
|
637
|
+
return () => {
|
|
638
|
+
willShow.remove();
|
|
639
|
+
willHide.remove();
|
|
640
|
+
};
|
|
641
|
+
}, [keyboardOffset]);
|
|
642
|
+
|
|
643
|
+
// This component can stay mounted across a conversationId change (e.g.
|
|
644
|
+
// navigation.replace reusing the same screen instance), so this ref must
|
|
645
|
+
// reset per-conversation — otherwise a leftover "user scrolled" flag from
|
|
646
|
+
// the previous conversation would wrongly let load-more fire on the
|
|
647
|
+
// newly opened one before any real scroll there.
|
|
648
|
+
useEffect(() => {
|
|
649
|
+
hasUserScrolledRef.current = false;
|
|
650
|
+
hasCorrectedInitialPaddingRef.current = false;
|
|
651
|
+
previousLastMessageIdRef.current = null;
|
|
568
652
|
}, [conversationId]);
|
|
569
653
|
|
|
654
|
+
// Single source of truth for "scroll to bottom on a new message" (self-
|
|
655
|
+
// sent or received). Skipped while prepending old history or scrolled
|
|
656
|
+
// away from the bottom. `moreOldLoading` is read via ref, not a
|
|
657
|
+
// dependency, so its own start/finish toggle doesn't re-run this.
|
|
658
|
+
useEffect(() => {
|
|
659
|
+
const previousLastMessageId = previousLastMessageIdRef.current;
|
|
660
|
+
previousLastMessageIdRef.current = lastMessageId;
|
|
661
|
+
|
|
662
|
+
// null → a real id is the initial fetch landing (initialScrollAtEnd +
|
|
663
|
+
// handleListLoad's job), not a new message; a real id → a different
|
|
664
|
+
// one is.
|
|
665
|
+
const shouldFire =
|
|
666
|
+
previousLastMessageId !== null &&
|
|
667
|
+
!!lastMessageId &&
|
|
668
|
+
!moreOldLoadingRef.current &&
|
|
669
|
+
isAtBottomRef.current;
|
|
670
|
+
|
|
671
|
+
if (__DEV__) {
|
|
672
|
+
console.log(
|
|
673
|
+
`${LOG_TAG} lastMessageId effect — ${shouldFire ? 'firing animated scroll' : 'skipped'}`,
|
|
674
|
+
{
|
|
675
|
+
conversationId,
|
|
676
|
+
lastMessageId,
|
|
677
|
+
initialPopulation: previousLastMessageId === null,
|
|
678
|
+
moreOldLoading: moreOldLoadingRef.current,
|
|
679
|
+
isAtBottom: isAtBottomRef.current,
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (!shouldFire) return;
|
|
685
|
+
|
|
686
|
+
scrollMessageToEnd({ animated: true, closeKeyboard: false });
|
|
687
|
+
}, [lastMessageId, scrollMessageToEnd, conversationId]);
|
|
688
|
+
|
|
689
|
+
const handleListLoad = useCallback(
|
|
690
|
+
(info: { elapsedTimeInMs: number }) => {
|
|
691
|
+
if (hasCorrectedInitialPaddingRef.current) return;
|
|
692
|
+
hasCorrectedInitialPaddingRef.current = true;
|
|
693
|
+
|
|
694
|
+
// Diagnostic: how long bootstrap + the flush below each take.
|
|
695
|
+
if (__DEV__) {
|
|
696
|
+
console.log(`${LOG_TAG} onLoad (bootstrap settle done)`, {
|
|
697
|
+
conversationId,
|
|
698
|
+
messagesLength: messages.length,
|
|
699
|
+
elapsedTimeInMs: info.elapsedTimeInMs,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const startedAt = __DEV__ ? Date.now() : 0;
|
|
704
|
+
const flushPromise = scrollMessageToEnd({
|
|
705
|
+
animated: false,
|
|
706
|
+
closeKeyboard: false,
|
|
707
|
+
});
|
|
708
|
+
if (__DEV__) {
|
|
709
|
+
flushPromise.then(() => {
|
|
710
|
+
console.log(`${LOG_TAG} handleListLoad flush resolved`, {
|
|
711
|
+
conversationId,
|
|
712
|
+
tookMs: Date.now() - startedAt,
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
[scrollMessageToEnd, conversationId, messages.length]
|
|
718
|
+
);
|
|
719
|
+
|
|
570
720
|
useEffect(() => {
|
|
571
721
|
const bottomSubscription = DeviceEventEmitter.addListener(
|
|
572
722
|
ChatEvent.ScrollToBottom,
|
|
@@ -576,7 +726,7 @@ export const ChatListLegend = memo(
|
|
|
576
726
|
const subscription = DeviceEventEmitter.addListener(
|
|
577
727
|
ChatEvent.ScrollToMessage,
|
|
578
728
|
({ clientMessageId }: ScrollToMessagePayload) => {
|
|
579
|
-
const index =
|
|
729
|
+
const index = messagesRef.current.findIndex(
|
|
580
730
|
(m) => m.clientMsgID === clientMessageId
|
|
581
731
|
);
|
|
582
732
|
|
|
@@ -621,11 +771,9 @@ export const ChatListLegend = memo(
|
|
|
621
771
|
subscription.remove();
|
|
622
772
|
};
|
|
623
773
|
}, [
|
|
624
|
-
messages,
|
|
625
774
|
applicationType,
|
|
626
775
|
searchMessages,
|
|
627
776
|
conversationId,
|
|
628
|
-
buttonOpacity,
|
|
629
777
|
handleScrollToBottom,
|
|
630
778
|
listRef,
|
|
631
779
|
]);
|
|
@@ -638,46 +786,57 @@ export const ChatListLegend = memo(
|
|
|
638
786
|
);
|
|
639
787
|
}
|
|
640
788
|
|
|
641
|
-
console.log('messages length: ', +messages.length);
|
|
642
789
|
return (
|
|
643
790
|
<KContainer.View flex background={KColors.white} style={styles.container}>
|
|
644
|
-
<
|
|
791
|
+
<KeyboardAwareLegendList
|
|
645
792
|
ref={listRef}
|
|
646
793
|
data={messages}
|
|
647
794
|
renderItem={renderItem}
|
|
648
|
-
|
|
795
|
+
// Deliberate lower bound (not a best-guess average) — see the
|
|
796
|
+
// comment above `hasCorrectedInitialPaddingRef`. A single-line
|
|
797
|
+
// bubble floors out around 38-44px, so this stays safely under it.
|
|
798
|
+
estimatedItemSize={36}
|
|
649
799
|
getItemType={getItemType}
|
|
650
800
|
getFixedItemSize={getFixedItemSize}
|
|
651
801
|
keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
|
|
652
|
-
recycleItems
|
|
802
|
+
recycleItems
|
|
653
803
|
extraData={listExtraData}
|
|
654
|
-
alignItemsAtEnd
|
|
804
|
+
alignItemsAtEnd={!isVeryShortConversation}
|
|
805
|
+
alwaysRender={{
|
|
806
|
+
top: 10,
|
|
807
|
+
}}
|
|
808
|
+
maintainScrollAtEnd
|
|
655
809
|
maintainVisibleContentPosition={maintainVisiblePositionConfig}
|
|
810
|
+
// See the comment above `maintainVisiblePositionConfig` — this is
|
|
811
|
+
// what keeps a history prepend's anchor item mounted through a slow
|
|
812
|
+
// fetch instead of the default 250px window losing it.
|
|
813
|
+
drawDistance={800}
|
|
656
814
|
freeze={freeze}
|
|
657
815
|
keyboardOffset={keyboardOffset}
|
|
658
816
|
scrollEventThrottle={16}
|
|
659
817
|
onScroll={handleScroll}
|
|
660
|
-
|
|
818
|
+
onScrollBeginDrag={handleScrollBeginDrag}
|
|
819
|
+
onLoad={handleListLoad}
|
|
820
|
+
contentContainerStyle={
|
|
821
|
+
isVeryShortConversation
|
|
822
|
+
? [styles.content, styles.contentBottomAnchored]
|
|
823
|
+
: styles.content
|
|
824
|
+
}
|
|
661
825
|
scrollIndicatorInsets={{ right: 1 }}
|
|
662
826
|
ListHeaderComponent={
|
|
663
827
|
<LegendChatLoadEarlier isLoading={moreOldLoading} />
|
|
664
828
|
}
|
|
665
|
-
onLoad={(info: { elapsedTimeInMs: number }) => {
|
|
666
|
-
hasSettledRef.current = true;
|
|
667
|
-
if (__DEV__) {
|
|
668
|
-
console.log('[LegendList] onLoad (initial settle done)', {
|
|
669
|
-
conversationId,
|
|
670
|
-
messagesLength: messages.length,
|
|
671
|
-
elapsedTimeInMs: info.elapsedTimeInMs,
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
}}
|
|
675
829
|
onStartReached={handleStartReached}
|
|
676
|
-
|
|
830
|
+
// LegendList only re-fires onStartReached after scrolling away from
|
|
831
|
+
// the threshold by 1.3x and back. At 0.5 that distance exceeded a
|
|
832
|
+
// short conversation's whole scrollable range, so the mount-time
|
|
833
|
+
// firing we suppress in handleStartReached "used up" the only
|
|
834
|
+
// trigger. 0.3 keeps that reset distance safely under a
|
|
835
|
+
// ~1.5-screen conversation; shorter ones can still hit it.
|
|
836
|
+
onStartReachedThreshold={0.2}
|
|
677
837
|
onEndReached={handleEndReached}
|
|
678
838
|
onEndReachedThreshold={0.2}
|
|
679
|
-
|
|
680
|
-
initialScrollIndex={messages.length - 1}
|
|
839
|
+
initialScrollAtEnd
|
|
681
840
|
/>
|
|
682
841
|
<Animated.View
|
|
683
842
|
style={[styles.scrollToBottomButton, animatedButtonStyle]}
|
|
@@ -713,6 +872,9 @@ const styles = StyleSheet.create({
|
|
|
713
872
|
paddingVertical: KSpacingValue['0.75rem'],
|
|
714
873
|
flexGrow: 1,
|
|
715
874
|
},
|
|
875
|
+
contentBottomAnchored: {
|
|
876
|
+
justifyContent: 'flex-end',
|
|
877
|
+
},
|
|
716
878
|
scrollToBottomButton: {
|
|
717
879
|
position: 'absolute',
|
|
718
880
|
bottom: 20,
|
|
@@ -147,14 +147,16 @@ export const LegendChatMessage = memo(
|
|
|
147
147
|
|
|
148
148
|
const hasMediaPress =
|
|
149
149
|
MEDIA_TYPES.includes(message?.contentType) && onMediaPress;
|
|
150
|
-
// AtTextMessage
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// a tap handler.
|
|
150
|
+
// AtTextMessage: mention+URL combo attaches its link-preview card via
|
|
151
|
+
// ex.urlMetadata (see TextMessage's doc comment) and/or the native
|
|
152
|
+
// AtTextElem.urls (SDK rc39+) for in-text link highlighting — either
|
|
153
|
+
// way the card/text needs a tap handler. QuoteMessage: same for a
|
|
154
|
+
// reply's own text via the native QuoteElem.urls (see QuotedMessage).
|
|
154
155
|
const hasUrlPress =
|
|
155
156
|
onPressUrl &&
|
|
156
157
|
(message?.contentType === MessageType.UrlTextMessage ||
|
|
157
|
-
message?.contentType === MessageType.AtTextMessage
|
|
158
|
+
message?.contentType === MessageType.AtTextMessage ||
|
|
159
|
+
message?.contentType === MessageType.QuoteMessage);
|
|
158
160
|
const canRevealTime = !TIME_REVEAL_EXCLUDED_TYPES.includes(
|
|
159
161
|
message?.contentType
|
|
160
162
|
);
|