fix(app): auto-scroll

This commit is contained in:
adamelmore
2026-01-27 17:48:21 -06:00
parent 15ffd3cba1
commit b4a9e1b190

View File

@@ -478,6 +478,12 @@ export default function Page() {
const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset
if (targetIndex < 0 || targetIndex >= msgs.length) return if (targetIndex < 0 || targetIndex >= msgs.length) return
if (targetIndex === msgs.length - 1) {
resumeScroll()
return
}
autoScroll.pause()
scrollToMessage(msgs[targetIndex], "auto") scrollToMessage(msgs[targetIndex], "auto")
} }
@@ -524,14 +530,7 @@ export default function Page() {
const scrollGestureWindowMs = 250 const scrollGestureWindowMs = 250
const scrollIgnoreWindowMs = 250 let touchGesture: number | undefined
let scrollIgnore = 0
const markScrollIgnore = () => {
scrollIgnore = Date.now()
}
const hasScrollIgnore = () => Date.now() - scrollIgnore < scrollIgnoreWindowMs
const markScrollGesture = (target?: EventTarget | null) => { const markScrollGesture = (target?: EventTarget | null) => {
const root = scroller const root = scroller
@@ -1274,9 +1273,15 @@ export default function Page() {
overflowAnchor: "dynamic", overflowAnchor: "dynamic",
}) })
const clearMessageHash = () => {
if (!window.location.hash) return
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
}
const resumeScroll = () => { const resumeScroll = () => {
setStore("messageId", undefined) setStore("messageId", undefined)
autoScroll.forceScrollToBottom() autoScroll.forceScrollToBottom()
clearMessageHash()
} }
// When the user returns to the bottom, treat the active message as "latest". // When the user returns to the bottom, treat the active message as "latest".
@@ -1286,6 +1291,7 @@ export default function Page() {
(scrolled) => { (scrolled) => {
if (scrolled) return if (scrolled) return
setStore("messageId", undefined) setStore("messageId", undefined)
clearMessageHash()
}, },
{ defer: true }, { defer: true },
), ),
@@ -1361,7 +1367,6 @@ export default function Page() {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const delta = el.scrollHeight - beforeHeight const delta = el.scrollHeight - beforeHeight
if (!delta) return if (!delta) return
markScrollIgnore()
el.scrollTop = beforeTop + delta el.scrollTop = beforeTop + delta
}) })
@@ -1399,7 +1404,6 @@ export default function Page() {
if (stick && el) { if (stick && el) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
markScrollIgnore()
el.scrollTo({ top: el.scrollHeight, behavior: "auto" }) el.scrollTo({ top: el.scrollHeight, behavior: "auto" })
}) })
} }
@@ -1494,6 +1498,7 @@ export default function Page() {
const match = hash.match(/^message-(.+)$/) const match = hash.match(/^message-(.+)$/)
if (match) { if (match) {
autoScroll.pause()
const msg = visibleUserMessages().find((m) => m.id === match[1]) const msg = visibleUserMessages().find((m) => m.id === match[1])
if (msg) { if (msg) {
scrollToMessage(msg, behavior) scrollToMessage(msg, behavior)
@@ -1507,6 +1512,7 @@ export default function Page() {
const target = document.getElementById(hash) const target = document.getElementById(hash)
if (target) { if (target) {
autoScroll.pause()
scrollToElement(target, behavior) scrollToElement(target, behavior)
return return
} }
@@ -1603,6 +1609,7 @@ export default function Page() {
const msg = visibleUserMessages().find((m) => m.id === targetId) const msg = visibleUserMessages().find((m) => m.id === targetId)
if (!msg) return if (!msg) return
if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined) if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined)
autoScroll.pause()
requestAnimationFrame(() => scrollToMessage(msg, "auto")) requestAnimationFrame(() => scrollToMessage(msg, "auto"))
}) })
@@ -1783,28 +1790,102 @@ export default function Page() {
> >
<button <button
class="pointer-events-auto size-8 flex items-center justify-center rounded-full bg-background-base border border-border-base shadow-sm text-text-base hover:bg-background-stronger transition-colors" class="pointer-events-auto size-8 flex items-center justify-center rounded-full bg-background-base border border-border-base shadow-sm text-text-base hover:bg-background-stronger transition-colors"
onClick={() => { onClick={resumeScroll}
setStore("messageId", undefined)
autoScroll.forceScrollToBottom()
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
}}
> >
<Icon name="arrow-down-to-line" /> <Icon name="arrow-down-to-line" />
</button> </button>
</div> </div>
<div <div
ref={setScrollRef} ref={setScrollRef}
onWheel={(e) => markScrollGesture(e.target)} onWheel={(e) => {
onTouchMove={(e) => markScrollGesture(e.target)} 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) => { onPointerDown={(e) => {
if (e.target !== e.currentTarget) return if (e.target !== e.currentTarget) return
markScrollGesture(e.target) markScrollGesture(e.currentTarget)
}} }}
onScroll={(e) => { onScroll={(e) => {
const gesture = hasScrollGesture() if (!hasScrollGesture()) return
if (!hasScrollIgnore() || gesture) autoScroll.handleScroll() autoScroll.handleScroll()
if (!gesture) return markScrollGesture(e.currentTarget)
markScrollGesture(e.target)
if (isDesktop()) scheduleScrollSpy(e.currentTarget) if (isDesktop()) scheduleScrollSpy(e.currentTarget)
}} }}
onClick={autoScroll.handleInteraction} onClick={autoScroll.handleInteraction}