Anchor scroll to message stream sentinel
This commit is contained in:
@@ -302,6 +302,12 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [scrollElement, setScrollElement] = createSignal<HTMLDivElement | undefined>()
|
const [scrollElement, setScrollElement] = createSignal<HTMLDivElement | undefined>()
|
||||||
|
const [bottomSentinel, setBottomSentinel] = createSignal<HTMLDivElement | null>(null)
|
||||||
|
createEffect(() => {
|
||||||
|
if (bottomSentinel()) {
|
||||||
|
scheduleAnchorScroll(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
const [initialForceActive, setInitialForceActive] = createSignal(true)
|
const [initialForceActive, setInitialForceActive] = createSignal(true)
|
||||||
const [initialForceInitialized, setInitialForceInitialized] = createSignal(false)
|
const [initialForceInitialized, setInitialForceInitialized] = createSignal(false)
|
||||||
const [initialForceStartIndex, setInitialForceStartIndex] = createSignal(0)
|
const [initialForceStartIndex, setInitialForceStartIndex] = createSignal(0)
|
||||||
@@ -338,10 +344,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
let lastKnownScrollTop = 0
|
let lastKnownScrollTop = 0
|
||||||
let lastMeasuredScrollHeight = 0
|
let lastMeasuredScrollHeight = 0
|
||||||
let pendingScrollFrame: number | null = null
|
let pendingScrollFrame: number | null = null
|
||||||
|
let pendingAnchorScroll: number | null = null
|
||||||
let userScrollIntentUntil = 0
|
let userScrollIntentUntil = 0
|
||||||
let detachScrollIntentListeners: (() => void) | undefined
|
let detachScrollIntentListeners: (() => void) | undefined
|
||||||
let hasRestoredScroll = false
|
let hasRestoredScroll = false
|
||||||
let hasInitialScroll = false
|
|
||||||
// When the user explicitly clicks "scroll to bottom", we want the
|
// When the user explicitly clicks "scroll to bottom", we want the
|
||||||
// smooth scroll animation to complete without being immediately
|
// smooth scroll animation to complete without being immediately
|
||||||
// overridden by the auto-scroll effects that react to new messages.
|
// overridden by the auto-scroll effects that react to new messages.
|
||||||
@@ -408,28 +414,14 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
const behavior = immediate ? "auto" : "smooth"
|
const behavior = immediate ? "auto" : "smooth"
|
||||||
if (!immediate) {
|
if (!immediate) {
|
||||||
// We initiated this scroll (e.g., via the button). Skip the
|
|
||||||
// next auto-scroll reaction so the smooth animation isn't
|
|
||||||
// overridden by changeToken/preference effects.
|
|
||||||
suppressAutoScrollOnce = true
|
suppressAutoScrollOnce = true
|
||||||
}
|
}
|
||||||
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior })
|
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior })
|
||||||
setAutoScroll(true)
|
setAutoScroll(true)
|
||||||
lastMeasuredScrollHeight = containerRef.scrollHeight
|
|
||||||
lastKnownScrollTop = containerRef.scrollTop
|
|
||||||
updateScrollIndicators(containerRef)
|
updateScrollIndicators(containerRef)
|
||||||
scheduleScrollPersist()
|
scheduleScrollPersist()
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottomAndClamp(immediate = false) {
|
|
||||||
scrollToBottom(immediate)
|
|
||||||
if (hasInitialScroll) {
|
|
||||||
requestAnimationFrame(() => clampScrollAfterShrink())
|
|
||||||
} else {
|
|
||||||
hasInitialScroll = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToTop(immediate = false) {
|
function scrollToTop(immediate = false) {
|
||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
const behavior = immediate ? "auto" : "smooth"
|
const behavior = immediate ? "auto" : "smooth"
|
||||||
@@ -440,18 +432,33 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
updateScrollIndicators(containerRef)
|
updateScrollIndicators(containerRef)
|
||||||
scheduleScrollPersist()
|
scheduleScrollPersist()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContentRendered() {
|
function scheduleAnchorScroll(immediate = false) {
|
||||||
if (!containerRef) return
|
|
||||||
if (!autoScroll()) return
|
if (!autoScroll()) return
|
||||||
scrollToBottomAndClamp(true)
|
const sentinel = bottomSentinel()
|
||||||
|
if (!sentinel) return
|
||||||
|
if (pendingAnchorScroll !== null) {
|
||||||
|
cancelAnimationFrame(pendingAnchorScroll)
|
||||||
|
pendingAnchorScroll = null
|
||||||
|
}
|
||||||
|
pendingAnchorScroll = requestAnimationFrame(() => {
|
||||||
|
pendingAnchorScroll = null
|
||||||
|
sentinel.scrollIntoView({ block: "end", inline: "nearest", behavior: "auto" })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleContentRendered() {
|
||||||
|
scheduleAnchorScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.registerScrollToBottom) {
|
if (props.registerScrollToBottom) {
|
||||||
props.registerScrollToBottom(() => scrollToBottomAndClamp(true))
|
props.registerScrollToBottom(() => scrollToBottom(true))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
let pendingScrollPersist: number | null = null
|
let pendingScrollPersist: number | null = null
|
||||||
|
|
||||||
@@ -465,21 +472,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampScrollAfterShrink() {
|
|
||||||
if (!containerRef || !autoScroll()) return
|
|
||||||
const currentHeight = containerRef.scrollHeight
|
|
||||||
const clientHeight = containerRef.clientHeight
|
|
||||||
if (currentHeight < lastMeasuredScrollHeight) {
|
|
||||||
const maxScrollTop = Math.max(currentHeight - clientHeight, 0)
|
|
||||||
containerRef.scrollTo({ top: maxScrollTop, behavior: "auto" })
|
|
||||||
lastKnownScrollTop = containerRef.scrollTop
|
|
||||||
}
|
|
||||||
lastMeasuredScrollHeight = currentHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function handleScroll(event: Event) {
|
function handleScroll(event: Event) {
|
||||||
|
|
||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
if (pendingScrollFrame !== null) {
|
if (pendingScrollFrame !== null) {
|
||||||
cancelAnimationFrame(pendingScrollFrame)
|
cancelAnimationFrame(pendingScrollFrame)
|
||||||
@@ -549,10 +545,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (autoScroll()) {
|
if (autoScroll()) {
|
||||||
scrollToBottomAndClamp(true)
|
scheduleAnchorScroll(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
preferenceSignature()
|
preferenceSignature()
|
||||||
if (props.loading) return
|
if (props.loading) return
|
||||||
@@ -563,9 +559,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
suppressAutoScrollOnce = false
|
suppressAutoScrollOnce = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scrollToBottomAndClamp(true)
|
scheduleAnchorScroll(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (messageIds().length === 0) {
|
if (messageIds().length === 0) {
|
||||||
@@ -585,6 +582,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
cancelAnimationFrame(pendingScrollPersist)
|
cancelAnimationFrame(pendingScrollPersist)
|
||||||
pendingScrollPersist = null
|
pendingScrollPersist = null
|
||||||
}
|
}
|
||||||
|
if (pendingAnchorScroll !== null) {
|
||||||
|
cancelAnimationFrame(pendingAnchorScroll)
|
||||||
|
pendingAnchorScroll = null
|
||||||
|
}
|
||||||
if (detachScrollIntentListeners) {
|
if (detachScrollIntentListeners) {
|
||||||
detachScrollIntentListeners()
|
detachScrollIntentListeners()
|
||||||
detachScrollIntentListeners = undefined
|
detachScrollIntentListeners = undefined
|
||||||
@@ -720,9 +721,11 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Index>
|
</Index>
|
||||||
|
<div ref={setBottomSentinel} aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={showScrollTopButton() || showScrollBottomButton()}>
|
<Show when={showScrollTopButton() || showScrollBottomButton()}>
|
||||||
|
|
||||||
<div class="message-scroll-button-wrapper">
|
<div class="message-scroll-button-wrapper">
|
||||||
<Show when={showScrollTopButton()}>
|
<Show when={showScrollTopButton()}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user