From 75d1d16cf1350161d07f5a529fe63d0e83ab5342 Mon Sep 17 00:00:00 2001 From: dtonon Date: Thu, 26 Mar 2026 16:09:03 +0100 Subject: [PATCH] Fetch and show real threads --- src/lib/components/ThreadItem.svelte | 76 ++++++++++--------- src/lib/threads.svelte.ts | 105 +++++++++++++++++++++++++++ src/routes/+page.svelte | 56 +++++++++++++- src/routes/room/[slug]/+page.svelte | 25 ++++++- 4 files changed, 220 insertions(+), 42 deletions(-) create mode 100644 src/lib/threads.svelte.ts diff --git a/src/lib/components/ThreadItem.svelte b/src/lib/components/ThreadItem.svelte index 9977988..6ed9f91 100644 --- a/src/lib/components/ThreadItem.svelte +++ b/src/lib/components/ThreadItem.svelte @@ -1,16 +1,39 @@ +{#snippet avatar(a: Author, cls: string)} + {#if a.picture} + + {:else} + + {a.name[0].toUpperCase()} + + {/if} +{/snippet} + {thread.title}
by - {thread.op.author.name} - {#if thread.participants.length > 1} + {@render avatar(thread.author, "h-5 w-5 rounded-full object-cover")} + {#if thread.repliers.length > 0} and
- {#each thread.participants - .filter((p) => p.pubkey !== thread.op.author.pubkey) - .slice(0, 4) as p} - {p.name} + {#each thread.repliers as r, i} + + {@render avatar(r, "h-5 w-5 rounded-full object-cover ring-1 ring-white")} + {/each}
{/if}
- -
- {#each thread.tags as tag} - - {/each} -
-
- {thread.replyCount} + {thread.replyCount} replies
- {score} + {thread.score} score
- {thread.participants[thread.participants.length - {thread.lastActivity} + {@render avatar(thread.lastActiveAuthor, "h-5 w-5 rounded-full object-cover")} + {thread.lastActivity}
activity
diff --git a/src/lib/threads.svelte.ts b/src/lib/threads.svelte.ts new file mode 100644 index 0000000..9a81331 --- /dev/null +++ b/src/lib/threads.svelte.ts @@ -0,0 +1,105 @@ +import { SimplePool } from "@nostr/tools"; +import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata"; +import { RELAY_URL, GROUP_ID } from "$lib/config"; + +export type ThreadData = { + id: string; + title: string; + authorPubkey: string; + createdAt: number; + replyCount: number; + latestAt: number; + latestPubkey: string; + replierPubkeys: string[]; // unique reply authors, excl. OP, max 4 +}; + +let threads = $state([]); +let profiles = $state>({}); + +export const threadStore = { + get threads() { return threads; }, + get profiles() { return profiles; }, +}; + +async function loadProfile(pubkey: string) { + if (profiles[pubkey]) return; + const user = await loadNostrUser(pubkey); + profiles[pubkey] = user; +} + +export async function loadThreads() { + const pool = new SimplePool(); + try { + // Step 1: fetch threads + const events = await pool.querySync([RELAY_URL], { + kinds: [11], + "#h": [GROUP_ID], + limit: 50, + }); + + events.sort((a, b) => b.created_at - a.created_at); + + threads = events.map((e) => ({ + id: e.id, + title: e.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)", + authorPubkey: e.pubkey, + createdAt: e.created_at, + replyCount: 0, + latestAt: e.created_at, + latestPubkey: e.pubkey, + replierPubkeys: [], + })); + + events.forEach((e) => loadProfile(e.pubkey)); + + if (events.length === 0) return; + + // Step 2: fetch replies for all threads in one request + const replies = await pool.querySync([RELAY_URL], { + kinds: [1111], + "#E": events.map((e) => e.id), + }); + + // Aggregate per-thread reply data + const replyMap = new Map< + string, + { count: number; latestAt: number; latestPubkey: string; pubkeys: Set } + >(); + + for (const reply of replies) { + const rootId = reply.tags.find((t) => t[0] === "E")?.[1]; + if (!rootId) continue; + if (!replyMap.has(rootId)) { + replyMap.set(rootId, { + count: 0, + latestAt: 0, + latestPubkey: reply.pubkey, + pubkeys: new Set(), + }); + } + const rd = replyMap.get(rootId)!; + rd.count++; + rd.pubkeys.add(reply.pubkey); + if (reply.created_at > rd.latestAt) { + rd.latestAt = reply.created_at; + rd.latestPubkey = reply.pubkey; + } + } + + threads = threads.map((t) => { + const rd = replyMap.get(t.id); + if (!rd) return t; + return { + ...t, + replyCount: rd.count, + latestAt: rd.latestAt, + latestPubkey: rd.latestPubkey, + replierPubkeys: [...rd.pubkeys].slice(0, 4), + }; + }); + + replies.forEach((r) => loadProfile(r.pubkey)); + } finally { + pool.close([RELAY_URL]); + } +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 378804a..66f3411 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,56 @@ @@ -18,7 +68,7 @@
- {#each threads as thread} + {#each rows as thread} {/each}
diff --git a/src/routes/room/[slug]/+page.svelte b/src/routes/room/[slug]/+page.svelte index 954a50b..c2da284 100644 --- a/src/routes/room/[slug]/+page.svelte +++ b/src/routes/room/[slug]/+page.svelte @@ -1,10 +1,29 @@ @@ -22,7 +41,7 @@
- {#each threads as thread} + {#each rows as thread} {/each}