diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index eda15a581..a845d3a65 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -478,6 +478,12 @@ export default function Page() { const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset if (targetIndex < 0 || targetIndex >= msgs.length) return + if (targetIndex === msgs.length - 1) { + resumeScroll() + return + } + + autoScroll.pause() scrollToMessage(msgs[targetIndex], "auto") } @@ -524,14 +530,7 @@ export default function Page() { const scrollGestureWindowMs = 250 - const scrollIgnoreWindowMs = 250 - let scrollIgnore = 0 - - const markScrollIgnore = () => { - scrollIgnore = Date.now() - } - - const hasScrollIgnore = () => Date.now() - scrollIgnore < scrollIgnoreWindowMs + let touchGesture: number | undefined const markScrollGesture = (target?: EventTarget | null) => { const root = scroller @@ -1274,9 +1273,15 @@ export default function Page() { overflowAnchor: "dynamic", }) + const clearMessageHash = () => { + if (!window.location.hash) return + window.history.replaceState(null, "", window.location.href.replace(/#.*$/, "")) + } + const resumeScroll = () => { setStore("messageId", undefined) autoScroll.forceScrollToBottom() + clearMessageHash() } // When the user returns to the bottom, treat the active message as "latest". @@ -1286,6 +1291,7 @@ export default function Page() { (scrolled) => { if (scrolled) return setStore("messageId", undefined) + clearMessageHash() }, { defer: true }, ), @@ -1361,7 +1367,6 @@ export default function Page() { requestAnimationFrame(() => { const delta = el.scrollHeight - beforeHeight if (!delta) return - markScrollIgnore() el.scrollTop = beforeTop + delta }) @@ -1399,7 +1404,6 @@ export default function Page() { if (stick && el) { requestAnimationFrame(() => { - markScrollIgnore() el.scrollTo({ top: el.scrollHeight, behavior: "auto" }) }) } @@ -1494,6 +1498,7 @@ export default function Page() { const match = hash.match(/^message-(.+)$/) if (match) { + autoScroll.pause() const msg = visibleUserMessages().find((m) => m.id === match[1]) if (msg) { scrollToMessage(msg, behavior) @@ -1507,6 +1512,7 @@ export default function Page() { const target = document.getElementById(hash) if (target) { + autoScroll.pause() scrollToElement(target, behavior) return } @@ -1603,6 +1609,7 @@ export default function Page() { const msg = visibleUserMessages().find((m) => m.id === targetId) if (!msg) return if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined) + autoScroll.pause() requestAnimationFrame(() => scrollToMessage(msg, "auto")) }) @@ -1783,28 +1790,102 @@ export default function Page() { >
markScrollGesture(e.target)} - onTouchMove={(e) => markScrollGesture(e.target)} + onWheel={(e) => { + const root = e.currentTarget + const target = e.target instanceof Element ? e.target : undefined + const nested = target?.closest("[data-scrollable]") + if (!nested || nested === root) { + markScrollGesture(root) + return + } + + if (!(nested instanceof HTMLElement)) { + markScrollGesture(root) + return + } + + const max = nested.scrollHeight - nested.clientHeight + if (max <= 1) { + markScrollGesture(root) + return + } + + const delta = + e.deltaMode === 1 + ? e.deltaY * 40 + : e.deltaMode === 2 + ? e.deltaY * root.clientHeight + : e.deltaY + if (!delta) return + + if (delta < 0) { + if (nested.scrollTop + delta <= 0) markScrollGesture(root) + return + } + + const remaining = max - nested.scrollTop + if (delta > remaining) markScrollGesture(root) + }} + onTouchStart={(e) => { + touchGesture = e.touches[0]?.clientY + }} + onTouchMove={(e) => { + const next = e.touches[0]?.clientY + const prev = touchGesture + touchGesture = next + if (next === undefined || prev === undefined) return + + const delta = prev - next + if (!delta) return + + const root = e.currentTarget + const target = e.target instanceof Element ? e.target : undefined + const nested = target?.closest("[data-scrollable]") + if (!nested || nested === root) { + markScrollGesture(root) + return + } + + if (!(nested instanceof HTMLElement)) { + markScrollGesture(root) + return + } + + const max = nested.scrollHeight - nested.clientHeight + if (max <= 1) { + markScrollGesture(root) + return + } + + if (delta < 0) { + if (nested.scrollTop + delta <= 0) markScrollGesture(root) + return + } + + const remaining = max - nested.scrollTop + if (delta > remaining) markScrollGesture(root) + }} + onTouchEnd={() => { + touchGesture = undefined + }} + onTouchCancel={() => { + touchGesture = undefined + }} onPointerDown={(e) => { if (e.target !== e.currentTarget) return - markScrollGesture(e.target) + markScrollGesture(e.currentTarget) }} onScroll={(e) => { - const gesture = hasScrollGesture() - if (!hasScrollIgnore() || gesture) autoScroll.handleScroll() - if (!gesture) return - markScrollGesture(e.target) + if (!hasScrollGesture()) return + autoScroll.handleScroll() + markScrollGesture(e.currentTarget) if (isDesktop()) scheduleScrollSpy(e.currentTarget) }} onClick={autoScroll.handleInteraction}