163 lines
3.6 KiB
TypeScript
163 lines
3.6 KiB
TypeScript
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<string[]>([]);
|
|
let content = $state("");
|
|
let publishing = $state(false);
|
|
let publishError = $state<string | null>(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<never>((_, 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;
|
|
}
|
|
}
|