From 9f3521779cd7084364045748a76cd2b948ec46f8 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 31 Aug 2026 08:07:09 -0700 Subject: [PATCH] Add virtualization to room chat --- src/app/components/RoomChat.svelte | 89 +++++++++++++-------- src/lib/components/VirtualList.svelte | 106 ++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 33 deletions(-) create mode 100644 src/lib/components/VirtualList.svelte diff --git a/src/app/components/RoomChat.svelte b/src/app/components/RoomChat.svelte index 58ce9963..391dd964 100644 --- a/src/app/components/RoomChat.svelte +++ b/src/app/components/RoomChat.svelte @@ -21,6 +21,8 @@ import Divider from "@lib/components/Divider.svelte" import Icon from "@lib/components/Icon.svelte" import Spinner from "@lib/components/Spinner.svelte" + import VirtualList from "@lib/components/VirtualList.svelte" + import type {VirtualListController} from "@lib/components/VirtualList.svelte" import RoomCompose from "@app/components/RoomCompose.svelte" import RoomComposeEdit from "@app/components/RoomComposeEdit.svelte" import RoomComposeParent from "@app/components/RoomComposeParent.svelte" @@ -232,6 +234,8 @@ } } + const getElementKey = (element: {id: string}) => element.id + const manageScrollPosition = () => { // Only treat an `at` jump as "scrolled up" when it targets an event below the // newest one; jumping to the most recent message already lands us at the bottom. @@ -259,6 +263,10 @@ const targetEvent = $events.find(event => event.created_at >= at) if (targetEvent) { + // The list renders from the newest message outward, so a jump target deep in history + // may not be on the page yet + virtualList?.reveal(targetEvent.id) + const target = element?.querySelector(`[data-event="${targetEvent.id}"]`) if (target instanceof HTMLElement) { @@ -280,8 +288,16 @@ isProgrammaticScroll = false } - const scrollToNewMessages = () => - document.getElementById("new-messages")?.scrollIntoView({behavior: "smooth", block: "center"}) + const scrollToNewMessages = () => { + virtualList?.reveal("new-messages") + + // Revealing it renders it on the next flush, so the scroll has to wait for that + requestAnimationFrame(() => + document + .getElementById("new-messages") + ?.scrollIntoView({behavior: "smooth", block: "center"}), + ) + } const scrollToBottom = () => { if (!isNaN(at)) { @@ -308,6 +324,7 @@ let userHasScrolled = $state(false) let isProgrammaticScroll = $state(false) let isUserScrolling = $state(false) + let virtualList: Maybe = $state() let older: Maybe> = $state() let newer: Maybe> = $state() let share: Maybe = $state() @@ -568,39 +585,45 @@ Looking for messages...

{/if} - {#each elements as { type, id, value, showPubkey } (id)} - {#if type === "new-messages"} -
-
-

- New Messages -

-
-
- {:else if type === "date"} - {value} - {:else} - {@const event = value as TrustedEvent} - {#if event.kind === addMemberKind} - + + {#snippet row({type, id, value, showPubkey})} + {#if type === "new-messages"} +
+
+

+ New Messages +

+
+
+ {:else if type === "date"} + {value} {:else} - + {@const event = value as TrustedEvent} + {#if event.kind === addMemberKind} + + {:else} + + {/if} {/if} - {/if} - {/each} + {/snippet} +

{#if loadingBackward} Looking for messages... diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte new file mode 100644 index 00000000..beaf62d9 --- /dev/null +++ b/src/lib/components/VirtualList.svelte @@ -0,0 +1,106 @@ + + + + +{#each visible as item (getKey(item))} + {@render row(item)} +{/each}