fix: keep scroll position when switching between cached conversations
This commit is contained in:
@@ -4777,9 +4777,16 @@ describe("App", () => {
|
||||
});
|
||||
expect(messageRenderProbe).not.toHaveBeenCalled();
|
||||
expect(firstPane?.querySelectorAll(".message")).toHaveLength(160);
|
||||
const paneOrder = (): string[] =>
|
||||
[...container.querySelectorAll<HTMLElement>(".chat-history-pane")].map(
|
||||
(pane) => pane.dataset.conversationId ?? "",
|
||||
);
|
||||
const warmPaneOrder = paneOrder();
|
||||
fireEvent.click(screen.getByText("第一段长会话").closest("button")!);
|
||||
|
||||
expect(await screen.findByText("第一段历史 001")).toBeInTheDocument();
|
||||
// Chromium resets scrollTop when React moves a kept pane; jsdom does not.
|
||||
expect(paneOrder()).toEqual(warmPaneOrder);
|
||||
const restoredFirstPane = container.querySelector<HTMLElement>(
|
||||
`[data-conversation-id="${firstConversationId}"]`,
|
||||
);
|
||||
|
||||
@@ -3594,18 +3594,26 @@ function App(): React.JSX.Element {
|
||||
() => new Set(cachedWorkspaceViews.map((entry) => entry.key)),
|
||||
[cachedWorkspaceViews],
|
||||
);
|
||||
// Kept panes must keep a stable DOM order. Reordering keyed siblings makes
|
||||
// React detach and reinsert scroll containers, which resets their scrollTop.
|
||||
const conversationPaneOrderRef = useRef<string[]>([]);
|
||||
const cachedConversations = useMemo(() => {
|
||||
const conversationById = new Map(
|
||||
conversations.map((conversation) => [conversation.id, conversation]),
|
||||
);
|
||||
const cachedIds = [
|
||||
activeId,
|
||||
...cachedConversationViews.map((entry) => entry.key),
|
||||
].filter(
|
||||
(conversationId, index, values) =>
|
||||
conversationId && values.indexOf(conversationId) === index,
|
||||
const cachedIds = new Set(
|
||||
[activeId, ...cachedConversationViews.map((entry) => entry.key)].filter(
|
||||
Boolean,
|
||||
),
|
||||
);
|
||||
return cachedIds.flatMap((conversationId) => {
|
||||
const paneOrder = conversationPaneOrderRef.current.filter((id) =>
|
||||
cachedIds.has(id),
|
||||
);
|
||||
cachedIds.forEach((id) => {
|
||||
if (!paneOrder.includes(id)) paneOrder.push(id);
|
||||
});
|
||||
conversationPaneOrderRef.current = paneOrder;
|
||||
return paneOrder.flatMap((conversationId) => {
|
||||
const conversation = conversationById.get(conversationId);
|
||||
return conversation ? [conversation] : [];
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
|
||||
const pinnedToBottomRef = useRef(scrollSnapshot?.pinnedToBottom ?? true);
|
||||
const latestScrollSnapshotRef = useRef(scrollSnapshot);
|
||||
const restorePendingRef = useRef(true);
|
||||
const wasActiveRef = useRef(false);
|
||||
const prependScrollPositionRef = useRef<
|
||||
| {
|
||||
scrollHeight: number;
|
||||
@@ -234,12 +235,15 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!active) {
|
||||
wasActiveRef.current = false;
|
||||
return;
|
||||
}
|
||||
const scrollContainer = scrollRef.current;
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
const activated = !wasActiveRef.current;
|
||||
wasActiveRef.current = true;
|
||||
if (restorePendingRef.current) {
|
||||
restorePendingRef.current = false;
|
||||
if (scrollSnapshot && !scrollSnapshot.pinnedToBottom) {
|
||||
@@ -247,6 +251,14 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
|
||||
scrollContainer.scrollTop = scrollSnapshot.scrollTop;
|
||||
return;
|
||||
}
|
||||
} else if (activated && !pinnedToBottomRef.current) {
|
||||
// Hidden panes may lose scrollTop when the browser detaches or re-lays
|
||||
// them out; restore the reader's last position instead of the top.
|
||||
const saved = latestScrollSnapshotRef.current;
|
||||
if (saved && !saved.pinnedToBottom) {
|
||||
scrollContainer.scrollTop = saved.scrollTop;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pinnedToBottomRef.current) {
|
||||
scrollContainer.scrollTo({
|
||||
|
||||
Reference in New Issue
Block a user