Merge pull request #90 from LINUXexpert-org/hold-the-opening-scroll
Hold the opening scroll while the conversation settles
This commit is contained in:
@@ -12,6 +12,9 @@ import { client } from "@/jmap/client";
|
|||||||
import { LabelPicker } from "./LabelPicker";
|
import { LabelPicker } from "./LabelPicker";
|
||||||
import { threadScrollTarget } from "@/lib/threadScroll";
|
import { threadScrollTarget } from "@/lib/threadScroll";
|
||||||
|
|
||||||
|
/** How long the opening scroll keeps its place while bodies and images land. */
|
||||||
|
const HOLD_MS = 2000;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
threadId: Id;
|
threadId: Id;
|
||||||
mailboxId: Id | null;
|
mailboxId: Id | null;
|
||||||
@@ -116,13 +119,54 @@ export function ThreadView({ threadId, mailboxId, onBack, actions, onNavigate, h
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [messages.map((m) => m.id + (m.keywords.$seen ? "1" : "0")).join(","), settings.markReadDelay]);
|
}, [messages.map((m) => m.id + (m.keywords.$seen ? "1" : "0")).join(","), settings.markReadDelay]);
|
||||||
|
|
||||||
// Open on the first unread message rather than the newest one (#87).
|
/*
|
||||||
|
* Open on the first unread message rather than the newest one (#87).
|
||||||
|
*
|
||||||
|
* Scrolling once is not enough. Message bodies are written into shadow roots
|
||||||
|
* by child effects, and the images in them load later still, so the pane goes
|
||||||
|
* on growing after the scroll -- and `scrollIntoView` clamps to the scroll
|
||||||
|
* range as it stands the moment it is called. The read-thread fallback always
|
||||||
|
* aims at the last message, which no thread has the room to lift to the top,
|
||||||
|
* so that clamp is the whole of the range: measuring it before the images
|
||||||
|
* landed stopped 39px short of the bottom, every time (#89).
|
||||||
|
*
|
||||||
|
* So the target is held against the top of the pane while the thread settles,
|
||||||
|
* and let go the moment the reader touches it. A pane that re-scrolls under
|
||||||
|
* someone who has started reading is worse than one that lands short, which
|
||||||
|
* is why the hold ends on the first sign of them rather than when the content
|
||||||
|
* stops changing.
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!messages.length || !scrollRef.current) return;
|
const sc = scrollRef.current;
|
||||||
|
if (!messages.length || !sc) return;
|
||||||
const target = threadScrollTarget(messages, wasUnread);
|
const target = threadScrollTarget(messages, wasUnread);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const el = scrollRef.current.querySelector<HTMLElement>(`[data-msg-id="${CSS.escape(target)}"]`);
|
|
||||||
el?.scrollIntoView({ block: "start" });
|
let held = true;
|
||||||
|
const align = () => {
|
||||||
|
if (held) sc.querySelector<HTMLElement>(`[data-msg-id="${CSS.escape(target)}"]`)?.scrollIntoView({ block: "start" });
|
||||||
|
};
|
||||||
|
const release = () => {
|
||||||
|
held = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
align();
|
||||||
|
|
||||||
|
// The messages and the reply box: what grows is one of their heights.
|
||||||
|
const ro = new ResizeObserver(align);
|
||||||
|
for (const child of sc.children) ro.observe(child);
|
||||||
|
// `scroll` is not in here: the aligning does that itself.
|
||||||
|
for (const ev of ["wheel", "pointerdown", "touchstart"]) sc.addEventListener(ev, release, { passive: true });
|
||||||
|
window.addEventListener("keydown", release);
|
||||||
|
const settled = window.setTimeout(release, HOLD_MS);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
release();
|
||||||
|
ro.disconnect();
|
||||||
|
for (const ev of ["wheel", "pointerdown", "touchstart"]) sc.removeEventListener(ev, release);
|
||||||
|
window.removeEventListener("keydown", release);
|
||||||
|
window.clearTimeout(settled);
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [threadId, messages.length > 0]);
|
}, [threadId, messages.length > 0]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user