From c9e1fccb4f444a630bbacc4dbf78ff2ad9e6cae5 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 20 Jan 2026 21:36:19 +0000
Subject: [PATCH] feat(post): add draft persistence and improve relay UI
Draft persistence:
- Save draft content to localStorage every 2 seconds
- Load draft on component mount (per-user with pubkey)
- Clear draft after successful publish
- Prevent data loss on tab change or page reload
UI improvements:
- Reset button now icon-only (RotateCcw) with tooltip
- Remove borders from relay list items
- Use RelayLink component for relay rendering
- Show relay icons and secure/insecure indicators
- Cleaner, more compact relay list appearance
- Increased spacing between relay items (space-y-1)
Storage key format: grimoire-post-draft-{pubkey}
---
src/components/PostViewer.tsx | 91 +++++++++++++++++++++++++++++++----
1 file changed, 82 insertions(+), 9 deletions(-)
diff --git a/src/components/PostViewer.tsx b/src/components/PostViewer.tsx
index ded238c..285a9bd 100644
--- a/src/components/PostViewer.tsx
+++ b/src/components/PostViewer.tsx
@@ -1,5 +1,5 @@
import { useState, useRef, useCallback, useMemo, useEffect } from "react";
-import { Paperclip, Send, Loader2, Check, X, RefreshCw } from "lucide-react";
+import { Paperclip, Send, Loader2, Check, X, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { Button } from "./ui/button";
import { Checkbox } from "./ui/checkbox";
@@ -10,6 +10,7 @@ import { useEmojiSearch } from "@/hooks/useEmojiSearch";
import { useBlossomUpload } from "@/hooks/useBlossomUpload";
import { RichEditor, type RichEditorHandle } from "./editor/RichEditor";
import type { BlobAttachment, EmojiTag } from "./editor/MentionEditor";
+import { RelayLink } from "./nostr/RelayLink";
import pool from "@/services/relay-pool";
import eventStore from "@/services/event-store";
import { EventFactory } from "applesauce-core/event-factory";
@@ -24,6 +25,9 @@ interface RelayPublishState {
error?: string;
}
+// Draft persistence key
+const DRAFT_STORAGE_KEY = "grimoire-post-draft";
+
export function PostViewer() {
const { pubkey, canSign, signer } = useAccount();
const { searchProfiles } = useProfileSearch();
@@ -62,6 +66,63 @@ export function PostViewer() {
}
}, [writeRelays, updateRelayStates]);
+ // Load draft from localStorage on mount
+ useEffect(() => {
+ if (!pubkey) return;
+
+ const draftKey = `${DRAFT_STORAGE_KEY}-${pubkey}`;
+ const savedDraft = localStorage.getItem(draftKey);
+
+ if (savedDraft) {
+ try {
+ const draft = JSON.parse(savedDraft);
+ // We'll set content via editor commands after editor is ready
+ // Store in ref for initial load
+ if (editorRef.current && draft.content) {
+ // Use setTimeout to ensure editor is fully mounted
+ setTimeout(() => {
+ if (editorRef.current) {
+ editorRef.current.insertText(draft.content);
+ }
+ }, 100);
+ }
+ } catch (err) {
+ console.error("Failed to load draft:", err);
+ }
+ }
+ }, [pubkey]);
+
+ // Save draft to localStorage on content change
+ const saveDraft = useCallback(() => {
+ if (!pubkey || !editorRef.current) return;
+
+ const content = editorRef.current.getContent();
+ if (!content.trim()) {
+ // Clear draft if empty
+ const draftKey = `${DRAFT_STORAGE_KEY}-${pubkey}`;
+ localStorage.removeItem(draftKey);
+ return;
+ }
+
+ const draftKey = `${DRAFT_STORAGE_KEY}-${pubkey}`;
+ const draft = {
+ content,
+ timestamp: Date.now(),
+ };
+
+ try {
+ localStorage.setItem(draftKey, JSON.stringify(draft));
+ } catch (err) {
+ console.error("Failed to save draft:", err);
+ }
+ }, [pubkey]);
+
+ // Debounced draft save (save every 2 seconds of inactivity)
+ useEffect(() => {
+ const timer = setInterval(saveDraft, 2000);
+ return () => clearInterval(timer);
+ }, [saveDraft]);
+
// Blossom upload for attachments
const { open: openUpload, dialog: uploadDialog } = useBlossomUpload({
accept: "image/*,video/*,audio/*",
@@ -201,6 +262,12 @@ export function PostViewer() {
// Clear editor on success
editorRef.current?.clear();
+ // Clear draft from localStorage
+ if (pubkey) {
+ const draftKey = `${DRAFT_STORAGE_KEY}-${pubkey}`;
+ localStorage.removeItem(draftKey);
+ }
+
toast.success(
`Published to ${selected.length} relay${selected.length > 1 ? "s" : ""}`,
);
@@ -305,13 +372,13 @@ export function PostViewer() {
{writeRelays.length > 0 && (
)}
@@ -322,11 +389,11 @@ export function PostViewer() {
settings.
) : (
-
+
{relayStates.map((relay) => (