fix(app): auto-scroll
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user