Skip to content

Commit be8ed26

Browse files
authored
fix(MessageList): prevent jump-to-message snap-back from bottom autoscroll (#3109)
1 parent b4ed464 commit be8ed26

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

src/components/MessageList/MessageList.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const MessageListWithContext = (props: MessageListWithContextProps) => {
140140
[messages],
141141
);
142142
const isJumpingToLatest = jumpToLatestPhase !== 'idle';
143+
const isHighlightedJumpRequested = !!highlightedMessageId;
143144
// Highlighted jumps temporarily disable prepend pagination so a target
144145
// message rendered near the top does not immediately load the previous page.
145146
const isJumpingToHighlightedMessage = highlightedJumpPhase !== 'idle';
@@ -152,7 +153,8 @@ const MessageListWithContext = (props: MessageListWithContextProps) => {
152153
scrollToBottom,
153154
wrapperRect,
154155
} = useScrollLocationLogic({
155-
disableAutoScrollToBottom: isJumpingToLatest || justReachedLatestMergedSet,
156+
disableAutoScrollToBottom:
157+
isJumpingToLatest || isHighlightedJumpRequested || justReachedLatestMergedSet,
156158
disableScrollManagement: isJumpingToLatest || isJumpingToHighlightedMessage,
157159
hasMoreNewer,
158160
listElement,

src/components/MessageList/hooks/MessageList/useScrollLocationLogic.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export const useScrollLocationLogic = (params: UseScrollLocationLogicParams) =>
6060
const closeToTop = useRef(false);
6161
const previousScrollTopRef = useRef(0);
6262
const previousMessagesLengthRef = useRef(messages.length);
63+
const previousDisableAutoScrollToBottomRef = useRef(disableAutoScrollToBottom);
64+
const previousDisableAutoScrollSettleRef = useRef(disableAutoScrollToBottom);
6365
const anchorRestoreCleanupRef = useRef<(() => void) | null>(null);
6466

6567
const captureAnchor = useCallback(() => {
@@ -249,6 +251,16 @@ export const useScrollLocationLogic = (params: UseScrollLocationLogicParams) =>
249251
* path where existing viewport position must be preserved.
250252
*/
251253
useLayoutEffect(() => {
254+
const disableAutoScrollJustReleased =
255+
previousDisableAutoScrollToBottomRef.current && !disableAutoScrollToBottom;
256+
previousDisableAutoScrollToBottomRef.current = disableAutoScrollToBottom;
257+
258+
// Re-enabling auto-scroll should not immediately force a jump to bottom.
259+
// This avoids snap-back after temporary suppression (e.g. jump-to-message).
260+
if (disableAutoScrollJustReleased) {
261+
return;
262+
}
263+
252264
if (listElement) {
253265
setWrapperRect(listElement.getBoundingClientRect());
254266
}
@@ -275,6 +287,19 @@ export const useScrollLocationLogic = (params: UseScrollLocationLogicParams) =>
275287
* to catch late layout updates without keeping the list in a prolonged lock loop.
276288
*/
277289
useLayoutEffect(() => {
290+
const disableAutoScrollJustReleased =
291+
previousDisableAutoScrollSettleRef.current && !disableAutoScrollToBottom;
292+
previousDisableAutoScrollSettleRef.current = disableAutoScrollToBottom;
293+
294+
// Skip one settle cycle when auto-scroll suppression is released.
295+
// Without this guard, a jump-to-message flow can scroll to the target and then
296+
// get pulled back down by the delayed "keep pinned to bottom" retries
297+
// (80/260/420/900/1700ms), which looks like a snap-back to the latest message.
298+
// Letting this transition frame pass preserves the jump destination.
299+
if (disableAutoScrollJustReleased) {
300+
return;
301+
}
302+
278303
if (
279304
!listElement ||
280305
disableAutoScrollToBottom ||

0 commit comments

Comments
 (0)