1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-23 20:37:42 +00:00

exdraw collaboration

This commit is contained in:
zhichaona
2025-05-09 16:37:57 +08:00
parent 668281b099
commit 4f24834d17
4 changed files with 3939 additions and 16 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
/**
* Transforms array of objects containing `id` attribute,
* or array of ids (strings), into a Map, keyd by `id`.
*/
export const arrayToMap = (items) => {
if (items instanceof Map) {
return items;
}
return items.reduce((acc, element) => {
acc.set(typeof element === 'string' ? element : element.id, element);
return acc;
}, new Map());
};
const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day
export const isSyncableElement = (element) => {
if (element.isDeleted) {
if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) {
return true; // 同步,在删除时间之内 1day
}
return false; // 不同步
}
return !isInvisiblySmallElement(element);
};
export const isInvisiblySmallElement = (
element
) => {
return element.width === 0 && element.height === 0;
};

View File

@@ -7,10 +7,11 @@ import toaster from '../../components/toast';
import { SAVE_INTERVAL_TIME } from './constants'; import { SAVE_INTERVAL_TIME } from './constants';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import ExdrawServerApi from './collab/exdraw-server-api'; import ExdrawServerApi from './collab/exdraw-server-api';
import { io } from 'socket.io-client';
import './index.css'; import './index.css';
const { docUuid, excalidrawServerUrl } = window.app.pageOptions; const { docUuid, excalidrawServerUrl, username } = window.app.pageOptions;
const ExcaliEditor = () => { const ExcaliEditor = () => {
const [fileContent, setFileContent] = useState(null); const [fileContent, setFileContent] = useState(null);
@@ -22,6 +23,11 @@ const ExcaliEditor = () => {
exdrawUuid: '', exdrawUuid: '',
accessToken: '' accessToken: ''
}); });
const exdrawClientConfigRef = useRef({
excalidrawServerUrl,
username
});
const exdrawClientRef = useRef(null);
useEffect(() => { useEffect(() => {
editorApi.getExdrawToken().then(res => { editorApi.getExdrawToken().then(res => {
@@ -35,6 +41,7 @@ const ExcaliEditor = () => {
setFileContent(res.data); setFileContent(res.data);
setIsFetching(false); setIsFetching(false);
}); });
exdrawClientRef.current = ExdrawClient(exdrawClientConfigRef.current);
}); });
onSetFavicon(); onSetFavicon();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -44,7 +51,9 @@ const ExcaliEditor = () => {
if (isChangedRef.current) { if (isChangedRef.current) {
try { try {
const exdrawServerApi = new ExdrawServerApi(exdrawServerConfigRef.current); const exdrawServerApi = new ExdrawServerApi(exdrawServerConfigRef.current);
// socket emit event
await exdrawServerApi.saveSceneContent(JSON.stringify(editorRef.current)); await exdrawServerApi.saveSceneContent(JSON.stringify(editorRef.current));
exdrawClientRef.current.emit('update-document', JSON.stringify(editorRef.current));
isChangedRef.current = false; isChangedRef.current = false;
toaster.success(gettext('Successfully saved'), { duration: 2 }); toaster.success(gettext('Successfully saved'), { duration: 2 });
} catch { } catch {
@@ -95,7 +104,9 @@ const ExcaliEditor = () => {
}, [saveSceneContent]); }, [saveSceneContent]);
const onChangeContent = useCallback((elements) => { const onChangeContent = useCallback((elements) => {
editorRef.current = { elements }; editorRef.current = elements;
// socket emit event
exdrawClientRef.current.emit('update-document', JSON.stringify(editorRef.current));
isChangedRef.current = true; isChangedRef.current = true;
}, []); }, []);
@@ -105,8 +116,28 @@ const ExcaliEditor = () => {
document.getElementById('favicon').href = fileIcon; document.getElementById('favicon').href = fileIcon;
}, []); }, []);
const ExdrawClient = useCallback((options) => {
const socket = io(options.excalidrawServerUrl + '/exdraw');
socket.on('connect', () => {
const name = options.username;
const userInfo = { name };
socket.emit('join-room', userInfo);
});
socket.on('join-room', (userInfo) => {
console.log('join-room', userInfo);
});
socket.on('update-document', function (msg) {
setFileContent(JSON.parse(msg.msg));
});
return socket;
}, []);
return ( return (
<SimpleEditor <SimpleEditor
exdrawClient={exdrawClientRef.current}
isFetching={isFetching} isFetching={isFetching}
sceneContent={fileContent} sceneContent={fileContent}
onSaveContent={onSaveContent} onSaveContent={onSaveContent}

View File

@@ -1,8 +1,16 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Excalidraw, MainMenu } from '@excalidraw/excalidraw'; import {
CaptureUpdateAction,
Excalidraw,
getSceneVersion,
MainMenu,
reconcileElements,
restoreElements
} from '@excalidraw/excalidraw';
import isHotkey from 'is-hotkey'; import isHotkey from 'is-hotkey';
import CodeMirrorLoading from '../../components/code-mirror-loading'; import CodeMirrorLoading from '../../components/code-mirror-loading';
import { langList } from './constants'; import { langList } from './constants';
import { isSyncableElement } from './collab/utils';
import '@excalidraw/excalidraw/index.css'; import '@excalidraw/excalidraw/index.css';
@@ -10,25 +18,27 @@ const SimpleEditor = ({
sceneContent = null, sceneContent = null,
onChangeContent, onChangeContent,
onSaveContent, onSaveContent,
isFetching isFetching,
exdrawClient
}) => { }) => {
const [excalidrawAPI, setExcalidrawAPI] = useState(null); const [excalidrawAPI, setExcalidrawAPI] = useState(null);
const prevElementsRef = useRef([]);
const UIOptions = { const UIOptions = {
canvasActions: { canvasActions: {
saveToActiveFile: false, saveToActiveFile: false,
LoadScene: false LoadScene: true
}, },
tools: { image: false }, tools: { image: false },
}; };
const handleChange = () => { const lastBroadcastedOrReceivedSceneVersion = useRef(-1);
const elements = excalidrawAPI.getSceneElements();
if (hasChanged(elements, prevElementsRef.current)) { const handleChange = (_elements) => {
onChangeContent(elements); broadcastElements(_elements);
}
prevElementsRef.current = elements;
}; };
useEffect(() => {
handleRemoteSceneUpdate(_reconcileElements(sceneContent?.elements));
}, [sceneContent]);
useEffect(() => { useEffect(() => {
const handleHotkeySave = (event) => { const handleHotkeySave = (event) => {
if (isHotkey('mod+s', event)) { if (isHotkey('mod+s', event)) {
@@ -43,14 +53,63 @@ const SimpleEditor = ({
}; };
}, [excalidrawAPI, onSaveContent]); }, [excalidrawAPI, onSaveContent]);
const hasChanged = (prev, current) => { const handleRemoteSceneUpdate = (elements) => {
if (prev.length !== current.length) return true; if (excalidrawAPI) {
excalidrawAPI.updateScene({
elements,
captureUpdate: CaptureUpdateAction.NEVER,
});
}
};
return current.some((element, index) => { const _reconcileElements = (remoteElements) => {
return element.version !== prev[index]?.version; if (!remoteElements || !excalidrawAPI) return;
const localElements = getSceneElementsIncludingDeleted();
const appState = excalidrawAPI.getAppState();
const restoredRemoteElements = restoreElements(remoteElements, null);
const reconciledElements = reconcileElements(
localElements,
restoredRemoteElements,
appState
);
setLastBroadcastedOrReceivedSceneVersion(
getSceneVersion(reconciledElements)
);
return reconciledElements;
};
const broadcastElements = (elements) => {
if (
getSceneVersion(elements) >
lastBroadcastedOrReceivedSceneVersion.current
) {
broadcastScene('SCENE_UPDATE', elements);
lastBroadcastedOrReceivedSceneVersion.current = getSceneVersion(elements);
}
};
const broadcastScene = (updateType, elements) => {
const data = {
type: updateType,
payload: {
elements: elements,
},
};
exdrawClient.emit('update-document', JSON.stringify(data.payload.elements));
onChangeContent({
elements: data.payload.elements,
}); });
}; };
const getSceneElementsIncludingDeleted = () => {
return excalidrawAPI?.getSceneElementsIncludingDeleted();
};
const setLastBroadcastedOrReceivedSceneVersion = (version) => {
lastBroadcastedOrReceivedSceneVersion.current = version;
};
if (isFetching) { if (isFetching) {
return ( return (
<div className='excali-container'> <div className='excali-container'>