2026-09-08 22:47:51 +00:00
|
|
|
<script lang="ts">
|
2026-09-09 00:35:47 +00:00
|
|
|
import {onDestroy} from "svelte"
|
|
|
|
|
import {writable} from "svelte/store"
|
|
|
|
|
import type {Writable} from "svelte/store"
|
|
|
|
|
import {sortBy} from "@welshman/lib"
|
|
|
|
|
import type {Maybe} from "@welshman/lib"
|
|
|
|
|
import {NOTE, outbox} from "@welshman/util"
|
2026-09-08 22:47:51 +00:00
|
|
|
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"
|
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-09 01:13:19 +00:00
|
|
|
import HomeNetworkItem from "@app/components/HomeNetworkItem.svelte"
|
2026-09-09 00:35:47 +00:00
|
|
|
import {followLists, relayLists, router, user} from "@app/core"
|
|
|
|
|
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
// 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
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 01:13:19 +00:00
|
|
|
const context = makeFeedContext({
|
|
|
|
|
relays: $relayLists.readUrls($user.pubkey).get(),
|
|
|
|
|
withReplies: true,
|
|
|
|
|
})
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
const followList = $derived($followLists.one($user.pubkey))
|
|
|
|
|
const follows = $derived($followList?.pubkeys())
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
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>
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
// Replies are left out - without their parent they read as half a conversation.
|
|
|
|
|
const notes = $derived(
|
|
|
|
|
sortBy(
|
|
|
|
|
e => -e.created_at,
|
|
|
|
|
$events.filter(e => getReplyTags(e.tags).replies.length === 0),
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
const loading = $derived(isFeedLoading($older))
|
|
|
|
|
const exhausted = $derived($older?.status === "exhausted")
|
|
|
|
|
const isEmpty = $derived(follows?.length === 0 || (exhausted && notes.length === 0))
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
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], authors: pubkeys}],
|
|
|
|
|
onEvent: context.add,
|
|
|
|
|
})
|
2026-09-08 22:47:51 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
events = feed.events
|
|
|
|
|
older = makeScrollLoader(element!, feed.loadOlder)
|
|
|
|
|
stop = () => {
|
|
|
|
|
older?.stop()
|
|
|
|
|
feed.cleanup()
|
2026-09-08 22:47:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-09-09 00:13:54 +00:00
|
|
|
|
2026-09-09 00:35:47 +00:00
|
|
|
onDestroy(() => {
|
|
|
|
|
stop?.()
|
|
|
|
|
context.cleanup()
|
2026-09-09 00:13:54 +00:00
|
|
|
})
|
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:35:47 +00:00
|
|
|
{:else if notes.length === 0}
|
2026-09-09 00:13:54 +00:00
|
|
|
<div class="flex justify-center pb-4">
|
|
|
|
|
<Spinner loading>Looking for notes from people you follow…</Spinner>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
2026-09-09 00:35:47 +00:00
|
|
|
<Masonry items={notes} getKey={event => event.id} columnWidth={80} maxColumns={2} gap={3}>
|
2026-09-09 00:13:54 +00:00
|
|
|
{#snippet child(event)}
|
2026-09-09 01:13:19 +00:00
|
|
|
<HomeNetworkItem {event} {context} />
|
2026-09-09 00:13:54 +00:00
|
|
|
{/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>
|