feat(kez-chat/web): auto-scroll thread on new message + on conversation open
Two rules, picked to feel natural: 1. Conversation just opened (peer_primary changed) → always scroll to bottom. You expect to see the latest exchange first. 2. New message landed in the current conversation → scroll to bottom IF you were already within 120px of the bottom. If you're scrolled up reading older history, the auto-scroll doesn't yank you back down. (Slack / Telegram / iMessage all do this; getting yanked out of history when you're searching for something is infuriating.) Implementation: a single $effect tracks activeConv.messages.length + activeConv.peer_primary. Compares against two cursor vars (prevPrimary, prevMessageCount) to distinguish "opened a new conversation" from "new message in current one". queueMicrotask after the DOM updates so scrollHeight reflects the just-rendered message. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
de120f7d6c
commit
4eeedb38fb
@ -77,6 +77,50 @@
|
|||||||
/** Unsubscribe handle for the inbox-service "new message" listener. */
|
/** Unsubscribe handle for the inbox-service "new message" listener. */
|
||||||
let unsubscribe: (() => void) | null = null;
|
let unsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
|
/** Scroll container for the thread view — bound for auto-scroll. */
|
||||||
|
let scrollEl: HTMLDivElement | null = $state(null);
|
||||||
|
/** Tracks the previously-rendered conversation so we know when the
|
||||||
|
* user just OPENED a different thread (always scroll to bottom) vs.
|
||||||
|
* when a new message landed in the current one (scroll only if they
|
||||||
|
* were near the bottom already — don't yank them out of history). */
|
||||||
|
let prevPrimary: string | null = null;
|
||||||
|
let prevMessageCount = 0;
|
||||||
|
|
||||||
|
/** Distance (px) from the bottom of the thread within which we still
|
||||||
|
* count "near bottom" for the follow-along rule. Roughly two short
|
||||||
|
* bubble heights — generous enough that small misses don't sticky. */
|
||||||
|
const SCROLL_FOLLOW_THRESHOLD_PX = 120;
|
||||||
|
|
||||||
|
// Auto-scroll effect — runs whenever activeConv.messages.length or
|
||||||
|
// activeConv.peer_primary changes (both are reactive reads).
|
||||||
|
$effect(() => {
|
||||||
|
if (!activeConv || !scrollEl) {
|
||||||
|
prevPrimary = null;
|
||||||
|
prevMessageCount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = scrollEl;
|
||||||
|
const count = activeConv.messages.length;
|
||||||
|
const conversationChanged = activeConv.peer_primary !== prevPrimary;
|
||||||
|
if (conversationChanged) {
|
||||||
|
prevPrimary = activeConv.peer_primary;
|
||||||
|
prevMessageCount = count;
|
||||||
|
// Wait a microtask so Svelte's DOM updates have flushed.
|
||||||
|
queueMicrotask(() => (el.scrollTop = el.scrollHeight));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (count > prevMessageCount) {
|
||||||
|
prevMessageCount = count;
|
||||||
|
const distFromBottom =
|
||||||
|
el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||||
|
if (distFromBottom < SCROLL_FOLLOW_THRESHOLD_PX) {
|
||||||
|
queueMicrotask(() => (el.scrollTop = el.scrollHeight));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prevMessageCount = count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// "Start chat with" lookup state.
|
// "Start chat with" lookup state.
|
||||||
let newPeerInput = $state("");
|
let newPeerInput = $state("");
|
||||||
let resolving = $state(false);
|
let resolving = $state(false);
|
||||||
@ -351,7 +395,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 space-y-2">
|
<div class="flex-1 overflow-y-auto p-4 space-y-2" bind:this={scrollEl}>
|
||||||
{#each activeConv.messages as m (m.seq + ":" + m.direction)}
|
{#each activeConv.messages as m (m.seq + ":" + m.direction)}
|
||||||
{@const boost = emojiOnlyBoost(m.body)}
|
{@const boost = emojiOnlyBoost(m.body)}
|
||||||
<div class={`max-w-md ${m.direction === "out" ? "ml-auto" : ""}`}>
|
<div class={`max-w-md ${m.direction === "out" ? "ml-auto" : ""}`}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user