Skip to content
32 linesCodeBlameRaw
1import { type RefObject, useEffect } 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 */
15export 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. */
30export function atBottom(element: HTMLElement): boolean {
31 return element.scrollHeight - element.scrollTop - element.clientHeight < 120;
32}