Support partials for homepage and contacts
Always using kind:30023 articles
This commit is contained in:
parent
052bc45260
commit
e453cafa76
4 changed files with 89 additions and 0 deletions
67
src/lib/partials.svelte.ts
Normal file
67
src/lib/partials.svelte.ts
Normal file
|
|
@ -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<string>(["home", "contacts"]);
|
||||||
|
|
||||||
|
export type Partial = {
|
||||||
|
id: string;
|
||||||
|
slot: PartialSlot;
|
||||||
|
title: string;
|
||||||
|
content: string; // markdown body
|
||||||
|
pubkey: string;
|
||||||
|
createdAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let all = $state<Partial[]>([]);
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
import { loadGroup } from "$lib/group.svelte";
|
import { loadGroup } from "$lib/group.svelte";
|
||||||
import { loadGroups, groupsStore } from "$lib/groups.svelte";
|
import { loadGroups, groupsStore } from "$lib/groups.svelte";
|
||||||
import { loadResources } from "$lib/resources.svelte";
|
import { loadResources } from "$lib/resources.svelte";
|
||||||
|
import { loadPartials } from "$lib/partials.svelte";
|
||||||
import { loadRoomAdmins } from "$lib/admins.svelte";
|
import { loadRoomAdmins } from "$lib/admins.svelte";
|
||||||
import { seedProfiles } from "$lib/profiles.svelte";
|
import { seedProfiles } from "$lib/profiles.svelte";
|
||||||
import { startChat } from "$lib/chat.svelte";
|
import { startChat } from "$lib/chat.svelte";
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
loadResources();
|
loadResources();
|
||||||
|
loadPartials();
|
||||||
const tasks = [restoreSession(), loadGroup()];
|
const tasks = [restoreSession(), loadGroup()];
|
||||||
if (mode === "full") tasks.push(loadGroups());
|
if (mode === "full") tasks.push(loadGroups());
|
||||||
await Promise.all(tasks);
|
await Promise.all(tasks);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DiscussionsFeed from "$lib/components/DiscussionsFeed.svelte";
|
import DiscussionsFeed from "$lib/components/DiscussionsFeed.svelte";
|
||||||
|
import PostContent from "$lib/components/PostContent.svelte";
|
||||||
import { groupsStore } from "$lib/groups.svelte";
|
import { groupsStore } from "$lib/groups.svelte";
|
||||||
import { overviewStore, loadOverview } from "$lib/overview.svelte";
|
import { overviewStore, loadOverview } from "$lib/overview.svelte";
|
||||||
|
import { partialsStore } from "$lib/partials.svelte";
|
||||||
import { MODE, GROUP_ID } from "$lib/config";
|
import { MODE, GROUP_ID } from "$lib/config";
|
||||||
import type { NostrUser } from "@nostr/gadgets/metadata";
|
import type { NostrUser } from "@nostr/gadgets/metadata";
|
||||||
|
|
||||||
|
const partial = $derived(partialsStore.get("home"));
|
||||||
|
|
||||||
// Full mode: load per-room activity + recent threads once the rooms are known.
|
// Full mode: load per-room activity + recent threads once the rooms are known.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (MODE !== "full") return;
|
if (MODE !== "full") return;
|
||||||
|
|
@ -46,6 +50,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
{#if partial}
|
||||||
|
<div class="mt-4 mb-8">
|
||||||
|
<PostContent content={partial.content} headingOffset={0} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if MODE === "simple"}
|
{#if MODE === "simple"}
|
||||||
<DiscussionsFeed groupId={GROUP_ID} title="Discussions" />
|
<DiscussionsFeed groupId={GROUP_ID} title="Discussions" />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@
|
||||||
ensureProfile,
|
ensureProfile,
|
||||||
type ProfileEntry,
|
type ProfileEntry,
|
||||||
} from "$lib/profiles.svelte";
|
} from "$lib/profiles.svelte";
|
||||||
|
import { partialsStore } from "$lib/partials.svelte";
|
||||||
|
import PostContent from "$lib/components/PostContent.svelte";
|
||||||
|
|
||||||
|
const partial = $derived(partialsStore.get("contacts"));
|
||||||
|
|
||||||
// Full mode pulls admins from every room; simple mode uses the one group.
|
// Full mode pulls admins from every room; simple mode uses the one group.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -70,6 +74,12 @@
|
||||||
<div class="mx-auto max-w-6xl">
|
<div class="mx-auto max-w-6xl">
|
||||||
<h1 class="py-2 text-[1.65rem] text-brand">Contacts</h1>
|
<h1 class="py-2 text-[1.65rem] text-brand">Contacts</h1>
|
||||||
|
|
||||||
|
{#if partial}
|
||||||
|
<div class="mb-8">
|
||||||
|
<PostContent content={partial.content} headingOffset={0} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="py-6 text-center text-neutral-400">Loading…</p>
|
<p class="py-6 text-center text-neutral-400">Loading…</p>
|
||||||
{:else if contacts.length === 0}
|
{:else if contacts.length === 0}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue