diff --git a/src/lib/partials.svelte.ts b/src/lib/partials.svelte.ts new file mode 100644 index 0000000..e2fe396 --- /dev/null +++ b/src/lib/partials.svelte.ts @@ -0,0 +1,67 @@ +import { SimplePool } from "@nostr/tools"; +import { RELAY_URL } from "$lib/config"; +import { adminPubkeys } from "$lib/admins.svelte"; + +// Named slots a partial can fill. The NIP-23 `d` tag carries the slot name. +export type PartialSlot = "home" | "contacts"; +const SLOTS = new Set(["home", "contacts"]); + +export type Partial = { + id: string; + slot: PartialSlot; + title: string; + content: string; // markdown body + pubkey: string; + createdAt: number; +}; + +let all = $state([]); +let loaded = $state(false); + +export const partialsStore = { + // The newest admin-authored partial for a slot, or undefined. The relay query + // is open, so the trusted admin set is the gate; resolving the winner here + // (rather than at load time) keeps a non-admin from shadowing it with a newer + // event, and picks up the admin set once it finishes loading in full mode. + get(slot: PartialSlot): Partial | undefined { + const admins = new Set(adminPubkeys.list); + let best: Partial | undefined; + for (const p of all) { + if (p.slot !== slot || !admins.has(p.pubkey)) continue; + if (!best || p.createdAt > best.createdAt) best = p; + } + return best; + }, + get loaded() { + return loaded; + }, +}; + +// Partials are kind 30023 (NIP-23 long-form) tagged ["t", "squalk-partial"]; +// the `d` tag names the slot the article fills. +export async function loadPartials() { + const pool = new SimplePool(); + try { + const events = await pool.querySync([RELAY_URL], { + kinds: [30023], + "#t": ["squalk-partial"], + }); + const next: Partial[] = []; + for (const e of events) { + const slot = e.tags.find((t) => t[0] === "d")?.[1]; + if (!slot || !SLOTS.has(slot)) continue; + next.push({ + id: e.id, + slot: slot as PartialSlot, + title: e.tags.find((t) => t[0] === "title")?.[1] ?? slot, + content: e.content, + pubkey: e.pubkey, + createdAt: e.created_at, + }); + } + all = next; + } finally { + loaded = true; + pool.close([RELAY_URL]); + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5b96522..344c0dc 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -15,6 +15,7 @@ import { loadGroup } from "$lib/group.svelte"; import { loadGroups, groupsStore } from "$lib/groups.svelte"; import { loadResources } from "$lib/resources.svelte"; + import { loadPartials } from "$lib/partials.svelte"; import { loadRoomAdmins } from "$lib/admins.svelte"; import { seedProfiles } from "$lib/profiles.svelte"; import { startChat } from "$lib/chat.svelte"; @@ -28,6 +29,7 @@ onMount(async () => { loadResources(); + loadPartials(); const tasks = [restoreSession(), loadGroup()]; if (mode === "full") tasks.push(loadGroups()); await Promise.all(tasks); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1782c82..b2e17b1 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,10 +1,14 @@