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"}
-
- {: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"}
+
+ {: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}