fix(collab): fix self-lockout, stuck live sessions, block locking propagation and reactive hashtags
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
This commit is contained in:
1 parent
0e06cc5577
commit
996f27133c
6 files changed
+303
-89
No files matched your search
@@ -14,9 +14,10 @@ import {
|
||||
Minimize2,
|
||||
Sparkles,
|
||||
Plus,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { Document, DocumentBlock, User, BlockLockInfo, LiveSessionInfo, WSMessage } from '../../lib/types';
|
||||
import { api, getToken, onWebSocketMessage } from '../../lib/api';
|
||||
import { api, getToken, getCurrentUserId, createRealtimeConnection } from '../../lib/api';
|
||||
import { useDocumentSync, DocSyncEvent, RemoteCursor, DocSyncStatus, getUserColor } from '../../lib/useDocumentSync';
|
||||
import { OutlinerTree } from './OutlinerTree';
|
||||
|
||||
@@ -229,6 +230,11 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
return computeBreadcrumbs(title || doc?.title || 'Dokument', blocks, zoomedBlockId);
|
||||
}, [title, doc?.title, blocks, zoomedBlockId]);
|
||||
|
||||
// Reliable current user ID from prop or JWT token
|
||||
const currentUserId = useMemo(() => {
|
||||
return currentUser?.id || getCurrentUserId() || '';
|
||||
}, [currentUser?.id]);
|
||||
|
||||
// Crucial fix: Collaborator with write permission can edit!
|
||||
const canEdit = useMemo(() => {
|
||||
if (!doc) return true;
|
||||
@@ -237,9 +243,19 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
}, [doc]);
|
||||
|
||||
// Is this document completely locked because another user has an active live session?
|
||||
const isLockedByOtherLive = Boolean(liveSession && liveSession.initiatorId !== currentUser?.id);
|
||||
const isLockedByOtherLive = Boolean(
|
||||
liveSession &&
|
||||
liveSession.initiatorId &&
|
||||
currentUserId &&
|
||||
liveSession.initiatorId !== currentUserId
|
||||
);
|
||||
const effectiveCanEdit = canEdit && !isLockedByOtherLive;
|
||||
|
||||
const isLiveInitiatorOrOwner = Boolean(
|
||||
(liveSession && currentUserId && liveSession.initiatorId === currentUserId) ||
|
||||
(doc && (doc.isOwner || doc.permission === 'owner'))
|
||||
);
|
||||
|
||||
// Handle incoming WebSocket events for THIS document room
|
||||
const handleRemoteEvent = useCallback(
|
||||
(evt: DocSyncEvent) => {
|
||||
@@ -524,23 +540,58 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
onLiveStateChange?.(computedLiveState, liveSession?.initiatorName);
|
||||
}, [computedLiveState, liveSession?.initiatorName, onLiveStateChange]);
|
||||
|
||||
const handleToggleLive = useCallback(() => {
|
||||
if (isLiveEnabled) {
|
||||
api.endLiveSession(docId).catch(() => {});
|
||||
const isLiveEnabledRef = useRef<boolean>(isLiveEnabled);
|
||||
isLiveEnabledRef.current = isLiveEnabled;
|
||||
|
||||
const handleToggleLive = useCallback(async () => {
|
||||
if (isLiveEnabled || isLiveInitiatorOrOwner) {
|
||||
try {
|
||||
await api.endLiveSession(docId);
|
||||
} catch {}
|
||||
setIsLiveEnabled(false);
|
||||
setLiveSession(null);
|
||||
} else {
|
||||
if (isLockedByOtherLive) return;
|
||||
api.startLiveSession(docId)
|
||||
.then((session) => {
|
||||
setLiveSession(session);
|
||||
setIsLiveEnabled(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[DocumentEditor] Failed to start live session:', err);
|
||||
});
|
||||
try {
|
||||
const session = await api.startLiveSession(docId);
|
||||
setLiveSession(session);
|
||||
setIsLiveEnabled(true);
|
||||
} catch (err) {
|
||||
console.error('[DocumentEditor] Failed to start live session:', err);
|
||||
}
|
||||
}
|
||||
}, [docId, isLiveEnabled, isLockedByOtherLive]);
|
||||
}, [docId, isLiveEnabled, isLiveInitiatorOrOwner, isLockedByOtherLive]);
|
||||
|
||||
// Keep live session lease alive while isLiveEnabled
|
||||
useEffect(() => {
|
||||
if (!isLiveEnabled || !docId) return;
|
||||
const interval = window.setInterval(() => {
|
||||
api.startLiveSession(docId).catch(() => {});
|
||||
}, 20000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [isLiveEnabled, docId]);
|
||||
|
||||
// Cleanup live session on page unload
|
||||
useEffect(() => {
|
||||
const handleUnloadLive = () => {
|
||||
if (isLiveEnabledRef.current) {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
fetch(`/api/v1/documents/${docId}/live/end`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
keepalive: true,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', handleUnloadLive);
|
||||
window.addEventListener('pagehide', handleUnloadLive);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleUnloadLive);
|
||||
window.removeEventListener('pagehide', handleUnloadLive);
|
||||
};
|
||||
}, [docId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (registerLiveToggle) {
|
||||
@@ -556,7 +607,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
if (cancelled) return;
|
||||
if (res.liveSession) {
|
||||
setLiveSession(res.liveSession);
|
||||
if (res.liveSession.initiatorId === currentUser?.id) {
|
||||
if (currentUserId && res.liveSession.initiatorId === currentUserId) {
|
||||
setIsLiveEnabled(true);
|
||||
}
|
||||
}
|
||||
@@ -573,36 +624,51 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [docId, currentUser?.id]);
|
||||
}, [docId, currentUserId]);
|
||||
|
||||
// Subscribe to realtime lock & live session events
|
||||
useEffect(() => {
|
||||
const unsub = onWebSocketMessage((msg: WSMessage) => {
|
||||
if (msg.event === 'DOC_BLOCK_LOCKED' && msg.payload?.docId === docId) {
|
||||
setActiveBlockLocks((prev) => ({
|
||||
...prev,
|
||||
[msg.payload.blockId]: msg.payload,
|
||||
}));
|
||||
} else if (msg.event === 'DOC_BLOCK_UNLOCKED' && msg.payload?.docId === docId) {
|
||||
setActiveBlockLocks((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[msg.payload.blockId];
|
||||
return next;
|
||||
});
|
||||
} else if (msg.event === 'DOC_LIVE_STARTED' && msg.payload?.docId === docId) {
|
||||
setLiveSession(msg.payload);
|
||||
} else if (msg.event === 'DOC_LIVE_ENDED' && msg.payload?.docId === docId) {
|
||||
setLiveSession(null);
|
||||
if (liveSession?.initiatorId === currentUser?.id) {
|
||||
setIsLiveEnabled(false);
|
||||
const conn = createRealtimeConnection({
|
||||
docId,
|
||||
onMessage: (msg: WSMessage) => {
|
||||
if (msg.event === 'DOC_BLOCK_LOCKED') {
|
||||
const p = (msg.payload?.lock || msg.payload) as BlockLockInfo;
|
||||
if (p?.docId === docId && p?.blockId) {
|
||||
setActiveBlockLocks((prev) => ({
|
||||
...prev,
|
||||
[p.blockId]: p,
|
||||
}));
|
||||
}
|
||||
} else if (msg.event === 'DOC_BLOCK_UNLOCKED') {
|
||||
const p = msg.payload;
|
||||
if (p?.docId === docId && p?.blockId) {
|
||||
setActiveBlockLocks((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[p.blockId];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} else if (msg.event === 'DOC_LIVE_STARTED') {
|
||||
const s = (msg.payload?.session || msg.payload) as LiveSessionInfo;
|
||||
if (s?.docId === docId) {
|
||||
setLiveSession(s);
|
||||
if (currentUserId && s.initiatorId === currentUserId) {
|
||||
setIsLiveEnabled(true);
|
||||
}
|
||||
}
|
||||
} else if (msg.event === 'DOC_LIVE_ENDED') {
|
||||
if (msg.payload?.docId === docId) {
|
||||
setLiveSession(null);
|
||||
setIsLiveEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
conn.close();
|
||||
};
|
||||
}, [docId, currentUser?.id, liveSession?.initiatorId]);
|
||||
}, [docId, currentUserId]);
|
||||
|
||||
// Block focus/blur locks
|
||||
const localUserColor = useMemo(
|
||||
@@ -1127,11 +1193,42 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
|
||||
{/* Live Session Active Banner (when another collaborator is live) */}
|
||||
{isLockedByOtherLive && (
|
||||
<div className="flex items-center gap-2 px-4 sm:px-6 py-2.5 bg-status-warn/15 border-b border-status-warn/30 text-status-warn text-xs font-medium">
|
||||
<Lock className="w-4 h-4 shrink-0" />
|
||||
<span>
|
||||
Live-Sitzung aktiv (Bearbeitet von <strong>{liveSession?.initiatorName || 'anderer Nutzer'}</strong>) – Dokument schreibgeschützt, um Versionskonflikte zu vermeiden.
|
||||
</span>
|
||||
<div className="flex items-center justify-between gap-3 px-4 sm:px-6 py-2.5 bg-status-warn/15 border-b border-status-warn/30 text-status-warn text-xs font-medium">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Lock className="w-4 h-4 shrink-0" />
|
||||
<span>
|
||||
Live-Sitzung aktiv (Bearbeitet von <strong>{liveSession?.initiatorName || 'anderer Nutzer'}</strong>) – Dokument schreibgeschützt, um Versionskonflikte zu vermeiden.
|
||||
</span>
|
||||
</div>
|
||||
{isLiveInitiatorOrOwner && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleLive}
|
||||
className="px-2.5 py-1 text-xs font-semibold rounded-btn bg-status-warn/25 border border-status-warn/40 hover:bg-status-warn/35 text-status-warn shrink-0 cursor-pointer transition-colors"
|
||||
title="Live-Sitzung als Notiz-Eigentümer/Initiator beenden"
|
||||
>
|
||||
Sitzung beenden
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* WebSocket Blocked Banner (when current user has live enabled but WS fails in corporate network) */}
|
||||
{isLiveEnabled && syncStatus === 'blocked' && (
|
||||
<div className="flex items-center justify-between gap-3 px-4 sm:px-6 py-2.5 bg-status-err/15 border-b border-status-err/30 text-status-err text-xs font-medium">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 text-status-err" />
|
||||
<span>
|
||||
Live-Zusammenarbeit nicht möglich (WebSockets im Firmennetzwerk blockiert). HTTPS Auto-Save ist aktiv.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleLive}
|
||||
className="px-2.5 py-1 text-xs font-semibold rounded-btn bg-status-err/25 border border-status-err/40 hover:bg-status-err/35 text-status-err shrink-0 cursor-pointer transition-colors"
|
||||
>
|
||||
Live deaktivieren
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1235,7 +1332,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
activeBlockLocks={activeBlockLocks}
|
||||
onFocusBlock={handleFocusBlock}
|
||||
onBlurBlock={handleBlurBlock}
|
||||
currentUserId={currentUser?.id}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1303,7 +1400,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
activeBlockLocks={activeBlockLocks}
|
||||
onFocusBlock={handleFocusBlock}
|
||||
onBlurBlock={handleBlurBlock}
|
||||
currentUserId={currentUser?.id}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -329,7 +329,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
||||
<div className="space-y-0.5">
|
||||
{visibleEntries.map(({ block, index, hasChildren, maxAllowedLevel }) => {
|
||||
const lockInfo = activeBlockLocks?.[block.id];
|
||||
const isLockedByOther = Boolean(lockInfo && lockInfo.userId !== currentUserId);
|
||||
const isLockedByOther = Boolean(lockInfo && currentUserId && lockInfo.userId !== currentUserId);
|
||||
|
||||
return (
|
||||
<OutlinerBlock
|
||||
|
||||
@@ -267,7 +267,29 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [docs, setDocs] = useState<Document[]>([]);
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const [sharedFolders, setSharedFolders] = useState<SharedFolder[]>([]);
|
||||
const [tagsSummary, setTagsSummary] = useState<TagSummary[]>([]);
|
||||
const [serverTagsSummary, setServerTagsSummary] = useState<TagSummary[]>([]);
|
||||
|
||||
// Dynamically compute hashtag counts directly from in-memory documents
|
||||
const tagsSummary = useMemo(() => {
|
||||
if (docs.length === 0) return serverTagsSummary;
|
||||
const tagMap = new Map<string, number>();
|
||||
for (const d of docs) {
|
||||
for (const b of d.blocks || []) {
|
||||
const matches = b.content?.match(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
||||
if (matches) {
|
||||
for (const m of matches) {
|
||||
const clean = m.slice(1).toLowerCase();
|
||||
tagMap.set(clean, (tagMap.get(clean) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const list: TagSummary[] = [];
|
||||
tagMap.forEach((count, tag) => {
|
||||
list.push({ tag, count });
|
||||
});
|
||||
return list.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
||||
}, [docs, serverTagsSummary]);
|
||||
|
||||
const [selectedDocId, setSelectedDocId] = useState<string | null>(initialDocId || null);
|
||||
const [targetBlockId, setTargetBlockId] = useState<string | null>(null);
|
||||
@@ -356,7 +378,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
setDocs(docsRes);
|
||||
setFolders(foldersRes);
|
||||
setSharedFolders(sharedFoldersRes);
|
||||
setTagsSummary(tagsRes);
|
||||
setServerTagsSummary(tagsRes);
|
||||
|
||||
setSelectedDocId((prevId) => {
|
||||
if (prevId && docsRes.some((d) => d.id === prevId)) {
|
||||
@@ -1196,32 +1218,64 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
{/* Interactive Live Collaboration Button (replaces passive radio icon) */}
|
||||
{docLiveState === 'locked_by_other' ? (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-warn/15 border border-status-warn/40 text-status-warn select-none"
|
||||
title={`Live-Zusammenarbeit aktiv durch ${docLiveInitiator || 'anderer Nutzer'}`}
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="truncate max-w-[120px]">Live: {docLiveInitiator || 'Gesperrt'}</span>
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-warn/15 border border-status-warn/40 text-status-warn select-none"
|
||||
title={`Live-Zusammenarbeit aktiv durch ${docLiveInitiator || 'anderer Nutzer'}`}
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="truncate max-w-[120px]">Live: {docLiveInitiator || 'Gesperrt'}</span>
|
||||
</div>
|
||||
{(selectedDocObj?.isOwner || selectedDocObj?.permission === 'owner') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => liveToggleRef.current?.()}
|
||||
className="px-2 py-1 rounded-btn text-xs font-semibold bg-status-warn/20 border border-status-warn/40 text-status-warn hover:bg-status-warn/30 transition-colors cursor-pointer"
|
||||
title="Als Notiz-Besitzer Live-Sitzung beenden und Dokument freigeben"
|
||||
>
|
||||
Freigeben
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : docLiveState === 'blocked' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWsBlockedModal(true)}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 transition-colors cursor-pointer"
|
||||
title="WebSockets im Firmennetzwerk blockiert – Hier klicken für Infos"
|
||||
>
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-status-err animate-pulse shrink-0" />
|
||||
<span>Live blockiert</span>
|
||||
</button>
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWsBlockedModal(true)}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 transition-colors cursor-pointer"
|
||||
title="WebSockets im Firmennetzwerk blockiert – Hier klicken für Infos"
|
||||
>
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-status-err animate-pulse shrink-0" />
|
||||
<span>Live blockiert</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => liveToggleRef.current?.()}
|
||||
className="px-2 py-1 rounded-btn text-xs font-medium bg-canvas border border-border text-muted hover:text-status-err hover:border-status-err/50 transition-colors cursor-pointer"
|
||||
title="Live-Sitzung beenden"
|
||||
>
|
||||
Beenden
|
||||
</button>
|
||||
</div>
|
||||
) : docLiveState === 'connecting' ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-amber-500/15 border border-amber-500/40 text-amber-500 opacity-80 cursor-wait"
|
||||
>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-amber-500 shrink-0" />
|
||||
<span>Verbinde…</span>
|
||||
</button>
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-amber-500/15 border border-amber-500/40 text-amber-500 opacity-80 cursor-wait"
|
||||
>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-amber-500 shrink-0" />
|
||||
<span>Verbinde…</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => liveToggleRef.current?.()}
|
||||
className="px-2 py-1 rounded-btn text-xs font-medium bg-canvas border border-border text-muted hover:text-status-err hover:border-status-err/50 transition-colors cursor-pointer"
|
||||
title="Verbindungsaufbau abbrechen"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
) : docLiveState === 'active' ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -2547,7 +2601,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
liveToggleRef.current?.();
|
||||
setShowWsBlockedModal(false);
|
||||
}}
|
||||
className="px-3 py-1.5 text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 rounded-btn transition-colors cursor-pointer"
|
||||
>
|
||||
Live-Sitzung beenden
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWsBlockedModal(false)}
|
||||
|
||||
+23
-10
@@ -40,6 +40,19 @@ export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function getCurrentUserId(): string | null {
|
||||
const token = getToken();
|
||||
if (!token) return null;
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
return payload.userId || payload.sub || payload.id || null;
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
||||
}
|
||||
@@ -637,10 +650,11 @@ export const api = {
|
||||
},
|
||||
|
||||
async lockBlock(docId: string, blockId: string, userColor?: string, ttlSeconds: number = 30): Promise<BlockLockInfo> {
|
||||
return request<BlockLockInfo>(`/api/v1/documents/${docId}/blocks/${encodeURIComponent(blockId)}/lock`, {
|
||||
const res = await request<any>(`/api/v1/documents/${docId}/blocks/${encodeURIComponent(blockId)}/lock`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userColor, ttlSeconds }),
|
||||
});
|
||||
return res?.lock || res;
|
||||
},
|
||||
|
||||
async unlockBlock(docId: string, blockId: string): Promise<{ status: string }> {
|
||||
@@ -650,9 +664,10 @@ export const api = {
|
||||
},
|
||||
|
||||
async startLiveSession(docId: string): Promise<LiveSessionInfo> {
|
||||
return request<LiveSessionInfo>(`/api/v1/documents/${docId}/live/start`, {
|
||||
const res = await request<any>(`/api/v1/documents/${docId}/live/start`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return res?.session || res;
|
||||
},
|
||||
|
||||
async endLiveSession(docId: string): Promise<{ status: string }> {
|
||||
@@ -1442,10 +1457,9 @@ export function createRealtimeConnection(options: {
|
||||
// Join rooms on the active connection
|
||||
const joinAll = () => {
|
||||
joinedRooms.forEach((r) => {
|
||||
if (r.startsWith('doc:')) {
|
||||
sendWebSocketMessage({ event: 'CRDT_JOIN', docId: r.slice(4) });
|
||||
} else {
|
||||
sendWebSocketMessage({ event: 'JOIN_ROOM', room: r });
|
||||
sendWebSocketMessage({ event: 'JOIN_ROOM', room: r });
|
||||
if (activeSSEId) {
|
||||
api.joinSSERoom(activeSSEId, r, 'join').catch(() => {});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1475,10 +1489,9 @@ export function createRealtimeConnection(options: {
|
||||
if (unsubMsg) unsubMsg();
|
||||
joinedRooms.forEach((r) => {
|
||||
globalSubscribedRooms.delete(r);
|
||||
if (r.startsWith('doc:')) {
|
||||
sendWebSocketMessage({ event: 'CRDT_LEAVE', docId: r.slice(4) });
|
||||
} else {
|
||||
sendWebSocketMessage({ event: 'LEAVE_ROOM', room: r });
|
||||
sendWebSocketMessage({ event: 'LEAVE_ROOM', room: r });
|
||||
if (activeSSEId) {
|
||||
api.joinSSERoom(activeSSEId, r, 'leave').catch(() => {});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user