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?.()}