From eee97cea27634969cdfd80a9b4cff789c4cae13e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 19:38:16 +0000 Subject: [PATCH] refactor(editor): simplify nostr preview display and reuse mentions Major simplification of the nostr bech32 preview display: Profile handling (npub/nprofile): - Now creates regular @mention nodes instead of custom preview nodes - Reuses existing mention infrastructure, styling, and UserName component - Displays as "@username" with existing mention chip styling - Serializes to nostr:npub1... on submit (same as manual @mentions) Event/Address display (note/nevent/naddr): - Removed emoji icons for cleaner, more minimal appearance - Display format: "event abc12345" for note/nevent - Display format: "address article-slug" for naddr (shows d identifier) - Falls back to short pubkey if naddr has no d identifier - Simple text-only chips with type + identifier Benefits: - Less visual noise (no emojis) - Consistent mention styling for all profiles - Profile mentions can now be clicked/hovered like manual mentions - Smaller code footprint (removed complex icon mapping logic) - Better UX: profiles look and behave like regular mentions Technical changes: - Paste handler creates mention nodes for npub/nprofile - NostrEventPreview only handles note/nevent/naddr now - Removed npub/nprofile from serialization (handled by mention serializer) - Updated type definitions to reflect reduced scope --- src/components/editor/MentionEditor.tsx | 101 ++++++++---------------- 1 file changed, 31 insertions(+), 70 deletions(-) diff --git a/src/components/editor/MentionEditor.tsx b/src/components/editor/MentionEditor.tsx index ebd96b7..94db0df 100644 --- a/src/components/editor/MentionEditor.tsx +++ b/src/components/editor/MentionEditor.tsx @@ -286,7 +286,7 @@ const NostrEventPreview = Node.create({ addAttributes() { return { - type: { default: null }, // 'npub' | 'note' | 'nevent' | 'naddr' | 'nprofile' + type: { default: null }, // 'note' | 'nevent' | 'naddr' data: { default: null }, // Decoded bech32 data (varies by type) }; }, @@ -310,16 +310,12 @@ const NostrEventPreview = Node.create({ // Serialize back to nostr: URI for plain text export const { type, data } = node.attrs; try { - if (type === "npub") { - return `nostr:${nip19.npubEncode(data)}`; - } else if (type === "note") { + if (type === "note") { return `nostr:${nip19.noteEncode(data)}`; } else if (type === "nevent") { return `nostr:${nip19.neventEncode(data)}`; } else if (type === "naddr") { return `nostr:${nip19.naddrEncode(data)}`; - } else if (type === "nprofile") { - return `nostr:${nip19.nprofileEncode(data)}`; } } catch (err) { console.error("[NostrEventPreview] Failed to encode:", err); @@ -337,60 +333,27 @@ const NostrEventPreview = Node.create({ "nostr-event-preview inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-primary/10 border border-primary/30 text-xs align-middle"; dom.contentEditable = "false"; - // Helper to get kind icon - const getKindIcon = (kind?: number): string => { - if (!kind) return "📝"; - if (kind === 0) return "👤"; // Profile - if (kind === 1) return "📝"; // Note - if (kind === 3) return "👥"; // Contacts - if (kind === 6) return "🔁"; // Repost - if (kind === 7) return "❤️"; // Reaction - if (kind === 9735) return "⚡"; // Zap - if (kind === 30023) return "📄"; // Long-form - if (kind === 30311) return "🎙️"; // Live event - if (kind === 1063) return "📦"; // File metadata - if (kind >= 30000 && kind < 40000) return "📌"; // Addressable - if (kind >= 10000 && kind < 20000) return "🔄"; // Replaceable - return "📝"; // Default - }; - - // Icon based on type and kind - const icon = document.createElement("span"); - icon.className = "text-primary flex-shrink-0"; + // Type label + const typeLabel = document.createElement("span"); + typeLabel.className = "text-primary font-medium"; // Content label - const label = document.createElement("span"); - label.className = "text-muted-foreground truncate max-w-[120px]"; + const contentLabel = document.createElement("span"); + contentLabel.className = "text-muted-foreground truncate max-w-[140px]"; - if (type === "npub") { - // npub: 👤 pubkey - icon.textContent = "👤"; - label.textContent = data.slice(0, 8); - } else if (type === "nprofile") { - // nprofile: 👤 pubkey - icon.textContent = "👤"; - label.textContent = data.pubkey.slice(0, 8); - } else if (type === "note") { - // note: 📝 event-id - icon.textContent = "📝"; - label.textContent = data.slice(0, 8); - } else if (type === "nevent") { - // nevent: kind-icon event-id (or author if available) - icon.textContent = getKindIcon(data.kind); - // nevent can optionally include author - if (data.author) { - label.textContent = data.author.slice(0, 8); - } else { - label.textContent = data.id.slice(0, 8); - } + if (type === "note" || type === "nevent") { + // event + short ID + typeLabel.textContent = "event"; + contentLabel.textContent = + type === "note" ? data.slice(0, 8) : data.id.slice(0, 8); } else if (type === "naddr") { - // naddr: kind-icon author - icon.textContent = getKindIcon(data.kind); - label.textContent = data.pubkey.slice(0, 8); + // address + d identifier (or short pubkey if no identifier) + typeLabel.textContent = "address"; + contentLabel.textContent = data.identifier || data.pubkey.slice(0, 8); } - dom.appendChild(icon); - dom.appendChild(label); + dom.appendChild(typeLabel); + dom.appendChild(contentLabel); return { dom }; }; @@ -439,12 +402,21 @@ const NostrPasteHandler = Extension.create({ try { const decoded = nip19.decode(bech32); - // Create preview node based on type + // For npub/nprofile, create regular mention nodes (reuse existing infrastructure) if (decoded.type === "npub") { + const pubkey = decoded.data as string; nodes.push( - view.state.schema.nodes.nostrEventPreview.create({ - type: "npub", - data: decoded.data, + view.state.schema.nodes.mention.create({ + id: pubkey, + label: pubkey.slice(0, 8), // Will be updated with profile name if available + }), + ); + } else if (decoded.type === "nprofile") { + const pubkey = (decoded.data as any).pubkey; + nodes.push( + view.state.schema.nodes.mention.create({ + id: pubkey, + label: pubkey.slice(0, 8), // Will be updated with profile name if available }), ); } else if (decoded.type === "note") { @@ -468,13 +440,6 @@ const NostrPasteHandler = Extension.create({ data: decoded.data, }), ); - } else if (decoded.type === "nprofile") { - nodes.push( - view.state.schema.nodes.nostrEventPreview.create({ - type: "nprofile", - data: decoded.data, - }), - ); } // Add space after preview node @@ -882,16 +847,12 @@ export const MentionEditor = forwardRef< // Nostr event preview - serialize back to nostr: URI const { type, data } = child.attrs; try { - if (type === "npub") { - text += `nostr:${nip19.npubEncode(data)}`; - } else if (type === "note") { + if (type === "note") { text += `nostr:${nip19.noteEncode(data)}`; } else if (type === "nevent") { text += `nostr:${nip19.neventEncode(data)}`; } else if (type === "naddr") { text += `nostr:${nip19.naddrEncode(data)}`; - } else if (type === "nprofile") { - text += `nostr:${nip19.nprofileEncode(data)}`; } } catch (err) { console.error(