fix(chat): re-pin to the measured content end after a resize, not the list's content length

Opening or closing the context panel re-wrapped the timeline; the settle
handler then scrolled to LegendList's total content length, which still
carried pre-wrap sizes for unmounted rows, leaving a viewport-sized blank
tail under the last message.
This commit is contained in:
Bohdan Triapitsyn
2026-08-29 01:51:47 +03:00
parent 8d80224a1e
commit e26f647404
3 changed files with 105 additions and 9 deletions
+28 -9
View File
@@ -8,6 +8,7 @@ import {
CHAT_LIST_ANCHOR_OFFSET,
getAnchoredTurnMetrics,
getRowBottom,
resolveRealContentEndOffset,
resolveTimelineIsAtEnd,
TIMELINE_FOLLOW_REARM_THRESHOLD_PX,
type TimelineListMeasurementState,
@@ -588,7 +589,25 @@ export const useChatTimelineScroll = ({
quietTimer = null;
widthResizingRef.current = false;
if (isAtEndRef.current && pendingAnchorRef.current === null) {
void listRef.current?.scrollToEnd({ animated: false });
// Not scrollToEnd: the list's end offset comes from the
// total content length, which still carries pre-wrap row
// sizes (and any reserved anchored end space) right after a
// width change. Landing there parks the last row near the
// top of the viewport with a blank tail below it. Target
// the measured bottom of the last real row instead.
const list = listRef.current;
const state = list?.getState();
const offset = state
? resolveRealContentEndOffset({
state,
composerOverlayHeight: composerOverlayHeightRef.current,
})
: null;
if (list && offset !== null) {
void list.scrollToOffset({ offset, animated: false });
} else {
void list?.scrollToEnd({ animated: false });
}
}
}, 350);
});
@@ -637,15 +656,15 @@ export const useChatTimelineScroll = ({
const lastIndex = state.data.length - 1;
const lastBottom = lastIndex >= 0 ? getRowBottom(state, lastIndex) : null;
if (lastBottom !== null && state.scroll > lastBottom) {
const visibleLength = Math.max(
0,
state.scrollLength - composerOverlayHeightRef.current - CHAT_LIST_ANCHOR_OFFSET,
);
void list.scrollToOffset({
offset: Math.max(0, lastBottom - visibleLength),
animated: false,
const offset = resolveRealContentEndOffset({
state,
composerOverlayHeight: composerOverlayHeightRef.current,
extraInset: CHAT_LIST_ANCHOR_OFFSET,
});
return;
if (offset !== null) {
void list.scrollToOffset({ offset, animated: false });
return;
}
}
}
}