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 onContentRendered?: () => void
setBottomSentinel: (element: HTMLDivElement | null) => void setBottomSentinel: (element: HTMLDivElement | null) => void
suspendMeasurements?: () => boolean suspendMeasurements?: () => boolean
onInitialRenderComplete?: () => void
} }
export default function MessageBlockList(props: MessageBlockListProps) { 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 ( return (
<> <>
<Index each={props.messageIds()}> <Index each={props.messageIds()}>
{(messageId) => { {(messageId) => {
return ( return (
<VirtualItem <VirtualItem
id={getMessageAnchorId(messageId())} id={getMessageAnchorId(messageId())}
cacheKey={messageId()} cacheKey={messageId()}
scrollContainer={props.scrollContainer} scrollContainer={props.scrollContainer}
threshold={VIRTUAL_ITEM_MARGIN_PX} threshold={VIRTUAL_ITEM_MARGIN_PX}
placeholderClass="message-stream-placeholder" placeholderClass="message-stream-placeholder"
virtualizationEnabled={() => !props.loading} virtualizationEnabled={() => !props.loading}
suspendMeasurements={props.suspendMeasurements} suspendMeasurements={props.suspendMeasurements}
> onMeasured={handleBlockRendered}
>
<MessageBlock <MessageBlock
messageId={messageId()} messageId={messageId()}
instanceId={props.instanceId} instanceId={props.instanceId}

View File

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