From 2e632b702f1352932aad704414d190fe9ee5f0a4 Mon Sep 17 00:00:00 2001 From: Bretton <36870434+BrettM86@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:45:58 -0700 Subject: [PATCH] fix(feed): unstick infinite scroll after fast jumps to the bottom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit svelte-infinite-scroll only re-dispatches loadMore once the user scrolls back out of the threshold zone, so an instant jump to the bottom (End key, scrollbar drag, programmatic scroll) latched it permanently and the feed stalled with an endless spinner. Observe the loading sentinel with an IntersectionObserver and chain the next page after a load if the viewport is still at the bottom. Also fix the end-of-feed message rendering as the truncated 'You have reached the end of' — the i18n placeholder branch never received its parameter, so the sentence tail was dropped. Co-Authored-By: Claude Fable 5 --- src/lib/app/i18n/en.json | 3 +- src/lib/feature/post/feed/VirtualFeed.svelte | 46 ++++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/lib/app/i18n/en.json b/src/lib/app/i18n/en.json index 92910c2e..14b7c398 100644 --- a/src/lib/app/i18n/en.json +++ b/src/lib/app/i18n/en.json @@ -235,7 +235,7 @@ "frontpage": { "title": "Coves", "footer": "{{users:number}} active {{users; 1:user; default:users;}}", - "endFeed": "You have reached the end of {{community_name; undefined:the feed.; default:{{community_name}}.}}", + "endFeed": "You have reached the end of the feed.", "empty": { "title": "No posts", "description": "There are no posts in this feed." @@ -564,6 +564,7 @@ "url": "URL" }, "post": { + "untitled": "Untitled post", "actions": { "comments": "Comments", "save": "Save", diff --git a/src/lib/feature/post/feed/VirtualFeed.svelte b/src/lib/feature/post/feed/VirtualFeed.svelte index e5b80b23..37c723f8 100644 --- a/src/lib/feature/post/feed/VirtualFeed.svelte +++ b/src/lib/feature/post/feed/VirtualFeed.svelte @@ -9,7 +9,7 @@ import Placeholder from '$lib/ui/info/Placeholder.svelte' import EndPlaceholder from '$lib/ui/layout/EndPlaceholder.svelte' import { Button, Material, Spinner } from 'mono-svelte' - import { onMount, untrack } from 'svelte' + import { onMount, tick, untrack } from 'svelte' import { ArchiveBox, ArrowTopRightOnSquare, @@ -62,6 +62,25 @@ (posts ?? []).map((fp) => fp.post.uri as string), ) + const SCROLL_THRESHOLD = 300 + + // svelte-infinite-scroll only re-dispatches loadMore after the user scrolls + // back OUT of the threshold zone, so a fast jump to the bottom (End key, + // scrollbar drag, programmatic scroll) permanently stalls the feed. Observe + // the loading sentinel directly so entering view always loads the next page. + let sentinel = $state() + $effect(() => { + if (!sentinel) return + const io = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) loadMore() + }, + { rootMargin: `${SCROLL_THRESHOLD}px` }, + ) + io.observe(sentinel) + return () => io.disconnect() + }) + async function loadMore(): Promise { if (!hasMore || loading || !loadFeed) return @@ -92,6 +111,21 @@ } finally { loading = false } + + // svelte-infinite-scroll latches after dispatching loadMore and only + // re-arms once the user scrolls back out of the threshold zone. If the + // viewport is still at the bottom when this page finishes loading, no + // further scroll event will ever fire — chain the next page ourselves. + if (!error && hasMore && browser) { + await tick() + const doc = document.documentElement + if ( + doc.scrollHeight - doc.clientHeight - doc.scrollTop <= + SCROLL_THRESHOLD + ) { + loadMore() + } + } } const callback: IntersectionObserverCallback = (entries, observer) => { @@ -198,7 +232,7 @@ class={['relative post-container', row < 7 && '']} > {:else if hasMore} -
+
{:else} @@ -259,7 +293,11 @@
{/if} - + {/if} {@render children?.()} -- 2.51.2