fix(chat): restore scroll after image viewer return

This commit is contained in:
2026-07-01 17:15:38 +08:00
parent 5f9df58dbe
commit fa5b380577
4 changed files with 205 additions and 4 deletions
+84 -2
View File
@@ -12,10 +12,16 @@
* - 新消息到达时自动滚到底部
* - 用户向上滚动时不强制拉回(保持阅读上下文)
*/
import { useEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useRef } from "react";
import type { UiMessage } from "@/data/dto/chat";
import {
consumeChatScrollSnapshot,
getChatScrollRestoreTop,
listenChatScrollSnapshotSave,
saveChatScrollSnapshot,
} from "../chat-scroll-session";
import { AiDisclosureBanner } from "./ai-disclosure-banner";
import { DateHeader } from "./date-header";
import { LottieMessageBubble } from "./lottie-message-bubble";
@@ -42,6 +48,23 @@ export function ChatArea({
}: ChatAreaProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const prevLengthRef = useRef(messages.length);
const restoredScrollRef = useRef(false);
const saveFrameRef = useRef<number | null>(null);
const saveCurrentScrollSnapshotNow = () => {
const scrollNode = scrollRef.current;
if (!scrollNode) return;
saveChatScrollSnapshot(scrollNode);
};
const saveCurrentScrollSnapshot = () => {
if (saveFrameRef.current !== null) return;
saveFrameRef.current = window.requestAnimationFrame(() => {
saveFrameRef.current = null;
saveCurrentScrollSnapshotNow();
});
};
// 新消息 → 滚到底部
useEffect(() => {
@@ -54,8 +77,67 @@ export function ChatArea({
}
}, [messages.length]);
useLayoutEffect(() => {
if (restoredScrollRef.current || messages.length === 0) return;
const scrollNode = scrollRef.current;
const snapshot = consumeChatScrollSnapshot();
if (!scrollNode || snapshot === null) return;
restoredScrollRef.current = true;
const restoreScroll = () => {
scrollNode.scrollTop = getChatScrollRestoreTop(scrollNode, snapshot);
};
restoreScroll();
const frame = window.requestAnimationFrame(restoreScroll);
const timer = window.setTimeout(restoreScroll, 120);
const resizeObserver =
typeof ResizeObserver === "undefined"
? null
: new ResizeObserver(restoreScroll);
Array.from(scrollNode.children).forEach((child) => {
resizeObserver?.observe(child);
});
const observerTimer = window.setTimeout(() => {
resizeObserver?.disconnect();
}, 800);
const lateRestoreTimer = window.setTimeout(restoreScroll, 600);
return () => {
window.cancelAnimationFrame(frame);
window.clearTimeout(timer);
window.clearTimeout(observerTimer);
window.clearTimeout(lateRestoreTimer);
resizeObserver?.disconnect();
};
}, [messages.length]);
useEffect(() => {
const scrollNode = scrollRef.current;
const stopListening = listenChatScrollSnapshotSave(
saveCurrentScrollSnapshotNow,
);
return () => {
stopListening();
if (saveFrameRef.current !== null) {
window.cancelAnimationFrame(saveFrameRef.current);
saveFrameRef.current = null;
}
if (!scrollNode) return;
saveChatScrollSnapshot(scrollNode);
};
}, []);
return (
<main ref={scrollRef} className={styles.area} aria-label="Chat messages">
<main
ref={scrollRef}
className={styles.area}
aria-label="Chat messages"
onScroll={saveCurrentScrollSnapshot}
>
<AiDisclosureBanner />
{renderMessagesWithDateHeaders(