mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix: support touch history loading
This commit is contained in:
@@ -695,6 +695,30 @@ describe("chat pane catalog session lifecycle", () => {
|
||||
expect(pane.loadOlderMessages).toHaveBeenCalledOnce();
|
||||
expect(pane.historyAutoLoadBlocked).toBe(false);
|
||||
});
|
||||
|
||||
it("loads a blocked unscrollable transcript from a downward touch pull", async () => {
|
||||
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
|
||||
const { pane } = createTestChatPane({ client, sessions: {} as SessionCapability });
|
||||
pane.historyAutoLoadBlocked = true;
|
||||
pane.hasOlderMessages = vi.fn(() => true);
|
||||
pane.loadOlderMessages = vi.fn(async () => undefined);
|
||||
vi.stubGlobal("IntersectionObserver", undefined);
|
||||
const thread = document.createElement("div");
|
||||
const touchEvent = (type: string, clientY: number) => {
|
||||
const event = new TouchEvent(type);
|
||||
Object.defineProperty(event, "currentTarget", { value: thread });
|
||||
Object.defineProperty(event, "touches", { value: [{ clientY }] });
|
||||
return event;
|
||||
};
|
||||
|
||||
pane.handleTranscriptHistoryIntent(touchEvent("touchstart", 100));
|
||||
pane.handleTranscriptHistoryIntent(touchEvent("touchmove", 106));
|
||||
pane.handleTranscriptHistoryIntent(touchEvent("touchmove", 112));
|
||||
await Promise.resolve();
|
||||
|
||||
expect(pane.loadOlderMessages).toHaveBeenCalledOnce();
|
||||
expect(pane.historyAutoLoadBlocked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chat pane native history pagination", () => {
|
||||
|
||||
@@ -153,6 +153,7 @@ type ChatHistoryAnchor = {
|
||||
const CATALOG_TOOL_RESULT_PREVIEW_MAX_CHARS = 500;
|
||||
const CHAT_HISTORY_INTENT_EDGE_PX = 300;
|
||||
const CHAT_HISTORY_INTENT_IDLE_MS = 200;
|
||||
const CHAT_HISTORY_TOUCH_INTENT_PX = 8;
|
||||
const CHAT_HISTORY_UPWARD_KEYS = new Set(["ArrowUp", "PageUp", "Home"]);
|
||||
|
||||
function catalogRawString(raw: unknown, keys: readonly string[]): string | null {
|
||||
@@ -314,6 +315,7 @@ class ChatPane extends OpenClawLightDomElement {
|
||||
private historyBootstrapPagesLoaded = 0;
|
||||
private historyIntentConsumed = false;
|
||||
private historyIntentTimer: number | null = null;
|
||||
private historyTouchY: number | null = null;
|
||||
private transcriptScrollTop: number | null = null;
|
||||
private pendingHistoryAnchor: ChatHistoryAnchor | null = null;
|
||||
private nativePaginationSnapshot: ChatHistoryPagination | null = null;
|
||||
@@ -939,6 +941,7 @@ class ChatPane extends OpenClawLightDomElement {
|
||||
this.historyAutoLoadBlocked = false;
|
||||
this.historyBootstrapPagesLoaded = 0;
|
||||
this.historyIntentConsumed = false;
|
||||
this.historyTouchY = null;
|
||||
if (this.historyIntentTimer !== null) {
|
||||
window.clearTimeout(this.historyIntentTimer);
|
||||
this.historyIntentTimer = null;
|
||||
@@ -1104,9 +1107,27 @@ class ChatPane extends OpenClawLightDomElement {
|
||||
|
||||
private handleTranscriptHistoryIntent(event: Event): void {
|
||||
const root = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
||||
const upward =
|
||||
let upward =
|
||||
(event instanceof WheelEvent && event.deltaY < 0) ||
|
||||
(event instanceof KeyboardEvent && CHAT_HISTORY_UPWARD_KEYS.has(event.key));
|
||||
if (event instanceof TouchEvent) {
|
||||
const touchY = event.touches[0]?.clientY ?? null;
|
||||
if (event.type === "touchstart") {
|
||||
this.historyTouchY = touchY;
|
||||
return;
|
||||
}
|
||||
if (event.type === "touchend" || event.type === "touchcancel") {
|
||||
this.historyTouchY = null;
|
||||
return;
|
||||
}
|
||||
const previousTouchY = this.historyTouchY;
|
||||
if (touchY !== null && previousTouchY !== null) {
|
||||
upward = touchY - previousTouchY >= CHAT_HISTORY_TOUCH_INTENT_PX;
|
||||
if (upward || touchY < previousTouchY) {
|
||||
this.historyTouchY = touchY;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!root ||
|
||||
!upward ||
|
||||
|
||||
@@ -725,6 +725,33 @@ describe("chat history pagination", () => {
|
||||
thread.dispatchEvent(new KeyboardEvent("keydown", { key: "PageUp", bubbles: true }));
|
||||
expect(onHistoryIntent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps wheel and touch history intent listeners passive", () => {
|
||||
const addEventListener = vi.spyOn(EventTarget.prototype, "addEventListener");
|
||||
try {
|
||||
renderChatView({
|
||||
historyPagination: {
|
||||
loading: false,
|
||||
},
|
||||
onHistoryIntent: vi.fn(),
|
||||
});
|
||||
|
||||
for (const eventName of ["wheel", "touchstart", "touchmove"]) {
|
||||
expect(
|
||||
addEventListener.mock.calls.some(
|
||||
([type, , options]) =>
|
||||
type === eventName &&
|
||||
typeof options === "object" &&
|
||||
options !== null &&
|
||||
"passive" in options &&
|
||||
options.passive === true,
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
} finally {
|
||||
addEventListener.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("direct thread avatar mode", () => {
|
||||
|
||||
@@ -653,8 +653,16 @@ export function renderChatThread(props: ChatThreadProps) {
|
||||
aria-live="polite"
|
||||
tabindex="0"
|
||||
@scroll=${props.onChatScroll}
|
||||
@wheel=${props.onHistoryIntent}
|
||||
@wheel=${props.onHistoryIntent ? { handleEvent: props.onHistoryIntent, passive: true } : null}
|
||||
@keydown=${props.onHistoryIntent}
|
||||
@touchstart=${props.onHistoryIntent
|
||||
? { handleEvent: props.onHistoryIntent, passive: true }
|
||||
: null}
|
||||
@touchmove=${props.onHistoryIntent
|
||||
? { handleEvent: props.onHistoryIntent, passive: true }
|
||||
: null}
|
||||
@touchend=${props.onHistoryIntent}
|
||||
@touchcancel=${props.onHistoryIntent}
|
||||
@mousedown=${beginNativeWindowDragFromTopInset}
|
||||
@click=${(event: Event) => {
|
||||
handleMarkdownCodeBlockCopy(event);
|
||||
|
||||
Reference in New Issue
Block a user