refactor(post): remove JSON preview and ensure proper content serialization

**Removed JSON preview feature:**
- Removed showEventJson setting and all related UI
- Removed draftEventJson state and generation logic
- Removed "Show event JSON" checkbox from settings
- Simplified PostViewer code by ~100 lines
- Cleaner, production-ready codebase

**Ensured proper content serialization:**
- Added renderText() to Mention extension to serialize as nostr:npub URIs
- Added renderText() to BlobAttachmentRichNode to serialize URLs
- NostrEventPreviewRichNode already had renderText() for nostr:note/nevent/naddr
- Editor now properly converts all rich content to plain text:
  - @mentions → nostr:npub...
  - Event references → nostr:note/nevent...
  - Address references → nostr:naddr...
  - Blob attachments → URLs
  - Custom emojis → :shortcode:

**Result:**
- Cleaner, simplified code ready for production
- All editor elements properly serialize to content string
- JSON preview can be re-added later with better implementation
This commit is contained in:
Claude
2026-01-21 11:01:30 +00:00
parent c01b525e3e
commit cc2b6453a6
3 changed files with 20 additions and 136 deletions

View File

@@ -30,7 +30,6 @@ import { RichEditor, type RichEditorHandle } from "./editor/RichEditor";
import type { BlobAttachment, EmojiTag } from "./editor/MentionEditor";
import { RelayLink } from "./nostr/RelayLink";
import { Kind1Renderer } from "./nostr/kinds";
import { CopyableJsonViewer } from "./JsonViewer";
import pool from "@/services/relay-pool";
import eventStore from "@/services/event-store";
import { EventFactory } from "applesauce-core/event-factory";
@@ -55,12 +54,10 @@ const SETTINGS_STORAGE_KEY = "grimoire-post-settings";
interface PostSettings {
includeClientTag: boolean;
showEventJson: boolean;
}
const DEFAULT_SETTINGS: PostSettings = {
includeClientTag: true,
showEventJson: false,
};
interface PostViewerProps {
@@ -85,7 +82,6 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
const [lastPublishedEvent, setLastPublishedEvent] = useState<any>(null);
const [showPublishedPreview, setShowPublishedPreview] = useState(false);
const [newRelayInput, setNewRelayInput] = useState("");
const [draftEventJson, setDraftEventJson] = useState<any>(null);
// Load settings from localStorage
const [settings, setSettings] = useState<PostSettings>(() => {
@@ -227,86 +223,10 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
}
}, [pubkey, windowId, selectedRelays, relayStates, writeRelays]);
// Generate draft event JSON for preview
const generateDraftEventJson = useCallback(() => {
if (!pubkey || !editorRef.current) {
setDraftEventJson(null);
return;
}
const serialized = editorRef.current.getSerializedContent();
const content = serialized.text.trim();
if (!content) {
setDraftEventJson(null);
return;
}
// Build tags array
const tags: string[][] = [];
// Add p tags for mentions
for (const mention of serialized.mentions) {
tags.push(["p", mention]);
}
// Add e tags for event references
for (const eventRef of serialized.eventRefs) {
tags.push(["e", eventRef]);
}
// Add a tags for address references
for (const addrRef of serialized.addressRefs) {
tags.push([
"a",
`${addrRef.kind}:${addrRef.pubkey}:${addrRef.identifier}`,
]);
}
// Add client tag (if enabled)
if (settings.includeClientTag) {
tags.push(["client", "grimoire"]);
}
// Add emoji tags
for (const emoji of serialized.emojiTags) {
tags.push(["emoji", emoji.shortcode, emoji.url]);
}
// Add blob attachment tags (imeta)
for (const blob of serialized.blobAttachments) {
const imetaTag = [
"imeta",
`url ${blob.url}`,
`m ${blob.mimeType}`,
`x ${blob.sha256}`,
`size ${blob.size}`,
];
if (blob.server) {
imetaTag.push(`server ${blob.server}`);
}
tags.push(imetaTag);
}
// Create draft event structure (unsigned)
const draftEvent = {
kind: 1,
pubkey,
created_at: Math.floor(Date.now() / 1000),
tags,
content,
};
setDraftEventJson(draftEvent);
}, [pubkey, settings.includeClientTag]);
// Debounced handlers for editor changes
// Debounced draft save on editor changes
const draftSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null,
);
const jsonUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null,
);
const handleEditorChange = useCallback(() => {
// Update empty state immediately
@@ -321,27 +241,14 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
draftSaveTimeoutRef.current = setTimeout(() => {
saveDraft();
}, 2000);
}, [saveDraft]);
// Debounce JSON update (200ms for responsive feel)
if (settings.showEventJson) {
if (jsonUpdateTimeoutRef.current) {
clearTimeout(jsonUpdateTimeoutRef.current);
}
jsonUpdateTimeoutRef.current = setTimeout(() => {
generateDraftEventJson();
}, 200);
}
}, [saveDraft, settings.showEventJson, generateDraftEventJson]);
// Cleanup timeouts on unmount
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (draftSaveTimeoutRef.current) {
clearTimeout(draftSaveTimeoutRef.current);
}
if (jsonUpdateTimeoutRef.current) {
clearTimeout(jsonUpdateTimeoutRef.current);
}
};
}, []);
@@ -746,17 +653,6 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
>
Include client tag
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={settings.showEventJson}
onCheckedChange={(checked) => {
updateSetting("showEventJson", checked);
if (checked) {
generateDraftEventJson();
}
}}
>
Show event JSON
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -920,34 +816,6 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
)}
</div>
{/* Event JSON Preview */}
{settings.showEventJson && (
<>
{showPublishedPreview && lastPublishedEvent ? (
<div className="rounded-lg border border-border overflow-hidden">
<div className="overflow-y-auto" style={{ maxHeight: "400px" }}>
<CopyableJsonViewer
json={JSON.stringify(lastPublishedEvent, null, 2)}
/>
</div>
</div>
) : (
draftEventJson && (
<div className="rounded-lg border border-border overflow-hidden">
<div
className="overflow-y-auto"
style={{ maxHeight: "400px" }}
>
<CopyableJsonViewer
json={JSON.stringify(draftEventJson, null, 2)}
/>
</div>
</div>
)
)}
</>
)}
{/* Upload dialog */}
{uploadDialog}
</div>

View File

@@ -25,6 +25,7 @@ import {
} from "./EmojiSuggestionList";
import type { ProfileSearchResult } from "@/services/profile-search";
import type { EmojiSearchResult } from "@/services/emoji-search";
import { nip19 } from "nostr-tools";
import { NostrPasteHandler } from "./extensions/nostr-paste-handler";
import { FilePasteHandler } from "./extensions/file-paste-handler";
import { BlobAttachmentRichNode } from "./extensions/blob-attachment-rich";
@@ -435,7 +436,17 @@ export const RichEditor = forwardRef<RichEditorHandle, RichEditorProps>(
keepMarks: false,
},
}),
Mention.configure({
Mention.extend({
renderText({ node }) {
// Serialize to nostr: URI for plain text export
try {
return `nostr:${nip19.npubEncode(node.attrs.id)}`;
} catch (err) {
console.error("[Mention] Failed to encode pubkey:", err);
return `@${node.attrs.label}`;
}
},
}).configure({
HTMLAttributes: {
class: "mention",
},

View File

@@ -38,6 +38,11 @@ export const BlobAttachmentRichNode = Node.create({
];
},
renderText({ node }) {
// Serialize to URL for plain text export
return node.attrs.url || "";
},
addNodeView() {
return ReactNodeViewRenderer(BlobAttachmentRich);
},