From 043be1d3fd5a786093270659d33376a7879a88d2 Mon Sep 17 00:00:00 2001 From: dtonon Date: Tue, 9 Jun 2026 21:23:13 +0100 Subject: [PATCH] Add NIP-50 thread search helper --- src/lib/search.ts | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/lib/search.ts diff --git a/src/lib/search.ts b/src/lib/search.ts new file mode 100644 index 0000000..e32aa07 --- /dev/null +++ b/src/lib/search.ts @@ -0,0 +1,71 @@ +import type { Event } from "@nostr/tools/core"; +import type { Filter } from "@nostr/tools/filter"; +import { queryForum } from "$lib/relay"; +import { GROUP_ID, MODE } from "$lib/config"; +import { groupsStore } from "$lib/groups.svelte"; + +export type SearchResult = { + threadId: string; + title: string; + snippet: string; + matchKind: "thread" | "reply"; + createdAt: number; +}; + +function snippetOf(content: string): string { + const flat = content.replace(/\s+/g, " ").trim(); + return flat.length > 140 ? flat.slice(0, 140) + "…" : flat; +} + +// NIP-50 search over the forum relay: thread OPs (kind 11) and replies +// (kind 1111), deduped by thread. Simple mode scopes to the single group; +// full mode spans every visible room. +export async function searchThreads(query: string): Promise { + const filter: Filter = { kinds: [11, 1111], search: query, limit: 30 }; + if (MODE === "simple") filter["#h"] = [GROUP_ID]; + else if (groupsStore.list.length > 0) + filter["#h"] = groupsStore.list.map((g) => g.id); + + const events = await queryForum(filter, { label: "search" }); + + const byThread = new Map(); + const opless: SearchResult[] = []; + for (const e of events) { + if (e.kind === 11) { + const existing = byThread.get(e.id); + // An OP match wins over a reply match on the same thread + if (existing && existing.matchKind === "thread") continue; + byThread.set(e.id, { + threadId: e.id, + title: e.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)", + snippet: snippetOf(e.content), + matchKind: "thread", + createdAt: e.created_at, + }); + } else { + const root = e.tags.find((t) => t[0] === "E")?.[1]; + if (!root || byThread.has(root)) continue; + const r: SearchResult = { + threadId: root, + title: "", + snippet: snippetOf(e.content), + matchKind: "reply", + createdAt: e.created_at, + }; + byThread.set(root, r); + opless.push(r); + } + } + + // Backfill titles for reply-only matches; the group relay truncates + // multi-id queries, so fetch each OP on its own + await Promise.all( + opless.map(async (r) => { + const ops = await queryForum({ kinds: [11], ids: [r.threadId] }); + const op: Event | undefined = ops[0]; + r.title = op?.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)"; + }), + ); + + return [...byThread.values()].sort((a, b) => b.createdAt - a.createdAt); +}