-
-
- {#if !isValidKind(kind)}
-
- Kind must be an integer between 0 and 65535 (NIP-01).
-
- {/if}
- {#if Number(kind) === 30040}
-
-
Publication Index
-
-
-
- 30040 - Publication Index: Events that organize AsciiDoc content into structured publications with metadata tags and section references.
-
-
-
- {/if}
-
-
-
-
+
+ {#if loading}
+
Publishing...
+ {/if}
+ {#if error}
+
{error}
+ {/if}
+ {#if success}
+
{success}
+
+ Relays: {publishedRelays.join(", ")}
- {#if loading}
-
Publishing...
- {/if}
- {#if error}
-
{error}
- {/if}
- {#if success}
-
{success}
-
- Relays: {publishedRelays.join(", ")}
+ {#if lastPublishedEventId}
+
+ Event ID: {lastPublishedEventId}
+
- {#if lastPublishedEventId}
-
- Event ID: {lastPublishedEventId}
-
-
- {/if}
{/if}
-
-
-
-
-
-
Event Preview
-
-
-
- {#if showJsonPreview}
- {@const preview = eventPreview()}
- {#if preview}
-
- {#if preview.type === 'error'}
-
- {preview.message}
-
- {:else}
-
-
- Event Type: {preview.type === '30040_index_event' ? '30040 Publication Index' : 'Standard Event'}
-
-
-
{JSON.stringify(preview.event, null, 2)}
- {/if}
-
- {:else}
-
-
- Please log in to see the event preview.
-
-
- {/if}
- {/if}
-
-
- {#if showWarning}
-
-
-
Warning
-
{warningMessage}
-
-
-
-
-
-
{/if}
+
+
+
showJsonPreview = !showJsonPreview}
+ />
diff --git a/src/lib/components/event_input/EventForm.svelte b/src/lib/components/event_input/EventForm.svelte
new file mode 100644
index 0000000..94bf99f
--- /dev/null
+++ b/src/lib/components/event_input/EventForm.svelte
@@ -0,0 +1,162 @@
+
+
+
diff --git a/src/lib/components/event_input/EventPreview.svelte b/src/lib/components/event_input/EventPreview.svelte
new file mode 100644
index 0000000..55742fb
--- /dev/null
+++ b/src/lib/components/event_input/EventPreview.svelte
@@ -0,0 +1,172 @@
+
+
+
+
+
+
Event Preview
+
+
+
+ {#if showJsonPreview}
+ {#if eventPreview}
+
+ {#if eventPreview.type === 'error'}
+
+ {eventPreview.message}
+
+ {:else}
+
+
+ Event Type: {eventPreview.type === '30040_index_event' ? '30040 Publication Index' : 'Standard Event'}
+
+
+
{JSON.stringify(eventPreview.event, null, 2)}
+ {/if}
+
+ {:else}
+
+
+ Please log in to see the event preview.
+
+
+ {/if}
+ {/if}
+
diff --git a/src/lib/components/event_input/TagManager.svelte b/src/lib/components/event_input/TagManager.svelte
new file mode 100644
index 0000000..648fe56
--- /dev/null
+++ b/src/lib/components/event_input/TagManager.svelte
@@ -0,0 +1,342 @@
+
+
+
+
+
+
+ {#if extractedMetadata.length > 0}
+
+
+ Extracted Metadata (from AsciiDoc header)
+
+
+ {extractedMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}
+
+
+ {/if}
+
+
+
+
diff --git a/src/lib/components/event_input/eventServices.ts b/src/lib/components/event_input/eventServices.ts
new file mode 100644
index 0000000..84cb072
--- /dev/null
+++ b/src/lib/components/event_input/eventServices.ts
@@ -0,0 +1,277 @@
+/**
+ * Event publishing and loading services
+ */
+
+import { get } from "svelte/store";
+import { userStore } from "$lib/stores/userStore";
+import NDK, { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk";
+import type { NDKEvent } from "$lib/utils/nostrUtils";
+import { prefixNostrAddresses } from "$lib/utils/nostrUtils";
+import { fetchEventWithFallback } from "$lib/utils/nostrUtils";
+
+import { WebSocketPool } from "$lib/data_structures/websocket_pool";
+import { anonymousRelays } from "$lib/consts";
+import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
+import { removeMetadataFromContent } from "$lib/utils/asciidoc_metadata";
+import { build30040EventSet } from "$lib/utils/event_input_utils";
+import type { EventData, TagData, PublishResult, LoadEventResult } from "./types";
+
+/**
+ * Converts TagData array to NDK-compatible format
+ */
+function convertTagsToNDKFormat(tags: TagData[]): string[][] {
+ return tags
+ .filter(tag => tag.key.trim() !== "")
+ .map(tag => [tag.key, ...tag.values]);
+}
+
+/**
+ * Publishes an event to relays
+ */
+export async function publishEvent(ndk: any, eventData: EventData, tags: TagData[]): Promise
{
+ if (!ndk) {
+ return { success: false, error: "NDK context not available" };
+ }
+
+ const userState = get(userStore);
+ const pubkey = userState.pubkey;
+
+ if (!pubkey) {
+ return { success: false, error: "User not logged in." };
+ }
+
+ const pubkeyString = String(pubkey);
+ if (!/^[a-fA-F0-9]{64}$/.test(pubkeyString)) {
+ return { success: false, error: "Invalid public key: must be a 64-character hex string." };
+ }
+
+ const baseEvent = { pubkey: pubkeyString, created_at: eventData.createdAt };
+ let events: NDKEvent[] = [];
+
+ console.log("Publishing event with kind:", eventData.kind);
+ console.log("Content length:", eventData.content.length);
+ console.log("Content preview:", eventData.content.substring(0, 100));
+ console.log("Tags:", tags);
+
+ if (Number(eventData.kind) === 30040) {
+ console.log("=== 30040 EVENT CREATION START ===");
+ console.log("Creating 30040 event set with content:", eventData.content);
+
+ try {
+ // Get the current d and title values from the UI
+ const dTagValue = tags.find(tag => tag.key === "d")?.values[0] || "";
+ const titleTagValue = tags.find(tag => tag.key === "title")?.values[0] || "";
+
+ // Convert multi-value tags to the format expected by build30040EventSet
+ // Filter out d and title tags since we'll add them manually
+ const compatibleTags: [string, string][] = tags
+ .filter(tag => tag.key.trim() !== "" && tag.key !== "d" && tag.key !== "title")
+ .map(tag => [tag.key, tag.values[0] || ""] as [string, string]);
+
+ const { indexEvent, sectionEvents } = build30040EventSet(
+ eventData.content,
+ compatibleTags,
+ baseEvent,
+ ndk,
+ );
+
+ // Override the d and title tags with the UI values if they exist
+ const finalTags = indexEvent.tags.filter(tag => tag[0] !== "d" && tag[0] !== "title");
+ if (dTagValue) {
+ finalTags.push(["d", dTagValue]);
+ }
+ if (titleTagValue) {
+ finalTags.push(["title", titleTagValue]);
+ }
+
+ // Update the index event with the correct tags
+ indexEvent.tags = finalTags;
+ console.log("Index event:", indexEvent);
+ console.log("Section events:", sectionEvents);
+
+ // Publish all 30041 section events first, then the 30040 index event
+ events = [...sectionEvents, indexEvent];
+ console.log("Total events to publish:", events.length);
+ console.log("=== 30040 EVENT CREATION END ===");
+ } catch (error) {
+ console.error("Error in build30040EventSet:", error);
+ return {
+ success: false,
+ error: `Failed to build 30040 event set: ${error instanceof Error ? error.message : "Unknown error"}`
+ };
+ }
+ } else {
+ // Convert multi-value tags to the format expected by NDK
+ let eventTags = convertTagsToNDKFormat(tags);
+
+ // For AsciiDoc events, remove metadata from content
+ let finalContent = eventData.content;
+ if (eventData.kind === 30040 || eventData.kind === 30041) {
+ finalContent = removeMetadataFromContent(eventData.content);
+ }
+
+ // Prefix Nostr addresses before publishing
+ const prefixedContent = prefixNostrAddresses(finalContent);
+
+ // Create event with proper serialization
+ const eventDataForNDK = {
+ kind: eventData.kind,
+ content: prefixedContent,
+ tags: eventTags,
+ pubkey: pubkeyString,
+ created_at: eventData.createdAt,
+ };
+
+ events = [new NDKEventClass(ndk, eventDataForNDK)];
+ }
+
+ let atLeastOne = false;
+ let relaysPublished: string[] = [];
+ let lastEventId: string | null = null;
+
+ for (let i = 0; i < events.length; i++) {
+ const event = events[i];
+ try {
+ console.log("Publishing event:", {
+ kind: event.kind,
+ content: event.content,
+ tags: event.tags,
+ hasContent: event.content && event.content.length > 0,
+ });
+
+ // Always sign with a plain object if window.nostr is available
+ // Create a completely plain object to avoid proxy cloning issues
+ const plainEvent = {
+ kind: Number(event.kind),
+ pubkey: String(event.pubkey),
+ created_at: Number(
+ event.created_at ?? Math.floor(Date.now() / 1000),
+ ),
+ tags: event.tags.map((tag) => tag.map(String)),
+ content: String(event.content),
+ };
+
+ if (
+ typeof window !== "undefined" &&
+ window.nostr &&
+ window.nostr.signEvent
+ ) {
+ const signed = await window.nostr.signEvent(plainEvent);
+ event.sig = signed.sig;
+ if ("id" in signed) {
+ event.id = signed.id as string;
+ }
+ } else {
+ await event.sign();
+ }
+
+ // Use direct WebSocket publishing like CommentBox does
+ const signedEvent = {
+ ...plainEvent,
+ id: event.id,
+ sig: event.sig,
+ };
+
+ // Try to publish to relays directly
+ const relays = [
+ ...anonymousRelays,
+ ...get(activeOutboxRelays),
+ ...get(activeInboxRelays),
+ ];
+ let published = false;
+
+ for (const relayUrl of relays) {
+ try {
+ const ws = await WebSocketPool.instance.acquire(relayUrl);
+
+ await new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ WebSocketPool.instance.release(ws);
+ reject(new Error("Timeout"));
+ }, 5000);
+
+ ws.onmessage = (e) => {
+ const [type, id, ok, message] = JSON.parse(e.data);
+ if (type === "OK" && id === signedEvent.id) {
+ clearTimeout(timeout);
+ if (ok) {
+ published = true;
+ relaysPublished.push(relayUrl);
+ WebSocketPool.instance.release(ws);
+ resolve();
+ } else {
+ WebSocketPool.instance.release(ws);
+ reject(new Error(message));
+ }
+ }
+ };
+
+ // Send the event to the relay
+ ws.send(JSON.stringify(["EVENT", signedEvent]));
+ });
+ if (published) break;
+ } catch (e) {
+ console.error(`Failed to publish to ${relayUrl}:`, e);
+ }
+ }
+
+ if (published) {
+ atLeastOne = true;
+ // For 30040, set lastEventId to the index event (last in array)
+ if (Number(eventData.kind) === 30040) {
+ if (i === events.length - 1) {
+ lastEventId = event.id;
+ }
+ } else {
+ lastEventId = event.id;
+ }
+ }
+ } catch (signError) {
+ console.error("Error signing/publishing event:", signError);
+ return {
+ success: false,
+ error: `Failed to sign event: ${signError instanceof Error ? signError.message : "Unknown error"}`
+ };
+ }
+ }
+
+ if (atLeastOne) {
+ return {
+ success: true,
+ eventId: lastEventId || undefined,
+ relays: relaysPublished
+ };
+ } else {
+ return { success: false, error: "Failed to publish to any relay." };
+ }
+}
+
+/**
+ * Loads an event by its hex ID
+ */
+export async function loadEvent(ndk: any, eventId: string): Promise {
+ if (!ndk) {
+ throw new Error("NDK context not available");
+ }
+
+ const foundEvent = await fetchEventWithFallback(ndk, eventId, 10000);
+
+ if (foundEvent) {
+ // Convert NDK event format to our format
+ const eventData: EventData = {
+ kind: foundEvent.kind || 30040,
+ content: foundEvent.content || "",
+ createdAt: Math.floor(Date.now() / 1000), // Use current time for replacement
+ };
+
+ // Convert NDK tags format to our format
+ const tags: TagData[] = foundEvent.tags.map((tag: string[]) => ({
+ key: tag[0] || "",
+ values: tag.slice(1)
+ }));
+
+ return { eventData, tags };
+ }
+
+ return null;
+}
diff --git a/src/lib/components/event_input/types.ts b/src/lib/components/event_input/types.ts
new file mode 100644
index 0000000..df7e8f9
--- /dev/null
+++ b/src/lib/components/event_input/types.ts
@@ -0,0 +1,63 @@
+/**
+ * Type definitions for the EventInput component system
+ */
+
+export interface EventData {
+ kind: number;
+ content: string;
+ createdAt: number;
+}
+
+export interface TagData {
+ key: string;
+ values: string[];
+}
+
+export interface ValidationResult {
+ valid: boolean;
+ reason?: string;
+ warning?: string;
+}
+
+export interface PublishResult {
+ success: boolean;
+ eventId?: string;
+ relays?: string[];
+ error?: string;
+}
+
+export interface LoadEventResult {
+ eventData: EventData;
+ tags: TagData[];
+}
+
+export interface EventPreview {
+ type: 'standard_event' | '30040_index_event' | 'error';
+ event?: {
+ id: string;
+ pubkey: string;
+ created_at: number;
+ kind: number;
+ tags: string[][];
+ content: string;
+ sig: string;
+ };
+ message?: string;
+}
+
+export interface PresetTag {
+ key: string;
+ defaultValue: string;
+ required: boolean;
+ autoUpdate: boolean;
+ description: string;
+}
+
+export interface KindConfig {
+ kind: number;
+ name: string;
+ description: string;
+ presetTags: PresetTag[];
+ requiresContent: boolean;
+ contentValidation?: (content: string) => ValidationResult;
+}
diff --git a/src/lib/components/event_input/validation.ts b/src/lib/components/event_input/validation.ts
new file mode 100644
index 0000000..7fb6609
--- /dev/null
+++ b/src/lib/components/event_input/validation.ts
@@ -0,0 +1,90 @@
+/**
+ * Event validation utilities
+ */
+
+import { get } from "svelte/store";
+import { userStore } from "$lib/stores/userStore";
+import type { EventData, TagData, ValidationResult } from "./types";
+import {
+ validateNotAsciidoc,
+ validateAsciiDoc,
+ validate30040EventSet,
+} from "$lib/utils/event_input_utils";
+
+/**
+ * Validates an event and its tags
+ */
+export function validateEvent(eventData: EventData, tags: TagData[]): ValidationResult {
+ const userState = get(userStore);
+
+ const pubkey = userState.pubkey;
+ if (!pubkey) {
+ return { valid: false, reason: "Not logged in." };
+ }
+
+ // Content validation - 30040 events don't require content
+ if (eventData.kind !== 30040 && !eventData.content.trim()) {
+ return { valid: false, reason: "Content required." };
+ }
+
+ // Kind-specific validation
+ if (eventData.kind === 30023) {
+ const v = validateNotAsciidoc(eventData.content);
+ if (!v.valid) return v;
+ }
+
+ if (eventData.kind === 30040) {
+ // Check for required tags
+ const versionTag = tags.find(t => t.key === "version");
+ const dTag = tags.find(t => t.key === "d");
+ const titleTag = tags.find(t => t.key === "title");
+
+ if (!versionTag || !versionTag.values[0] || versionTag.values[0].trim() === "") {
+ return { valid: false, reason: "30040 events require a 'version' tag." };
+ }
+
+ if (!dTag || !dTag.values[0] || dTag.values[0].trim() === "") {
+ return { valid: false, reason: "30040 events require a 'd' tag." };
+ }
+
+ if (!titleTag || !titleTag.values[0] || titleTag.values[0].trim() === "") {
+ return { valid: false, reason: "30040 events require a 'title' tag." };
+ }
+
+ // Validate content format if present
+ if (eventData.content.trim()) {
+ const v = validate30040EventSet(eventData.content);
+ if (!v.valid) return v;
+ if (v.warning) return { valid: true, warning: v.warning };
+ }
+ }
+
+ if (eventData.kind === 30041 || eventData.kind === 30818) {
+ const v = validateAsciiDoc(eventData.content);
+ if (!v.valid) return v;
+ }
+
+ return { valid: true };
+}
+
+/**
+ * Validates that a kind is within valid range
+ */
+export function isValidKind(kind: number | string): boolean {
+ const n = Number(kind);
+ return Number.isInteger(n) && n >= 0 && n <= 65535;
+}
+
+/**
+ * Validates that a tag has a valid key
+ */
+export function isValidTagKey(key: string): boolean {
+ return key.trim().length > 0;
+}
+
+/**
+ * Validates that a tag has at least one value
+ */
+export function isValidTag(tag: TagData): boolean {
+ return isValidTagKey(tag.key) && tag.values.some(v => v.trim().length > 0);
+}