Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 1 | import { type RefObject, useEffect, useRef } from "react"; |
| 2 | ||
| 3 | /** | |
| 4 | * Keeps a conversation's newest message in view while the reader is at | |
| 5 | * the bottom: when the list itself changes size (the keyboard opening on | |
| 6 | * a phone shrinks it, a window resized) and when what is in it does (a | |
| 7 | * card, an image or a code block laying out after it arrived). Without | |
| 8 | * this, a list that gets shorter keeps its distance from the top, and the | |
| 9 | * newest messages slide out of sight under the composer. | |
| 10 | * | |
| 11 | * `stick` says whether the reader is at the bottom (within 120px), as the | |
| 12 | * list's `onScroll` keeps it (`atBottom`); a reader scrolled up into the | |
| 13 | * history is left where they are. | |
| 14 | */ | |
| 15 | export function useKeepAtBottom(scroller: RefObject<HTMLElement | null>, stick: RefObject<boolean>) { | |
| 16 | useEffect(() => { | |
| 17 | const element = scroller.current; | |
| 18 | if (!element || typeof ResizeObserver === "undefined") return; | |
| 19 | const keep = () => { | |
| 20 | if (stick.current) element.scrollTop = element.scrollHeight; | |
| 21 | }; | |
| 22 | const observer = new ResizeObserver(keep); | |
| 23 | observer.observe(element); | |
| 24 | for (const child of Array.from(element.children)) observer.observe(child); | |
| 25 | return () => observer.disconnect(); | |
| 26 | }, [scroller, stick]); | |
| 27 | } | |
| 28 | ||
| 29 | /** Whether a list is scrolled to (within 120px of) its bottom. */ | |
| 30 | export function atBottom(element: HTMLElement): boolean { | |
| 31 | return element.scrollHeight - element.scrollTop - element.clientHeight < 120; | |
| 32 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.