From 78b1cbe0c5325cecd4b6f35d436dd61146e87cd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 16:08:54 +0000 Subject: [PATCH] fix: prevent TipTap editor crash when view is not ready The POST command would sometimes crash with "editor view is not available" because code was accessing editor.view.dom before the editor was fully mounted. This fix: - Adds defensive checks for editor.view?.dom in RichEditor's useEffect that attaches keyboard listeners - Makes setContent method check editor view is ready before setting content - Fixes PostViewer draft loading to use retry logic instead of fixed timeout - Removes relayStates from dependency array to prevent effect re-runs - Adds ref to track if draft was already loaded --- src/components/PostViewer.tsx | 46 +++++++++++++++++----------- src/components/editor/RichEditor.tsx | 9 ++++-- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/components/PostViewer.tsx b/src/components/PostViewer.tsx index 6779888..0d12741 100644 --- a/src/components/PostViewer.tsx +++ b/src/components/PostViewer.tsx @@ -142,9 +142,12 @@ export function PostViewer({ windowId }: PostViewerProps = {}) { } }, [writeRelays, updateRelayStates]); + // Track if draft has been loaded to prevent re-runs + const draftLoadedRef = useRef(false); + // Load draft from localStorage on mount useEffect(() => { - if (!pubkey) return; + if (!pubkey || draftLoadedRef.current) return; const draftKey = windowId ? `${DRAFT_STORAGE_KEY}-${pubkey}-${windowId}` @@ -154,15 +157,20 @@ export function PostViewer({ windowId }: PostViewerProps = {}) { if (savedDraft) { try { const draft = JSON.parse(savedDraft); + draftLoadedRef.current = true; - // Restore editor content - if (editorRef.current && draft.editorState) { - // Use setTimeout to ensure editor is fully mounted - setTimeout(() => { - if (editorRef.current && draft.editorState) { + // Restore editor content with retry logic for editor readiness + if (draft.editorState) { + const trySetContent = (attempts = 0) => { + if (editorRef.current) { editorRef.current.setContent(draft.editorState); + } else if (attempts < 10) { + // Retry up to 10 times with 50ms intervals (500ms total) + setTimeout(() => trySetContent(attempts + 1), 50); } - }, 100); + }; + // Start trying after a short delay to let editor mount + setTimeout(() => trySetContent(), 50); } // Restore selected relays @@ -172,22 +180,24 @@ export function PostViewer({ windowId }: PostViewerProps = {}) { // Restore added relays (relays not in writeRelays) if (draft.addedRelays && Array.isArray(draft.addedRelays)) { - const currentRelayUrls = new Set(relayStates.map((r) => r.url)); - const newRelays = draft.addedRelays - .filter((url: string) => !currentRelayUrls.has(url)) - .map((url: string) => ({ - url, - status: "pending" as RelayStatus, - })); - if (newRelays.length > 0) { - setRelayStates((prev) => [...prev, ...newRelays]); - } + setRelayStates((prev) => { + const currentRelayUrls = new Set(prev.map((r) => r.url)); + const newRelays = draft.addedRelays + .filter((url: string) => !currentRelayUrls.has(url)) + .map((url: string) => ({ + url, + status: "pending" as RelayStatus, + })); + return newRelays.length > 0 ? [...prev, ...newRelays] : prev; + }); } } catch (err) { console.error("Failed to load draft:", err); } + } else { + draftLoadedRef.current = true; } - }, [pubkey, windowId, relayStates]); + }, [pubkey, windowId]); // Save draft to localStorage on content change const saveDraft = useCallback(() => { diff --git a/src/components/editor/RichEditor.tsx b/src/components/editor/RichEditor.tsx index e315d2d..947c911 100644 --- a/src/components/editor/RichEditor.tsx +++ b/src/components/editor/RichEditor.tsx @@ -564,7 +564,8 @@ export const RichEditor = forwardRef( return editor?.getJSON() || null; }, setContent: (json: any) => { - if (editor && json) { + // Check editor and view are ready before setting content + if (editor?.view?.dom && json) { editor.commands.setContent(json); } }, @@ -574,7 +575,8 @@ export const RichEditor = forwardRef( // Handle submit on Ctrl/Cmd+Enter useEffect(() => { - if (!editor) return; + // Check both editor and editor.view exist (view may not be ready immediately) + if (!editor?.view?.dom) return; const handleKeyDown = (event: KeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { @@ -585,7 +587,8 @@ export const RichEditor = forwardRef( editor.view.dom.addEventListener("keydown", handleKeyDown); return () => { - editor.view.dom.removeEventListener("keydown", handleKeyDown); + // Also check view.dom exists in cleanup (editor might be destroyed) + editor.view?.dom?.removeEventListener("keydown", handleKeyDown); }; }, [editor, handleSubmit]);