flotilla/src/app/components/HomeNetwork.svelte

111 lines
3.9 KiB
Svelte

<script lang="ts">
import {onDestroy} from "svelte"
import {writable} from "svelte/store"
import type {Writable} from "svelte/store"
import {sortBy, uniqBy} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {NOTE, getIdOrAddress, outbox} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {getReplyTags} from "@welshman/domain"
import Planet from "@assets/icons/planet.svg?dataurl"
import Link from "@lib/components/Link.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import HomeSection from "@app/components/HomeSection.svelte"
import HomeNetworkItem from "@app/components/HomeNetworkItem.svelte"
import {CONTENT_KINDS} from "@app/content"
import {followLists, relayLists, router, user} from "@app/core"
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
const {class: className = ""}: {class?: string} = $props()
// The hubs most of the user's follows publish to, which is the same set and the same limit
// `syncFollowNetwork` reads their lists from.
const RELAY_LIMIT = 8
const context = makeFeedContext({
relays: $relayLists.readUrls($user.pubkey).get(),
withReplies: true,
})
const followList = $derived($followLists.one($user.pubkey))
const follows = $derived($followList?.pubkeys())
let element: HTMLElement | undefined = $state()
let older: Maybe<ReturnType<typeof makeScrollLoader>> = $state()
let events: Writable<TrustedEvent[]> = $state(writable([]))
let started = false
let stop: Maybe<() => void>
// Kind 1 replies are left out - without their parent they read as half a conversation. Every
// other kind here is a root, and an edited one is shown once, at its latest version.
const isRoot = (event: TrustedEvent) =>
event.kind !== NOTE || getReplyTags(event.tags).replies.length === 0
const items = $derived(
uniqBy(
getIdOrAddress,
sortBy(e => -e.created_at, $events.filter(isRoot)),
),
)
const loading = $derived(isFeedLoading($older))
const exhausted = $derived($older?.status === "exhausted")
const isEmpty = $derived(follows?.length === 0 || (exhausted && items.length === 0))
const start = async (pubkeys: string[]) => {
const scenario = await $router.resolve(pubkeys.map(pubkey => outbox(pubkey)))
const feed = makeFeed({
relays: scenario.limit(RELAY_LIMIT).getUrls(),
filters: [{kinds: [NOTE, ...CONTENT_KINDS], authors: pubkeys}],
onEvent: context.add,
})
events = feed.events
older = makeScrollLoader(element!, feed.loadOlder)
stop = () => {
older?.stop()
feed.cleanup()
}
}
// The follow list is what the feed is made of, so it waits for one rather than asking about
// nobody. Follows added later don't rebuild it - the feed keeps what is already on screen.
$effect(() => {
if (!started && follows && follows.length > 0) {
started = true
start(follows)
}
})
onDestroy(() => {
stop?.()
context.cleanup()
})
</script>
<HomeSection title="Network" icon={Planet} class={className}>
<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">
<p class="font-medium">Follow a few people to fill this out</p>
<p class="max-w-md text-sm opacity-75">
Posts 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>
{:else if items.length === 0}
<div class="flex justify-center py-8">
<Spinner loading>Loading recent posts...</Spinner>
</div>
{:else}
{#each items as event (event.id)}
<HomeNetworkItem {event} {context} />
{/each}
{#if loading}
<div class="flex justify-center py-8">
<Spinner loading />
</div>
{/if}
{/if}
</div>
</HomeSection>