fix(chat): drive anchored end-follow from the list's total size
Phase 2 of the anchored turn (following the live edge once the reply outgrows the viewport) never ran: the reveal correction was triggered only by entries-array changes, and the streaming tail grows inside a single row without touching the array. The hook now also subscribes to LegendList's totalSize and re-runs the same guarded correction on every content growth.
This commit is contained in:
@@ -56,7 +56,13 @@ export interface AnimationHandlers {
|
|||||||
// hook stays testable without a renderer and does not hard-depend on the list
|
// hook stays testable without a renderer and does not hard-depend on the list
|
||||||
// implementation.
|
// implementation.
|
||||||
export interface TimelineListHandle {
|
export interface TimelineListHandle {
|
||||||
getState: () => TimelineListMeasurementState & { readonly scroll: number };
|
getState: () => TimelineListMeasurementState & {
|
||||||
|
readonly scroll: number;
|
||||||
|
readonly listen?: (
|
||||||
|
listenerType: 'totalSize',
|
||||||
|
callback: (value: number) => void,
|
||||||
|
) => () => void;
|
||||||
|
};
|
||||||
getScrollableNode: () => HTMLElement | null;
|
getScrollableNode: () => HTMLElement | null;
|
||||||
scrollToEnd: (options?: { animated?: boolean }) => unknown;
|
scrollToEnd: (options?: { animated?: boolean }) => unknown;
|
||||||
scrollToOffset: (params: { offset: number; animated?: boolean }) => unknown;
|
scrollToOffset: (params: { offset: number; animated?: boolean }) => unknown;
|
||||||
@@ -555,6 +561,22 @@ export const useChatTimelineScroll = ({
|
|||||||
});
|
});
|
||||||
}, [isLiveFollowActive, realContentOverflowsViewport]);
|
}, [isLiveFollowActive, realContentOverflowsViewport]);
|
||||||
|
|
||||||
|
// The streaming tail grows inside one row without changing the entries
|
||||||
|
// array, so data-change callbacks are silent for the entire stream. The
|
||||||
|
// list's total content size is the authoritative growth signal; every
|
||||||
|
// change re-runs the same guarded correction.
|
||||||
|
const onTimelineDataChangeRef = React.useRef(onTimelineDataChange);
|
||||||
|
onTimelineDataChangeRef.current = onTimelineDataChange;
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!scrollNode) return;
|
||||||
|
const listen = listRef.current?.getState().listen;
|
||||||
|
if (!listen) return;
|
||||||
|
const unsubscribe = listen('totalSize', () => {
|
||||||
|
onTimelineDataChangeRef.current();
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
}, [scrollNode]);
|
||||||
|
|
||||||
// ── gesture opt-out ─────────────────────────────────────────────────────
|
// ── gesture opt-out ─────────────────────────────────────────────────────
|
||||||
const onManualNavigationRef = React.useRef(onManualNavigation);
|
const onManualNavigationRef = React.useRef(onManualNavigation);
|
||||||
onManualNavigationRef.current = onManualNavigation;
|
onManualNavigationRef.current = onManualNavigation;
|
||||||
|
|||||||
Reference in New Issue
Block a user