fix(chat): restore scroll after image viewer return
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user