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 memoUnreadDividerBeforeIdRef = useRef<number | undefined>();
|
||||||
const memoFocusingIdRef = useRef<number>();
|
const memoFocusingIdRef = useRef<number>();
|
||||||
const isScrollTopJustUpdatedRef = useRef(false);
|
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 shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
|
||||||
const scrollSnapDisabledTimerRef = useRef<number>();
|
const scrollSnapDisabledTimerRef = useRef<number>();
|
||||||
const typingDraftSnapTriggeredIdRef = useRef<number>();
|
const typingDraftSnapTriggeredIdRef = useRef<number>();
|
||||||
@ -636,7 +639,10 @@ const MessageList = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useSyncEffect(
|
useSyncEffect(
|
||||||
() => forceMeasure(() => rememberScrollPositionRef.current()),
|
() => {
|
||||||
|
isReplacingHistoryRef.current = true;
|
||||||
|
forceMeasure(() => rememberScrollPositionRef.current());
|
||||||
|
},
|
||||||
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
||||||
[messageIds, isViewportNewest, rememberScrollPositionRef],
|
[messageIds, isViewportNewest, rememberScrollPositionRef],
|
||||||
);
|
);
|
||||||
@ -770,6 +776,9 @@ const MessageList = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resetScroll(container, Math.ceil(newScrollTop));
|
resetScroll(container, Math.ceil(newScrollTop));
|
||||||
|
requestMeasure(() => {
|
||||||
|
isReplacingHistoryRef.current = false;
|
||||||
|
});
|
||||||
restartCurrentScrollAnimation();
|
restartCurrentScrollAnimation();
|
||||||
|
|
||||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||||
@ -907,6 +916,7 @@ const MessageList = ({
|
|||||||
anchorIdRef={anchorIdRef}
|
anchorIdRef={anchorIdRef}
|
||||||
memoUnreadDividerBeforeIdRef={memoUnreadDividerBeforeIdRef}
|
memoUnreadDividerBeforeIdRef={memoUnreadDividerBeforeIdRef}
|
||||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||||
|
isReplacingHistoryRef={isReplacingHistoryRef}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
type={type}
|
type={type}
|
||||||
isReady={isReady}
|
isReady={isReady}
|
||||||
|
|||||||
@ -69,6 +69,7 @@ interface OwnProps {
|
|||||||
anchorIdRef: { current: string | undefined };
|
anchorIdRef: { current: string | undefined };
|
||||||
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
||||||
memoFirstUnreadIdRef: { current: number | undefined };
|
memoFirstUnreadIdRef: { current: number | undefined };
|
||||||
|
isReplacingHistoryRef: { current: boolean };
|
||||||
type: MessageListType;
|
type: MessageListType;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
hasLinkedChat: boolean | undefined;
|
hasLinkedChat: boolean | undefined;
|
||||||
@ -109,6 +110,7 @@ const MessageListContent = ({
|
|||||||
anchorIdRef,
|
anchorIdRef,
|
||||||
memoUnreadDividerBeforeIdRef,
|
memoUnreadDividerBeforeIdRef,
|
||||||
memoFirstUnreadIdRef,
|
memoFirstUnreadIdRef,
|
||||||
|
isReplacingHistoryRef,
|
||||||
type,
|
type,
|
||||||
isReady,
|
isReady,
|
||||||
hasLinkedChat,
|
hasLinkedChat,
|
||||||
@ -164,6 +166,7 @@ const MessageListContent = ({
|
|||||||
isViewportNewest,
|
isViewportNewest,
|
||||||
isUnread,
|
isUnread,
|
||||||
isReady,
|
isReady,
|
||||||
|
isReplacingHistoryRef,
|
||||||
onScrollDownToggle,
|
onScrollDownToggle,
|
||||||
onNotchToggle,
|
onNotchToggle,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export default function useScrollHooks({
|
|||||||
isViewportNewest,
|
isViewportNewest,
|
||||||
isUnread,
|
isUnread,
|
||||||
isReady,
|
isReady,
|
||||||
|
isReplacingHistoryRef,
|
||||||
onScrollDownToggle,
|
onScrollDownToggle,
|
||||||
onNotchToggle,
|
onNotchToggle,
|
||||||
}: {
|
}: {
|
||||||
@ -42,6 +43,7 @@ export default function useScrollHooks({
|
|||||||
isViewportNewest: boolean;
|
isViewportNewest: boolean;
|
||||||
isUnread: boolean;
|
isUnread: boolean;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
|
isReplacingHistoryRef: { current: boolean };
|
||||||
onScrollDownToggle: BooleanToVoidFunction | undefined;
|
onScrollDownToggle: BooleanToVoidFunction | undefined;
|
||||||
onNotchToggle: AnyToVoidFunction | undefined;
|
onNotchToggle: AnyToVoidFunction | undefined;
|
||||||
}) {
|
}) {
|
||||||
@ -109,6 +111,10 @@ export default function useScrollHooks({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isReplacingHistoryRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
entries.forEach(({ isIntersecting, target }) => {
|
entries.forEach(({ isIntersecting, target }) => {
|
||||||
if (!isIntersecting) return;
|
if (!isIntersecting) return;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user