fix(app): don't show scroll-to-bottom unecessarily
This commit is contained in:
@@ -279,6 +279,10 @@ export default function Page() {
|
|||||||
pendingMessage: undefined as string | undefined,
|
pendingMessage: undefined as string | undefined,
|
||||||
scrollGesture: 0,
|
scrollGesture: 0,
|
||||||
autoCreated: false,
|
autoCreated: false,
|
||||||
|
scroll: {
|
||||||
|
overflow: false,
|
||||||
|
bottom: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -795,6 +799,7 @@ export default function Page() {
|
|||||||
let inputRef!: HTMLDivElement
|
let inputRef!: HTMLDivElement
|
||||||
let promptDock: HTMLDivElement | undefined
|
let promptDock: HTMLDivElement | undefined
|
||||||
let scroller: HTMLDivElement | undefined
|
let scroller: HTMLDivElement | undefined
|
||||||
|
let content: HTMLDivElement | undefined
|
||||||
|
|
||||||
const scrollGestureWindowMs = 250
|
const scrollGestureWindowMs = 250
|
||||||
|
|
||||||
@@ -1618,10 +1623,40 @@ export default function Page() {
|
|||||||
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
|
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scrollStateFrame: number | undefined
|
||||||
|
let scrollStateTarget: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const updateScrollState = (el: HTMLDivElement) => {
|
||||||
|
const max = el.scrollHeight - el.clientHeight
|
||||||
|
const overflow = max > 1
|
||||||
|
const bottom = !overflow || el.scrollTop >= max - 2
|
||||||
|
|
||||||
|
if (ui.scroll.overflow === overflow && ui.scroll.bottom === bottom) return
|
||||||
|
setUi("scroll", { overflow, bottom })
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleScrollState = (el: HTMLDivElement) => {
|
||||||
|
scrollStateTarget = el
|
||||||
|
if (scrollStateFrame !== undefined) return
|
||||||
|
|
||||||
|
scrollStateFrame = requestAnimationFrame(() => {
|
||||||
|
scrollStateFrame = undefined
|
||||||
|
|
||||||
|
const target = scrollStateTarget
|
||||||
|
scrollStateTarget = undefined
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
updateScrollState(target)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const resumeScroll = () => {
|
const resumeScroll = () => {
|
||||||
setStore("messageId", undefined)
|
setStore("messageId", undefined)
|
||||||
autoScroll.forceScrollToBottom()
|
autoScroll.forceScrollToBottom()
|
||||||
clearMessageHash()
|
clearMessageHash()
|
||||||
|
|
||||||
|
const el = scroller
|
||||||
|
if (el) scheduleScrollState(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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".
|
||||||
@@ -1657,8 +1692,17 @@ export default function Page() {
|
|||||||
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
||||||
scroller = el
|
scroller = el
|
||||||
autoScroll.scrollRef(el)
|
autoScroll.scrollRef(el)
|
||||||
|
if (el) scheduleScrollState(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createResizeObserver(
|
||||||
|
() => content,
|
||||||
|
() => {
|
||||||
|
const el = scroller
|
||||||
|
if (el) scheduleScrollState(el)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const turnInit = 20
|
const turnInit = 20
|
||||||
const turnBatch = 20
|
const turnBatch = 20
|
||||||
let turnHandle: number | undefined
|
let turnHandle: number | undefined
|
||||||
@@ -1759,6 +1803,8 @@ export default function Page() {
|
|||||||
el.scrollTo({ top: el.scrollHeight, behavior: "auto" })
|
el.scrollTo({ top: el.scrollHeight, behavior: "auto" })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (el) scheduleScrollState(el)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1839,6 +1885,9 @@ export default function Page() {
|
|||||||
const hash = window.location.hash.slice(1)
|
const hash = window.location.hash.slice(1)
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
autoScroll.forceScrollToBottom()
|
autoScroll.forceScrollToBottom()
|
||||||
|
|
||||||
|
const el = scroller
|
||||||
|
if (el) scheduleScrollState(el)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1864,6 +1913,9 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
autoScroll.forceScrollToBottom()
|
autoScroll.forceScrollToBottom()
|
||||||
|
|
||||||
|
const el = scroller
|
||||||
|
if (el) scheduleScrollState(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
const closestMessage = (node: Element | null): HTMLElement | null => {
|
const closestMessage = (node: Element | null): HTMLElement | null => {
|
||||||
@@ -2029,6 +2081,7 @@ export default function Page() {
|
|||||||
cancelTurnBackfill()
|
cancelTurnBackfill()
|
||||||
document.removeEventListener("keydown", handleKeyDown)
|
document.removeEventListener("keydown", handleKeyDown)
|
||||||
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
|
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
|
||||||
|
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -2133,8 +2186,9 @@ export default function Page() {
|
|||||||
<div
|
<div
|
||||||
class="absolute left-1/2 -translate-x-1/2 bottom-[calc(var(--prompt-height,8rem)+32px)] z-[60] pointer-events-none transition-all duration-200 ease-out"
|
class="absolute left-1/2 -translate-x-1/2 bottom-[calc(var(--prompt-height,8rem)+32px)] z-[60] pointer-events-none transition-all duration-200 ease-out"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100 translate-y-0 scale-100": autoScroll.userScrolled(),
|
"opacity-100 translate-y-0 scale-100": ui.scroll.overflow && !ui.scroll.bottom,
|
||||||
"opacity-0 translate-y-2 scale-95 pointer-events-none": !autoScroll.userScrolled(),
|
"opacity-0 translate-y-2 scale-95 pointer-events-none":
|
||||||
|
!ui.scroll.overflow || ui.scroll.bottom,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -2232,6 +2286,7 @@ export default function Page() {
|
|||||||
markScrollGesture(e.currentTarget)
|
markScrollGesture(e.currentTarget)
|
||||||
}}
|
}}
|
||||||
onScroll={(e) => {
|
onScroll={(e) => {
|
||||||
|
scheduleScrollState(e.currentTarget)
|
||||||
if (!hasScrollGesture()) return
|
if (!hasScrollGesture()) return
|
||||||
autoScroll.handleScroll()
|
autoScroll.handleScroll()
|
||||||
markScrollGesture(e.currentTarget)
|
markScrollGesture(e.currentTarget)
|
||||||
@@ -2359,7 +2414,13 @@ export default function Page() {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={autoScroll.contentRef}
|
ref={(el) => {
|
||||||
|
content = el
|
||||||
|
autoScroll.contentRef(el)
|
||||||
|
|
||||||
|
const root = scroller
|
||||||
|
if (root) scheduleScrollState(root)
|
||||||
|
}}
|
||||||
role="log"
|
role="log"
|
||||||
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||||
classList={{
|
classList={{
|
||||||
|
|||||||
Reference in New Issue
Block a user