diff --git a/README.md b/README.md index 8b64c1f..4e6990c 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,11 @@ author: "Author Name" summary: "Article summary" image: "https://example.com/image.jpg" topics: "bitcoin, nostr" -published_at: "1234567890" --- ``` +**Note**: The `published_at` tag is automatically generated with the current UNIX timestamp during event creation for all replaceable event kinds. It should not be included in metadata and will be ignored if present. + ### AsciiDoc Files (`.adoc`) Supported event kinds: **30040**, **30041**, **30818** @@ -175,8 +176,8 @@ All tag values are normalized per NKBIP-08 spec (lowercase, hyphens, numbers onl ### Stand-alone vs Nested 30041 -- **Stand-alone 30041**: Uses NKBIP-01 tags (d, title, image, summary, published_at, topics) -- **Nested 30041** (under 30040): Uses NKBIP-08 tags +- **Stand-alone 30041**: Uses NKBIP-01 tags (d, title, image, summary, topics) plus automatically-generated `published_at` +- **Nested 30041** (under 30040): Uses NKBIP-08 tags plus automatically-generated `published_at` - **Two-level structure** (book + chapters): 30041 events are chapters (c tag from chapter title, no s tag) - **Three-level structure** (book + chapters + sections): 30041 events are sections (c tag from parent chapter, s tag from section title) - All nested 30041 events inherit C tag (collection_id) and v tag (version_tag) from root 30040 @@ -186,6 +187,8 @@ All tag values are normalized per NKBIP-08 spec (lowercase, hyphens, numbers onl All predefined metadata fields are shown in frontmatter/attributes with placeholder descriptions. Remove or update placeholders you don't need. Placeholder values are automatically skipped when creating events. +**Important**: The `published_at` tag is automatically generated with the current UNIX timestamp during event creation for all replaceable event kinds (all event kinds supported by this plugin). Do not include `published_at` in your metadata - it will be automatically added and any existing `published_at` values in metadata will be ignored. + ### Common Fields - `kind` - Event kind (required) @@ -198,7 +201,7 @@ All predefined metadata fields are shown in frontmatter/attributes with placehol ### Kind-Specific Fields **30023 (Article)**: -- `published_at` - Unix timestamp +- No additional fields beyond common ones **30040 (Publication Index)**: - `type` - Publication type (book, illustrated, magazine, documentation, academic, blog) @@ -211,7 +214,7 @@ All predefined metadata fields are shown in frontmatter/attributes with placehol - `version_tag` - NKBIP-08 version identifier (v tag) - If set in root 30040, inherited by all events in the hierarchy **30041 (Publication Content)**: -- **Stand-alone**: Same as 30023 (image, summary, published_at, topics) +- **Stand-alone**: Same as 30023 (image, summary, topics) - **Nested** (under 30040): NKBIP-08 tags - `collection_id` - Inherited from root 30040 (C tag) - `title_id` - From root 30040 book title (T tag) diff --git a/src/main.ts b/src/main.ts index 13f874c..7ad9e98 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { Plugin, TFile, Notice } from "obsidian"; +import { Plugin, TFile, Notice, Menu } from "obsidian"; import { ScriptoriumSettings, EventKind, DEFAULT_SETTINGS } from "./types"; import { ScriptoriumSettingTab } from "./ui/settingsTab"; import { NewDocumentModal } from "./ui/newDocumentModal"; @@ -65,9 +65,31 @@ export default class ScriptoriumPlugin extends Plugin { callback: () => this.handleNewDocument(), }); - // Add ribbon icon for creating new documents - this.addRibbonIcon("file-plus", "New Nostr Document", () => { - this.handleNewDocument(); + // Add ribbon icon with menu for Nostr actions + const ribbonIcon = this.addRibbonIcon("zap", "Nostr", () => { + // Create and show menu + const menu = new Menu(); + menu.addItem((item) => { + item.setTitle("Write Nostr note") + .setIcon("file-plus") + .onClick(() => this.handleNewDocument()); + }); + menu.addItem((item) => { + item.setTitle("Create Nostr events") + .setIcon("file-check") + .onClick(() => this.handleCreateEvents()); + }); + menu.addItem((item) => { + item.setTitle("Publish events to relays") + .setIcon("upload") + .onClick(() => this.handlePublishEvents()); + }); + + // Show menu at the ribbon icon position + if (ribbonIcon) { + const rect = ribbonIcon.getBoundingClientRect(); + menu.showAtPosition({ x: rect.left, y: rect.bottom + 5 }); + } }); // Status bar @@ -163,17 +185,15 @@ export default class ScriptoriumPlugin extends Plugin { } // Create default content based on kind + // Note: This content will be replaced by writeMetadata() which formats + // the file properly with metadata. We just need minimal content here. let content = ""; - if (kind === 30040) { - // AsciiDoc document header for 30040 + if (kind === 30040 || kind === 30041 || kind === 30818) { + // AsciiDoc files - minimal content, writeMetadata will format properly content = `= ${title}\n\n`; - } else if (kind === 30023 || kind === 30817 || kind === 30818) { - // Add title as heading for other kinds that require title - if (kind === 30817 || kind === 30818) { - content = `# ${title}\n\n`; - } else { - content = `# ${title}\n\n`; - } + } else if (kind === 30023 || kind === 30817) { + // Markdown files - add title as heading + content = `# ${title}\n\n`; } else if (kind === 1 || kind === 11) { // For kind 1 and 11, add a simple placeholder content = `\n`; @@ -207,8 +227,8 @@ export default class ScriptoriumPlugin extends Plugin { // Create metadata with title preset from the filename const metadata = createDefaultMetadata(kind); - // Always set title if provided (even for kind 1 where it's optional) - if (title && title.trim()) { + // Set title if provided (skip for kind 1 which doesn't have title) + if (kind !== 1 && title && title.trim()) { (metadata as any).title = title.trim(); } diff --git a/src/metadataManager.ts b/src/metadataManager.ts index a6931ad..5b16ea1 100644 --- a/src/metadataManager.ts +++ b/src/metadataManager.ts @@ -14,9 +14,6 @@ interface TagDefinition { const TAG_DEFINITIONS: Record = { 1: [ - { key: "title", description: "Note title (optional)", required: false }, - { key: "author", description: "Author name", required: false }, - { key: "summary", description: "Brief summary", required: false }, { key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false }, ], 11: [ @@ -30,7 +27,6 @@ const TAG_DEFINITIONS: Record = { { key: "author", description: "Author name", required: false }, { key: "summary", description: "Article summary", required: false }, { key: "image", description: "Image URL", required: false }, - { key: "published_at", description: "Unix timestamp of first publication", required: false }, { key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false }, ], 30040: [ @@ -52,7 +48,6 @@ const TAG_DEFINITIONS: Record = { { key: "title", description: "Chapter/section title (required)", required: true }, { key: "image", description: "Image URL", required: false }, { key: "summary", description: "Article summary", required: false }, - { key: "published_at", description: "Unix timestamp of first publication", required: false }, { key: "topics", description: "Comma-separated topics (e.g., 'bitcoin, nostr')", required: false }, // Note: NKBIP-08 tags (collection_id, title_id, chapter_id, section_id, version_tag) // are only used when 30041 is nested under 30040, not for stand-alone 30041 events @@ -214,14 +209,21 @@ function parseAsciiDocAttributes(content: string): { metadata: Record, kind: EventKind): Record { const filtered: Record = {}; @@ -242,6 +245,11 @@ function filterPlaceholders(metadata: Record, kind: EventKind): Rec continue; } + // Remove published_at - it's automatically generated during event creation + if (key === "published_at") { + continue; + } + // Skip placeholder values if (isPlaceholder(value, key, kind)) { continue; @@ -356,7 +364,21 @@ export async function writeMetadata( if (isMarkdownFile(file)) { const { body } = parseMarkdownFrontmatter(currentContent); const frontmatter = formatMarkdownFrontmatter(metadata); - const newContent = frontmatter ? `---\n${frontmatter}---\n${body}` : body; + + // If body is empty or only whitespace, add default content + const trimmedBody = body.trim(); + let finalBody = body; + if (!trimmedBody || trimmedBody.length === 0) { + // For kind 1, just add placeholder text (no header) + if (metadata.kind === 1) { + finalBody = `place your content here\n\n---\n\n**How to use this app:**\n1. Edit your content above\n2. Click the Nostr menu button (lightning bolt icon ⚡) in the left sidebar\n3. Select "Create Nostr events" to create and sign events\n4. Select "Publish events to relays" to publish to relays`; + } else { + // For other kinds, add level-one header (#) with default text + finalBody = `# This is the first header in this document\n\nplace your content here\n\n---\n\n**How to use this app:**\n1. Edit your content above\n2. Click the Nostr menu button (lightning bolt icon ⚡) in the left sidebar\n3. Select "Create Nostr events" to create and sign events\n4. Select "Publish events to relays" to publish to relays`; + } + } + + const newContent = frontmatter ? `---\n${frontmatter}---\n${finalBody}` : finalBody; await app.vault.modify(file, newContent); } else if (isAsciiDocFile(file)) { // For AsciiDoc, we need to preserve the title if it exists in the body @@ -391,13 +413,13 @@ export async function writeMetadata( const actualBody = bodyLines.slice(actualBodyStart).join("\n"); // Format new content with title + attributes + body + // Note: No blank line between document header and attributes (AsciiDoc spec) const lines: string[] = []; if (titleLine) { lines.push(titleLine); } else if (metadata.title) { lines.push(`= ${metadata.title}`); } - lines.push(""); // Add all predefined attributes with placeholders or actual values const kind = metadata.kind; @@ -450,9 +472,30 @@ export async function writeMetadata( } } + // Add blank line after attributes (before body content) lines.push(""); - const newContent = lines.join("\n") + actualBody; + // If body is empty or only whitespace, add default content with level-one header + const trimmedBody = actualBody.trim(); + if (!trimmedBody || trimmedBody.length === 0) { + // Add level-one header (==) with default text (title is already in doc header) + lines.push(`== This is the first header in this document`); + lines.push(""); + lines.push("place your content here"); + lines.push(""); + lines.push("---"); + lines.push(""); + lines.push("**How to use this app:**"); + lines.push("1. Edit your content above"); + lines.push("2. Click the Nostr menu button (lightning bolt icon ⚡) in the left sidebar"); + lines.push("3. Select \"Create Nostr events\" to create and sign events"); + lines.push("4. Select \"Publish events to relays\" to publish to relays"); + } else { + // Use existing body content + lines.push(actualBody); + } + + const newContent = lines.join("\n"); await app.vault.modify(file, newContent); } } catch (error) { diff --git a/src/nostr/eventBuilder.ts b/src/nostr/eventBuilder.ts index 70a1c92..1f1d6c5 100644 --- a/src/nostr/eventBuilder.ts +++ b/src/nostr/eventBuilder.ts @@ -67,6 +67,7 @@ export function getNpubFromPrivkey(privkey: string): string { /** * Build tags array from metadata + * Automatically adds published_at tag with current UNIX timestamp for all replaceable event kinds */ export function buildTagsFromMetadata( metadata: EventMetadata, @@ -74,6 +75,11 @@ export function buildTagsFromMetadata( childEvents?: Array<{ kind: number; dTag: string; eventId?: string }> ): string[][] { const tags: string[][] = []; + + // All event kinds in this plugin are replaceable (0-9999 range) + // Add published_at tag automatically with current UNIX timestamp + const publishedAt = Math.floor(Date.now() / 1000).toString(); + tags.push(["published_at", publishedAt]); switch (metadata.kind) { case 1: @@ -108,7 +114,6 @@ export function buildTagsFromMetadata( if (metadata.title) tags.push(["title", metadata.title]); if (metadata.image) tags.push(["image", metadata.image]); if (metadata.summary) tags.push(["summary", metadata.summary]); - if (metadata.published_at) tags.push(["published_at", metadata.published_at]); if (metadata.topics) { metadata.topics.forEach((topic) => tags.push(["t", topic])); } @@ -176,7 +181,6 @@ export function buildTagsFromMetadata( // Stand-alone 30041 can have same tags as 30023 if (meta30041.image) tags.push(["image", meta30041.image]); if (meta30041.summary) tags.push(["summary", meta30041.summary]); - if (meta30041.published_at) tags.push(["published_at", meta30041.published_at]); if (meta30041.topics) { meta30041.topics.forEach((topic) => tags.push(["t", topic])); } diff --git a/src/types.ts b/src/types.ts index 6e1a203..6706fe3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,7 +55,6 @@ export interface Kind30023Metadata extends BaseMetadata { kind: 30023; title: string; // mandatory image?: string; - published_at?: string; topics?: string[]; // t tags } @@ -92,7 +91,6 @@ export interface Kind30041Metadata extends BaseMetadata { title: string; // mandatory // Stand-alone 30041 can have same tags as 30023 image?: string; - published_at?: string; topics?: string[]; // t tags // NKBIP-08 tags (only for nested 30041 under 30040) collection_id?: string; // C tag (inherited from root 30040) diff --git a/src/ui/metadataModal.ts b/src/ui/metadataModal.ts index b04a175..23c4745 100644 --- a/src/ui/metadataModal.ts +++ b/src/ui/metadataModal.ts @@ -34,28 +34,31 @@ export class MetadataModal extends Modal { }); } - new Setting(contentEl) - .setName("Author") - .setDesc("Author name") - .addText((text) => { - text.setValue(this.metadata.author || "") - .setPlaceholder("Enter author") - .onChange((value) => { - this.metadata.author = value; - }); - }); + // Author and Summary are not shown for kind 1 + if (this.metadata.kind !== 1) { + new Setting(contentEl) + .setName("Author") + .setDesc("Author name") + .addText((text) => { + text.setValue(this.metadata.author || "") + .setPlaceholder("Enter author") + .onChange((value) => { + this.metadata.author = value; + }); + }); - new Setting(contentEl) - .setName("Summary") - .setDesc("Brief summary or description") - .addTextArea((text) => { - text.setValue(this.metadata.summary || "") - .setPlaceholder("Enter summary") - .onChange((value) => { - this.metadata.summary = value; - }); - text.inputEl.rows = 3; - }); + new Setting(contentEl) + .setName("Summary") + .setDesc("Brief summary or description") + .addTextArea((text) => { + text.setValue(this.metadata.summary || "") + .setPlaceholder("Enter summary") + .onChange((value) => { + this.metadata.summary = value; + }); + text.inputEl.rows = 3; + }); + } // Kind-specific fields this.renderKindSpecificFields(contentEl); @@ -119,17 +122,6 @@ export class MetadataModal extends Modal { }); }); - new Setting(container) - .setName("Published At") - .setDesc("Unix timestamp of first publication") - .addText((text) => { - text.setValue(meta.published_at || "") - .setPlaceholder("Unix timestamp") - .onChange((value) => { - meta.published_at = value; - }); - }); - new Setting(container) .setName("Topics") .setDesc("Comma-separated topics (t tags)")