feat(chat): implement pull-to-refresh functionality and pagination for chat history

This commit is contained in:
2026-07-15 18:19:35 +08:00
parent 05ca15be48
commit c37a2f9040
21 changed files with 892 additions and 66 deletions
@@ -144,12 +144,47 @@ describe("ChatArea scrolling", () => {
expect(scrollNode.scrollTop).toBe(100);
});
it("preserves the visible message after older history is prepended", () => {
scrollHeight = 1_000;
clientHeight = 300;
const onLoadMoreHistory = vi.fn();
const history = [createMessage("history-1")];
renderChatArea(root, history, true, {
canLoadMoreHistory: true,
onLoadMoreHistory,
});
const scrollNode = getScrollNode(container);
scrollNode.scrollTop = 0;
act(() => scrollNode.dispatchEvent(new Event("scroll", { bubbles: true })));
pull(scrollNode, 180);
expect(onLoadMoreHistory).toHaveBeenCalledOnce();
scrollHeight = 1_400;
renderChatArea(
root,
[createMessage("older-1"), ...history],
true,
{
canLoadMoreHistory: true,
onLoadMoreHistory,
},
);
expect(scrollNode.scrollTop).toBe(400);
});
});
function renderChatArea(
root: Root,
messages: readonly UiMessage[],
initialScrollReady: boolean,
historyOptions: {
canLoadMoreHistory?: boolean;
isLoadingMoreHistory?: boolean;
onLoadMoreHistory?: () => void;
} = {},
): void {
act(() => {
root.render(
@@ -157,11 +192,32 @@ function renderChatArea(
messages={messages}
isReplyingAI={false}
initialScrollReady={initialScrollReady}
{...historyOptions}
/>,
);
});
}
function pull(target: HTMLElement, distance: number): void {
act(() => {
target.dispatchEvent(createTouchEvent("touchstart", 0));
target.dispatchEvent(createTouchEvent("touchmove", distance));
target.dispatchEvent(createTouchEvent("touchend", distance, true));
});
}
function createTouchEvent(
type: string,
clientY: number,
ended = false,
): Event {
const event = new Event(type, { bubbles: true, cancelable: true });
Object.defineProperty(event, "touches", {
value: ended ? [] : [{ identifier: 1, clientY }],
});
return event;
}
function createMessage(id: string): UiMessage {
return {
id,