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:
Bohdan Triapitsyn
2026-08-25 01:54:52 +03:00
parent 955b2cc812
commit 6a00d6a830
+23 -1
View File
@@ -56,7 +56,13 @@ export interface AnimationHandlers {
// hook stays testable without a renderer and does not hard-depend on the list
// implementation.
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;
scrollToEnd: (options?: { animated?: boolean }) => unknown;
scrollToOffset: (params: { offset: number; animated?: boolean }) => unknown;
@@ -555,6 +561,22 @@ export const useChatTimelineScroll = ({
});
}, [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 ─────────────────────────────────────────────────────
const onManualNavigationRef = React.useRef(onManualNavigation);
onManualNavigationRef.current = onManualNavigation;