Defer initial message scroll until list renders

This commit is contained in:
Shantur Rathore
2025-12-09 17:09:30 +00:00
parent 82ff1916b7
commit 783fb5c5b2
2 changed files with 56 additions and 10 deletions

View File

@@ -27,24 +27,45 @@ interface MessageBlockListProps {
onContentRendered?: () => void
setBottomSentinel: (element: HTMLDivElement | null) => void
suspendMeasurements?: () => boolean
onInitialRenderComplete?: () => void
}
export default function MessageBlockList(props: MessageBlockListProps) {
const totalMessages = () => props.messageIds().length
let renderedCount = 0
let initialRenderReported = false
const handleBlockRendered = () => {
if (initialRenderReported) return
renderedCount += 1
if (renderedCount >= totalMessages() && totalMessages() > 0) {
initialRenderReported = true
renderedCount = 0
props.onInitialRenderComplete?.()
}
}
createEffect(() => {
if (props.loading) {
renderedCount = 0
initialRenderReported = false
}
})
return (
<>
<Index each={props.messageIds()}>
{(messageId) => {
return (
<VirtualItem
id={getMessageAnchorId(messageId())}
cacheKey={messageId()}
scrollContainer={props.scrollContainer}
threshold={VIRTUAL_ITEM_MARGIN_PX}
placeholderClass="message-stream-placeholder"
virtualizationEnabled={() => !props.loading}
suspendMeasurements={props.suspendMeasurements}
>
id={getMessageAnchorId(messageId())}
cacheKey={messageId()}
scrollContainer={props.scrollContainer}
threshold={VIRTUAL_ITEM_MARGIN_PX}
placeholderClass="message-stream-placeholder"
virtualizationEnabled={() => !props.loading}
suspendMeasurements={props.suspendMeasurements}
onMeasured={handleBlockRendered}
>
<MessageBlock
messageId={messageId()}
instanceId={props.instanceId}

View File

@@ -162,6 +162,9 @@ export default function MessageSection(props: MessageSectionProps) {
let pendingActiveScroll = false
let scrollToBottomFrame: number | null = null
let scrollToBottomDelayedFrame: number | null = null
let pendingInitialScroll = true
const [initialRenderComplete, setInitialRenderComplete] = createSignal(false)
function markUserScrollIntent() {
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
@@ -368,11 +371,18 @@ export default function MessageSection(props: MessageSectionProps) {
}
function handleContentRendered() {
if (props.loading) {
return
}
scheduleAnchorScroll()
}
function handleInitialRenderComplete() {
setInitialRenderComplete(true)
}
function handleScroll() {
if (!containerRef) return
if (pendingScrollFrame !== null) {
cancelAnimationFrame(pendingScrollFrame)
@@ -412,12 +422,26 @@ export default function MessageSection(props: MessageSectionProps) {
lastActiveState = active
})
createEffect(() => {
const loading = Boolean(props.loading)
if (loading) {
pendingInitialScroll = true
setInitialRenderComplete(false)
return
}
if (pendingInitialScroll && initialRenderComplete()) {
pendingInitialScroll = false
requestScrollToBottom(false)
}
})
createEffect(() => {
if (!props.onQuoteSelection) {
clearQuoteSelection()
}
})
createEffect(() => {
if (typeof document === "undefined") return
const handleSelectionChange = () => updateQuoteSelectionFromSelection()
@@ -647,6 +671,7 @@ export default function MessageSection(props: MessageSectionProps) {
onContentRendered={handleContentRendered}
setBottomSentinel={setBottomSentinel}
suspendMeasurements={() => props.isActive === false}
onInitialRenderComplete={handleInitialRenderComplete}
/>