diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 7ff4bebb4..f74eadc87 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -279,6 +279,10 @@ export default function Page() { pendingMessage: undefined as string | undefined, scrollGesture: 0, autoCreated: false, + scroll: { + overflow: false, + bottom: true, + }, }) createEffect( @@ -795,6 +799,7 @@ export default function Page() { let inputRef!: HTMLDivElement let promptDock: HTMLDivElement | undefined let scroller: HTMLDivElement | undefined + let content: HTMLDivElement | undefined const scrollGestureWindowMs = 250 @@ -1618,10 +1623,40 @@ export default function Page() { 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 = () => { setStore("messageId", undefined) autoScroll.forceScrollToBottom() clearMessageHash() + + const el = scroller + if (el) scheduleScrollState(el) } // 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) => { scroller = el autoScroll.scrollRef(el) + if (el) scheduleScrollState(el) } + createResizeObserver( + () => content, + () => { + const el = scroller + if (el) scheduleScrollState(el) + }, + ) + const turnInit = 20 const turnBatch = 20 let turnHandle: number | undefined @@ -1759,6 +1803,8 @@ export default function Page() { 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) if (!hash) { autoScroll.forceScrollToBottom() + + const el = scroller + if (el) scheduleScrollState(el) return } @@ -1864,6 +1913,9 @@ export default function Page() { } autoScroll.forceScrollToBottom() + + const el = scroller + if (el) scheduleScrollState(el) } const closestMessage = (node: Element | null): HTMLElement | null => { @@ -2029,6 +2081,7 @@ export default function Page() { cancelTurnBackfill() document.removeEventListener("keydown", handleKeyDown) if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame) + if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame) }) return ( @@ -2133,8 +2186,9 @@ export default function Page() {