fix: prevent TipTap editor crash when view is not ready (#188)

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

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-21 17:15:41 +01:00
committed by GitHub
parent c955bf8eb0
commit 66618fb150
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]);