| 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 | } |