import { auth } from "$lib/auth.svelte"; import { withJoin } from "$lib/join.svelte"; import { activeGroup } from "$lib/active.svelte"; import { publishForum } from "$lib/relay"; import { extractMentionPubkeys, buildPTagHints } from "$lib/mentions"; import { convertForumUrls } from "$lib/linkify"; let modalOpen = $state(false); let iconized = $state(false); let title = $state(""); let labels = $state([]); let content = $state(""); let publishing = $state(false); let publishError = $state(null); export const draftState = { get modalOpen() { return modalOpen; }, get iconized() { return iconized; }, get title() { return title; }, set title(v: string) { title = v; }, get labels() { return labels; }, get content() { return content; }, set content(v: string) { content = v; }, get publishing() { return publishing; }, get publishError() { return publishError; }, get hasDraft() { return ( title.trim().length > 0 || content.trim().length > 0 || labels.length > 0 ); }, }; export function openDraft() { modalOpen = true; iconized = false; publishError = null; } export function iconizeDraft() { // If nothing was entered, fully close instead of showing the resume pill if (!draftState.hasDraft) { discardDraft(); return; } modalOpen = false; iconized = true; } export function resumeDraft() { modalOpen = true; iconized = false; } export function discardDraft() { title = ""; labels = []; content = ""; publishError = null; modalOpen = false; iconized = false; } export function addLabel(l: string) { const v = l.trim(); if (!v) return; if (labels.includes(v)) return; labels = [...labels, v]; } export function removeLabel(l: string) { labels = labels.filter((x) => x !== l); } export async function publishDraft(): Promise<{ ok: boolean; threadId?: string; }> { if (!auth.signer) { publishError = "Not logged in"; return { ok: false }; } const t = title.trim(); if (!t || !content.trim()) { publishError = "Title and content are required"; return { ok: false }; } const c = convertForumUrls(content.trim()); const groupId = activeGroup.id; if (!groupId) { publishError = "No room selected"; return { ok: false }; } publishing = true; publishError = null; let threadId: string | undefined; const ownPubkey = auth.user?.pubkey; const mentionPubkeys = extractMentionPubkeys(c).filter( (pk) => pk !== ownPubkey, ); const hints = await buildPTagHints(mentionPubkeys); try { const success = await withJoin(groupId, async () => { const tags: string[][] = [ ["h", groupId], ["title", t], ]; for (const l of labels) tags.push(["t", l]); for (const pk of mentionPubkeys) { const hint = hints.get(pk); tags.push(hint ? ["p", pk, hint] : ["p", pk]); } const event = await auth.signer!.signEvent({ kind: 11, created_at: Math.floor(Date.now() / 1000), tags, content: c, }); const timeout = new Promise((_, reject) => setTimeout( () => reject(new Error("Relay did not respond in time")), 8000, ), ); await Promise.race([Promise.all(publishForum(event)), timeout]); threadId = event.id; }); if (success && threadId) { discardDraft(); return { ok: true, threadId }; } return { ok: false }; } catch (e) { publishError = e instanceof Error ? e.message : "Failed to publish"; return { ok: false }; } finally { publishing = false; } }