fix(chat): stop double scroll write on prepend that resonates into oscillation
When older history is prepended while the viewport is pinned to the bottom, the
timeline controller wrote the re-pin manually (scrollTop += delta). That write
is not flagged as programmatic, so useChatAutoFollow's scroll handler treated it
as movement and issued its own correcting scroll — a redundant up/down move on
every prepend. On most setups it settles after one move, but with different
virtualizer measurement/timing it never converges, producing the reported
infinite up/down scroll glitch.
When pinned, delegate the prepend re-pin to auto-follow's goToBottom('instant'):
a single authoritative write to the bottom that IS marked programmatic, so
auto-follow ignores it instead of fighting it. The released case (user reading
back through history) is unchanged and still preserves the read position.
This also covers the on-open history auto-load (loadEarlierIfPinnedViewport-
Underfilled), which only runs while pinned, so its prepends now go through the
single writer too.
This commit is contained in:
@@ -350,6 +350,25 @@ export const useChatTimelineController = ({
|
|||||||
const container = scrollRef.current;
|
const container = scrollRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
// Bottom-pinned: auto-follow is the single owner of the scroll position.
|
||||||
|
// Route the prepend re-pin through goToBottom (a programmatic, authoritative
|
||||||
|
// instant write to the bottom) rather than a manual scrollTop adjustment. A
|
||||||
|
// manual write here is NOT marked programmatic, so auto-follow's scroll
|
||||||
|
// handler treats it as movement and issues its own correcting scroll — a
|
||||||
|
// redundant up/down move on every prepend that, on some setups, resonates
|
||||||
|
// into the reported infinite oscillation. Delegating keeps exactly one
|
||||||
|
// writer and no fight.
|
||||||
|
if (isPinnedRef.current) {
|
||||||
|
prePrependScrollRef.current = null;
|
||||||
|
goToBottom('instant');
|
||||||
|
prependTrackingRef.current = {
|
||||||
|
oldestId: renderedMessages[0]?.info?.id ?? null,
|
||||||
|
newestId: renderedMessages[renderedMessages.length - 1]?.info?.id ?? null,
|
||||||
|
scrollHeight: container.scrollHeight,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const snap = prePrependScrollRef.current;
|
const snap = prePrependScrollRef.current;
|
||||||
if (snap) {
|
if (snap) {
|
||||||
prePrependScrollRef.current = null;
|
prePrependScrollRef.current = null;
|
||||||
@@ -395,7 +414,7 @@ export const useChatTimelineController = ({
|
|||||||
newestId: renderedMessages[renderedMessages.length - 1]?.info?.id ?? null,
|
newestId: renderedMessages[renderedMessages.length - 1]?.info?.id ?? null,
|
||||||
scrollHeight: container.scrollHeight,
|
scrollHeight: container.scrollHeight,
|
||||||
};
|
};
|
||||||
}, [renderedMessages, scrollRef, restoreViewportAnchor]);
|
}, [renderedMessages, scrollRef, restoreViewportAnchor, goToBottom]);
|
||||||
|
|
||||||
const revealBufferedTurns = React.useCallback(async (): Promise<boolean> => false, []);
|
const revealBufferedTurns = React.useCallback(async (): Promise<boolean> => false, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user