diff --git a/frontend/src/features/workspace/DocumentEditor.tsx b/frontend/src/features/workspace/DocumentEditor.tsx index 856a065..6cea80e 100644 --- a/frontend/src/features/workspace/DocumentEditor.tsx +++ b/frontend/src/features/workspace/DocumentEditor.tsx @@ -197,6 +197,7 @@ export const DocumentEditor: React.FC = ({ currentTitleRef.current = title; const currentBlocksRef = useRef(blocks); currentBlocksRef.current = blocks; + const lastKnownUpdatedAtRef = useRef(initialDoc?.updatedAt || null); useEffect(() => { if (!loading && (autoFocusTitle || (doc?.title === 'Neue Notiz' && (!doc?.blocks || doc?.blocks.length <= 1)))) { @@ -434,6 +435,7 @@ export const DocumentEditor: React.FC = ({ }); isSavingRef.current = false; lastSavedSnapshotRef.current = snapshot; + lastKnownUpdatedAtRef.current = updated.updatedAt || null; setSaveStatus('saved'); setSaveErrorMessage(null); onSaveStatusChange?.('saved'); @@ -646,6 +648,9 @@ export const DocumentEditor: React.FC = ({ }); lastSavedSnapshotRef.current = newSnapshot; currentSnapshotRef.current = newSnapshot; + if (remoteDoc.updatedAt) { + lastKnownUpdatedAtRef.current = remoteDoc.updatedAt; + } if (docRef.current) { docRef.current = { ...docRef.current, ...remoteDoc }; @@ -715,32 +720,61 @@ export const DocumentEditor: React.FC = ({ [docId] ); - // Periodic poll for locks & updates (guarantees synchronization even across flaky networks) + // Sync when initialDoc prop changes externally + useEffect(() => { + if (initialDoc && initialDoc.id === docId && initialDoc.updatedAt && initialDoc.updatedAt !== lastKnownUpdatedAtRef.current) { + lastKnownUpdatedAtRef.current = initialDoc.updatedAt; + handleRemoteDocUpdated(initialDoc); + } + }, [initialDoc, docId, handleRemoteDocUpdated]); + + // Periodic poll for locks & document text (guarantees text synchronization even without WebSockets) useEffect(() => { if (!docId) return; - const syncLocks = () => { - api.getDocLocks(docId) - .then((res) => { - if (Array.isArray(res.blockLocks)) { - const map: Record = {}; - for (const l of res.blockLocks) { - map[l.blockId] = l; - } - setActiveBlockLocks(map); + let isCancelled = false; + + const syncLocksAndDoc = async () => { + try { + // 1. Sync locks + const locksRes = await api.getDocLocks(docId); + if (isCancelled) return; + if (Array.isArray(locksRes.blockLocks)) { + const map: Record = {}; + for (const l of locksRes.blockLocks) { + map[l.blockId] = l; } - if (res.liveSession) { - setLiveSession(res.liveSession); - } else { - setLiveSession(null); + setActiveBlockLocks(map); + } + if (locksRes.liveSession) { + setLiveSession(locksRes.liveSession); + } else { + setLiveSession(null); + } + + // 2. Sync document content if not currently actively saving + if (!isSavingRef.current) { + const remoteDoc = await api.getDocument(docId); + if (isCancelled || !remoteDoc) return; + + // Check if server has newer or different state + if (remoteDoc.updatedAt && remoteDoc.updatedAt !== lastKnownUpdatedAtRef.current) { + console.log(`[DocumentEditor] Polling: Neuer Text-Stand vom Server (${remoteDoc.updatedAt}). Aktualisiere nicht-fokussierte Blöcke...`); + lastKnownUpdatedAtRef.current = remoteDoc.updatedAt; + handleRemoteDocUpdated(remoteDoc); } - }) - .catch(() => {}); + } + } catch (err) { + // ignore poll errors + } }; - syncLocks(); - const interval = window.setInterval(syncLocks, 3000); - return () => window.clearInterval(interval); - }, [docId]); + syncLocksAndDoc(); + const interval = window.setInterval(syncLocksAndDoc, 2000); + return () => { + isCancelled = true; + window.clearInterval(interval); + }; + }, [docId, handleRemoteDocUpdated]); // Subscribe to realtime lock & live session events useEffect(() => { @@ -842,7 +876,7 @@ export const DocumentEditor: React.FC = ({ focusedBlockIdRef.current = null; } api.unlockBlock(docId, blockId).catch(() => {}); - executeAutoSave(); + executeAutoSave(true); }, [docId, executeAutoSave] );