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
This commit is contained in:
Claude
2026-01-21 16:08:54 +00:00
parent c955bf8eb0
commit 78b1cbe0c5
2 changed files with 34 additions and 21 deletions

View File

@@ -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(() => {

View File

@@ -564,7 +564,8 @@ export const RichEditor = forwardRef<RichEditorHandle, RichEditorProps>(
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<RichEditorHandle, RichEditorProps>(
// 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<RichEditorHandle, RichEditorProps>(
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]);