Dev #5
No files matched your search
@@ -132,6 +132,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
// Dedicated WebSocket connection & status for live indicator
|
// Dedicated WebSocket connection & status for live indicator
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const wsReconnectTimerRef = useRef<any>(null);
|
const wsReconnectTimerRef = useRef<any>(null);
|
||||||
|
const wsReconnectDelayRef = useRef<number>(2000);
|
||||||
const [wsConnected, setWsConnected] = useState<boolean>(false);
|
const [wsConnected, setWsConnected] = useState<boolean>(false);
|
||||||
|
|
||||||
// Folder state (synchronized with server)
|
// Folder state (synchronized with server)
|
||||||
@@ -970,6 +971,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
wsReconnectDelayRef.current = 2000;
|
||||||
setWsConnected(true);
|
setWsConnected(true);
|
||||||
api.getFolders().then(setCustomFolders).catch(() => {});
|
api.getFolders().then(setCustomFolders).catch(() => {});
|
||||||
api.getSharedFolders().then(setSharedFolders).catch(() => {});
|
api.getSharedFolders().then(setSharedFolders).catch(() => {});
|
||||||
@@ -979,7 +981,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setWsConnected(false);
|
setWsConnected(false);
|
||||||
clearTimeout(wsReconnectTimerRef.current);
|
clearTimeout(wsReconnectTimerRef.current);
|
||||||
wsReconnectTimerRef.current = setTimeout(connect, 2000);
|
const delay = wsReconnectDelayRef.current;
|
||||||
|
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
|
||||||
|
wsReconnectTimerRef.current = setTimeout(connect, delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
@@ -997,7 +1001,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setWsConnected(false);
|
setWsConnected(false);
|
||||||
clearTimeout(wsReconnectTimerRef.current);
|
clearTimeout(wsReconnectTimerRef.current);
|
||||||
wsReconnectTimerRef.current = setTimeout(connect, 3000);
|
const delay = wsReconnectDelayRef.current;
|
||||||
|
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
|
||||||
|
wsReconnectTimerRef.current = setTimeout(connect, delay);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1024,6 +1030,44 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
};
|
};
|
||||||
}, [onStatsChange]);
|
}, [onStatsChange]);
|
||||||
|
|
||||||
|
// Fallback HTTP polling when WebSocket is disconnected (e.g. corporate proxy / firewall blocking WebSockets)
|
||||||
|
useEffect(() => {
|
||||||
|
if (wsConnected) return;
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
if (!isMounted || document.visibilityState !== 'visible') return;
|
||||||
|
|
||||||
|
// 1. Poll lock status of currently selected document
|
||||||
|
if (selectedDocIdRef.current) {
|
||||||
|
try {
|
||||||
|
const res = await api.getDocLock(selectedDocIdRef.current);
|
||||||
|
if (res.locked && res.lock && res.lock.userId !== currentUserIdRef.current) {
|
||||||
|
setActiveLock(res.lock);
|
||||||
|
} else {
|
||||||
|
setActiveLock(null);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Poll document content if not actively typing and document has changed on server
|
||||||
|
if (selectedDocIdRef.current && !isEditorFocusedRef.current && !pendingSaveDocRef.current) {
|
||||||
|
try {
|
||||||
|
const fresh = await api.getDocument(selectedDocIdRef.current);
|
||||||
|
if (fresh && (!selectedDoc || fresh.updatedAt !== selectedDoc.updatedAt)) {
|
||||||
|
setSelectedDoc(fresh);
|
||||||
|
setDocs((prev) => prev.map((d) => (d.id === fresh.id ? fresh : d)));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
};
|
||||||
|
}, [wsConnected, selectedDoc?.updatedAt]);
|
||||||
|
|
||||||
const openShareModal = async () => {
|
const openShareModal = async () => {
|
||||||
if (!selectedDoc) return;
|
if (!selectedDoc) return;
|
||||||
setShowShareModal(true);
|
setShowShareModal(true);
|
||||||
@@ -4001,11 +4045,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
className={`h-8 w-8 flex items-center justify-center rounded-btn border transition-colors ${
|
className={`h-8 w-8 flex items-center justify-center rounded-btn border transition-colors ${
|
||||||
wsConnected
|
wsConnected
|
||||||
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
|
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
|
||||||
: 'bg-canvas border-border text-muted'
|
: 'bg-amber-500/10 border-amber-500/30 text-amber-500'
|
||||||
}`}
|
}`}
|
||||||
title={wsConnected ? 'Live synchronisiert (Online)' : 'Offline - Verbindung wird wiederhergestellt'}
|
title={
|
||||||
|
wsConnected
|
||||||
|
? 'Live synchronisiert (WebSocket online)'
|
||||||
|
: 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
|
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4037,11 +4085,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
className={`h-9 w-9 flex items-center justify-center rounded-btn border transition-colors ${
|
className={`h-9 w-9 flex items-center justify-center rounded-btn border transition-colors ${
|
||||||
wsConnected
|
wsConnected
|
||||||
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
|
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
|
||||||
: 'bg-canvas border-border text-muted'
|
: 'bg-amber-500/10 border-amber-500/30 text-amber-500'
|
||||||
}`}
|
}`}
|
||||||
title={wsConnected ? 'Live synchronisiert (Online)' : 'Offline - Verbindung wird wiederhergestellt'}
|
title={
|
||||||
|
wsConnected
|
||||||
|
? 'Live synchronisiert (WebSocket online)'
|
||||||
|
: 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
|
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1048,6 +1048,7 @@ let wsListeners: ((msg: WSMessage) => void)[] = [];
|
|||||||
let wsStatusListeners: ((connected: boolean) => void)[] = [];
|
let wsStatusListeners: ((connected: boolean) => void)[] = [];
|
||||||
let isWSConnected = false;
|
let isWSConnected = false;
|
||||||
let manualDisconnect = false;
|
let manualDisconnect = false;
|
||||||
|
let wsReconnectDelay = 2000;
|
||||||
|
|
||||||
export function buildWebSocketURL(listId?: string): string {
|
export function buildWebSocketURL(listId?: string): string {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
@@ -1097,6 +1098,7 @@ export function connectWebSocket(): void {
|
|||||||
activeGlobalWS = ws;
|
activeGlobalWS = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
|
wsReconnectDelay = 2000;
|
||||||
notifyStatusChange(true);
|
notifyStatusChange(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1121,9 +1123,11 @@ export function connectWebSocket(): void {
|
|||||||
notifyStatusChange(false);
|
notifyStatusChange(false);
|
||||||
if (!manualDisconnect && getToken()) {
|
if (!manualDisconnect && getToken()) {
|
||||||
clearTimeout(wsReconnectTimer);
|
clearTimeout(wsReconnectTimer);
|
||||||
|
const delay = wsReconnectDelay;
|
||||||
|
wsReconnectDelay = Math.min(30000, Math.round(wsReconnectDelay * 1.5));
|
||||||
wsReconnectTimer = setTimeout(() => {
|
wsReconnectTimer = setTimeout(() => {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}, 3000);
|
}, delay);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user