diff --git a/src/app/components/HomeNetwork.svelte b/src/app/components/HomeNetwork.svelte index fb8c233a..9524b242 100644 --- a/src/app/components/HomeNetwork.svelte +++ b/src/app/components/HomeNetwork.svelte @@ -7,8 +7,9 @@ import {Feeds} from "@welshman/app" import {Scope, feedFromFilter, makeIntersectionFeed, makeScopeFeed} from "@welshman/feeds" import Planet from "@assets/icons/planet.svg?dataurl" + import {createScroller} from "@lib/html" import Link from "@lib/components/Link.svelte" - import Button from "@lib/components/Button.svelte" + import Masonry from "@lib/components/Masonry.svelte" import Spinner from "@lib/components/Spinner.svelte" import HomeSection from "@app/components/HomeSection.svelte" import NoteItem from "@app/components/NoteItem.svelte" @@ -28,61 +29,83 @@ feed: makeIntersectionFeed(makeScopeFeed(Scope.Follows), feedFromFilter({kinds: [NOTE]})), onEvent: (event: TrustedEvent) => { if (getReplyTags(event.tags).replies.length === 0) { - events = uniqBy( - event => event.id, - sortBy(event => -event.created_at, [...events, event]), - ) + buffer.push(event) context.add(event) } }, }) + let element: Element | undefined = $state() let events = $state([]) - let limit = $state(PAGE_SIZE) - let loading = $state(true) + let loading = $state(false) + let caughtUp = $state(false) + let buffer: TrustedEvent[] = [] - const load = () => { - loading = true - controller - .load(PAGE_SIZE * 4) - .catch(e => console.error(e)) - .finally(() => (loading = false)) - } + const isEmpty = $derived(caughtUp && events.length === 0) - const showMore = () => { - limit += PAGE_SIZE + const load = async () => { + if (!loading) { + loading = true - if (events.length < limit + PAGE_SIZE) { - load() + try { + await controller.load(PAGE_SIZE * 4) + } catch (e) { + console.error(e) + } finally { + loading = false + caughtUp = buffer.length === 0 + } } } - onMount(load) + onMount(() => { + const scroller = createScroller({ + element: element!, + delay: 300, + threshold: 3000, + onScroll: () => { + buffer = uniqBy( + e => e.id, + sortBy(e => -e.created_at, buffer), + ) + + events = uniqBy(e => e.id, [...events, ...buffer.splice(0, PAGE_SIZE)]) + + if (buffer.length < PAGE_SIZE * 4) { + load() + } + }, + }) + + return scroller.stop + }) - {#if events.length === 0} - {#if loading} -
- Looking for notes from people you follow… -
- {:else} -
+
+ {#if isEmpty} +

Follow a few people to fill this out

Notes from the people you follow collect here. Spaces are a good place to find some.

Browse spaces
+ {:else if events.length === 0} +
+ Looking for notes from people you follow… +
+ {:else} + event.id} columnWidth={80} maxColumns={2} gap={3}> + {#snippet child(event)} + + {/snippet} + + {#if loading} +
+ +
+ {/if} {/if} - {:else} -
- {#each events.slice(0, limit) as event (event.id)} - - {/each} -
-
- -
- {/if} +
diff --git a/src/lib/components/Masonry.svelte b/src/lib/components/Masonry.svelte index 1913e084..8b3ebd9b 100644 --- a/src/lib/components/Masonry.svelte +++ b/src/lib/components/Masonry.svelte @@ -5,23 +5,40 @@ items: T[] child: Snippet<[T]> getKey: (item: T) => PropertyKey - // Minimum column width, in tailwind spacing units (1 = 0.25rem). The browser - // fits as many columns as the container allows, so the layout stays - // responsive without media queries. columnWidth?: number - // Space between columns and between stacked items, in tailwind units. + maxColumns?: number gap?: number } - const {items, child, getKey, columnWidth = 72, gap = 3}: Props = $props() + // columnWidth and gap are tailwind spacing units, not pixels. + const {items, child, getKey, columnWidth = 72, maxColumns = Infinity, gap = 3}: Props = $props() + + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16 const rem = (units: number) => `${units * 0.25}rem` + + let width = $state(0) + + const count = $derived( + Math.min(maxColumns, Math.max(1, Math.floor(width / (columnWidth * 0.25 * rootFontSize)))), + ) + + // Which column an item lands in depends only on its index, so appending to `items` leaves + // everything already on screen where it was. CSS multi-column balances the columns instead, + // which reshuffles the whole list every time an infinite scroll loads another page. + const columns = $derived( + Array.from({length: count}, (_, column) => + items.filter((_, index) => index % count === column), + ), + ) -
- {#each items as item (getKey(item))} -
- {@render child(item)} +
+ {#each columns as column, index (index)} +
+ {#each column as item (getKey(item))} + {@render child(item)} + {/each}
{/each}
diff --git a/src/routes/home/+page.svelte b/src/routes/home/+page.svelte index 7903e33a..18a31fa8 100644 --- a/src/routes/home/+page.svelte +++ b/src/routes/home/+page.svelte @@ -39,7 +39,7 @@
-
+