2026-09-08 22:47:51 +00:00
|
|
|
<script lang="ts">
|
|
|
|
|
import {onDestroy, onMount} from "svelte"
|
|
|
|
|
import {sortBy, uniqBy} from "@welshman/lib"
|
|
|
|
|
import {NOTE} from "@welshman/util"
|
|
|
|
|
import type {TrustedEvent} from "@welshman/util"
|
|
|
|
|
import {getReplyTags} from "@welshman/domain"
|
|
|
|
|
import {Feeds} from "@welshman/app"
|
|
|
|
|
import {Scope, feedFromFilter, makeIntersectionFeed, makeScopeFeed} from "@welshman/feeds"
|
|
|
|
|
import Planet from "@assets/icons/planet.svg?dataurl"
|
2026-09-09 00:13:54 +00:00
|
|
|
import {createScroller} from "@lib/html"
|
2026-09-08 22:47:51 +00:00
|
|
|
import Link from "@lib/components/Link.svelte"
|
2026-09-09 00:13:54 +00:00
|
|
|
import Masonry from "@lib/components/Masonry.svelte"
|
2026-09-08 22:47:51 +00:00
|
|
|
import Spinner from "@lib/components/Spinner.svelte"
|
2026-09-08 23:30:23 +00:00
|
|
|
import HomeSection from "@app/components/HomeSection.svelte"
|
2026-09-08 22:47:51 +00:00
|
|
|
import NoteItem from "@app/components/NoteItem.svelte"
|
|
|
|
|
import {app, relayLists, user} from "@app/core"
|
|
|
|
|
import {makeFeedContext} from "@app/feeds"
|
|
|
|
|
|
|
|
|
|
const PAGE_SIZE = 5
|
|
|
|
|
|
|
|
|
|
const context = makeFeedContext({relays: $relayLists.readUrls($user.pubkey).get()})
|
|
|
|
|
|
|
|
|
|
onDestroy(context.cleanup)
|
|
|
|
|
|
|
|
|
|
// Notes from the people the user follows, resolved against each author's outbox relays.
|
|
|
|
|
// Replies are left out - without their parent they read as half a conversation.
|
|
|
|
|
const controller = $app.use(Feeds).makeFeedController({
|
|
|
|
|
useWindowing: true,
|
|
|
|
|
feed: makeIntersectionFeed(makeScopeFeed(Scope.Follows), feedFromFilter({kinds: [NOTE]})),
|
|
|
|
|
onEvent: (event: TrustedEvent) => {
|
|
|
|
|
if (getReplyTags(event.tags).replies.length === 0) {
|
2026-09-09 00:13:54 +00:00
|
|
|
buffer.push(event)
|
2026-09-08 22:47:51 +00:00
|
|
|
context.add(event)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-09 00:13:54 +00:00
|
|
|
let element: Element | undefined = $state()
|
2026-09-08 22:47:51 +00:00
|
|
|
let events = $state<TrustedEvent[]>([])
|
2026-09-09 00:13:54 +00:00
|
|
|
let loading = $state(false)
|
|
|
|
|
let caughtUp = $state(false)
|
|
|
|
|
let buffer: TrustedEvent[] = []
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:13:54 +00:00
|
|
|
const isEmpty = $derived(caughtUp && events.length === 0)
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:13:54 +00:00
|
|
|
const load = async () => {
|
|
|
|
|
if (!loading) {
|
|
|
|
|
loading = true
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:13:54 +00:00
|
|
|
try {
|
|
|
|
|
await controller.load(PAGE_SIZE * 4)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e)
|
|
|
|
|
} finally {
|
|
|
|
|
loading = false
|
|
|
|
|
caughtUp = buffer.length === 0
|
|
|
|
|
}
|
2026-09-08 22:47:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 00:13:54 +00:00
|
|
|
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
|
|
|
|
|
})
|
2026-09-08 22:47:51 +00:00
|
|
|
</script>
|
|
|
|
|
|
2026-09-08 23:30:23 +00:00
|
|
|
<HomeSection title="Network" icon={Planet}>
|
2026-09-09 00:13:54 +00:00
|
|
|
<div class="flex flex-col gap-3 px-4 pb-4" bind:this={element}>
|
|
|
|
|
{#if isEmpty}
|
|
|
|
|
<div class="flex flex-col items-center gap-3 pb-4 text-center">
|
2026-09-08 22:47:51 +00:00
|
|
|
<p class="font-medium">Follow a few people to fill this out</p>
|
|
|
|
|
<p class="max-w-md text-sm opacity-75">
|
|
|
|
|
Notes from the people you follow collect here. Spaces are a good place to find some.
|
|
|
|
|
</p>
|
|
|
|
|
<Link href="/spaces" class="button button-neutral button-sm">Browse spaces</Link>
|
|
|
|
|
</div>
|
2026-09-09 00:13:54 +00:00
|
|
|
{:else if events.length === 0}
|
|
|
|
|
<div class="flex justify-center pb-4">
|
|
|
|
|
<Spinner loading>Looking for notes from people you follow…</Spinner>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<Masonry items={events} getKey={event => event.id} columnWidth={80} maxColumns={2} gap={3}>
|
|
|
|
|
{#snippet child(event)}
|
|
|
|
|
<NoteItem {event} {context} />
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Masonry>
|
|
|
|
|
{#if loading}
|
|
|
|
|
<div class="flex justify-center py-4">
|
|
|
|
|
<Spinner loading />
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2026-09-08 22:47:51 +00:00
|
|
|
{/if}
|
2026-09-09 00:13:54 +00:00
|
|
|
</div>
|
2026-09-08 23:30:23 +00:00
|
|
|
</HomeSection>
|