Message List: Suppress spurious infinite scroll triggers on Safari
In Safari the `IntersectionObserver` can deliver entries computed between DOM mutation and the deferred scroll restore, causing the history trigger to fire repeatedly and the viewport to jump. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
569082537f
commit
6cff49e8b7
@ -271,6 +271,9 @@ const MessageList = ({
|
||||
const memoUnreadDividerBeforeIdRef = useRef<number | undefined>();
|
||||
const memoFocusingIdRef = useRef<number>();
|
||||
const isScrollTopJustUpdatedRef = useRef(false);
|
||||
// Suppresses spurious load-more triggers caused by Safari delivering stale
|
||||
// `IntersectionObserver` entries between DOM mutation and scroll restore
|
||||
const isReplacingHistoryRef = useRef(false);
|
||||
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
|
||||
const scrollSnapDisabledTimerRef = useRef<number>();
|
||||
const typingDraftSnapTriggeredIdRef = useRef<number>();
|
||||
@ -636,7 +639,10 @@ const MessageList = ({
|
||||
});
|
||||
|
||||
useSyncEffect(
|
||||
() => forceMeasure(() => rememberScrollPositionRef.current()),
|
||||
() => {
|
||||
isReplacingHistoryRef.current = true;
|
||||
forceMeasure(() => rememberScrollPositionRef.current());
|
||||
},
|
||||
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
||||
[messageIds, isViewportNewest, rememberScrollPositionRef],
|
||||
);
|
||||
@ -770,6 +776,9 @@ const MessageList = ({
|
||||
|
||||
return () => {
|
||||
resetScroll(container, Math.ceil(newScrollTop));
|
||||
requestMeasure(() => {
|
||||
isReplacingHistoryRef.current = false;
|
||||
});
|
||||
restartCurrentScrollAnimation();
|
||||
|
||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||
@ -907,6 +916,7 @@ const MessageList = ({
|
||||
anchorIdRef={anchorIdRef}
|
||||
memoUnreadDividerBeforeIdRef={memoUnreadDividerBeforeIdRef}
|
||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||
isReplacingHistoryRef={isReplacingHistoryRef}
|
||||
threadId={threadId}
|
||||
type={type}
|
||||
isReady={isReady}
|
||||
|
||||
@ -69,6 +69,7 @@ interface OwnProps {
|
||||
anchorIdRef: { current: string | undefined };
|
||||
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
||||
memoFirstUnreadIdRef: { current: number | undefined };
|
||||
isReplacingHistoryRef: { current: boolean };
|
||||
type: MessageListType;
|
||||
isReady: boolean;
|
||||
hasLinkedChat: boolean | undefined;
|
||||
@ -109,6 +110,7 @@ const MessageListContent = ({
|
||||
anchorIdRef,
|
||||
memoUnreadDividerBeforeIdRef,
|
||||
memoFirstUnreadIdRef,
|
||||
isReplacingHistoryRef,
|
||||
type,
|
||||
isReady,
|
||||
hasLinkedChat,
|
||||
@ -164,6 +166,7 @@ const MessageListContent = ({
|
||||
isViewportNewest,
|
||||
isUnread,
|
||||
isReady,
|
||||
isReplacingHistoryRef,
|
||||
onScrollDownToggle,
|
||||
onNotchToggle,
|
||||
});
|
||||
|
||||
@ -32,6 +32,7 @@ export default function useScrollHooks({
|
||||
isViewportNewest,
|
||||
isUnread,
|
||||
isReady,
|
||||
isReplacingHistoryRef,
|
||||
onScrollDownToggle,
|
||||
onNotchToggle,
|
||||
}: {
|
||||
@ -42,6 +43,7 @@ export default function useScrollHooks({
|
||||
isViewportNewest: boolean;
|
||||
isUnread: boolean;
|
||||
isReady: boolean;
|
||||
isReplacingHistoryRef: { current: boolean };
|
||||
onScrollDownToggle: BooleanToVoidFunction | undefined;
|
||||
onNotchToggle: AnyToVoidFunction | undefined;
|
||||
}) {
|
||||
@ -109,6 +111,10 @@ export default function useScrollHooks({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReplacingHistoryRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach(({ isIntersecting, target }) => {
|
||||
if (!isIntersecting) return;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user