1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-23 12:27:48 +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 { Utils } from '../../utils/utils';
import ExdrawServerApi from './collab/exdraw-server-api';
import { io } from 'socket.io-client';
import './index.css';
const { docUuid, excalidrawServerUrl } = window.app.pageOptions;
const { docUuid, excalidrawServerUrl, username } = window.app.pageOptions;
const ExcaliEditor = () => {
const [fileContent, setFileContent] = useState(null);
@@ -22,6 +23,11 @@ const ExcaliEditor = () => {
exdrawUuid: '',
accessToken: ''
});
const exdrawClientConfigRef = useRef({
excalidrawServerUrl,
username
});
const exdrawClientRef = useRef(null);
useEffect(() => {
editorApi.getExdrawToken().then(res => {
@@ -35,6 +41,7 @@ const ExcaliEditor = () => {
setFileContent(res.data);
setIsFetching(false);
});
exdrawClientRef.current = ExdrawClient(exdrawClientConfigRef.current);
});
onSetFavicon();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -44,7 +51,9 @@ const ExcaliEditor = () => {
if (isChangedRef.current) {
try {
const exdrawServerApi = new ExdrawServerApi(exdrawServerConfigRef.current);
// socket emit event
await exdrawServerApi.saveSceneContent(JSON.stringify(editorRef.current));
exdrawClientRef.current.emit('update-document', JSON.stringify(editorRef.current));
isChangedRef.current = false;
toaster.success(gettext('Successfully saved'), { duration: 2 });
} catch {
@@ -95,7 +104,9 @@ const ExcaliEditor = () => {
}, [saveSceneContent]);
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;
}, []);
@@ -105,8 +116,28 @@ const ExcaliEditor = () => {
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 (
<SimpleEditor
exdrawClient={exdrawClientRef.current}
isFetching={isFetching}
sceneContent={fileContent}
onSaveContent={onSaveContent}

View File

@@ -1,8 +1,16 @@
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 CodeMirrorLoading from '../../components/code-mirror-loading';
import { langList } from './constants';
import { isSyncableElement } from './collab/utils';
import '@excalidraw/excalidraw/index.css';
@@ -10,25 +18,27 @@ const SimpleEditor = ({
sceneContent = null,
onChangeContent,
onSaveContent,
isFetching
isFetching,
exdrawClient
}) => {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
const prevElementsRef = useRef([]);
const UIOptions = {
canvasActions: {
saveToActiveFile: false,
LoadScene: false
LoadScene: true
},
tools: { image: false },
};
const handleChange = () => {
const elements = excalidrawAPI.getSceneElements();
if (hasChanged(elements, prevElementsRef.current)) {
onChangeContent(elements);
}
prevElementsRef.current = elements;
const lastBroadcastedOrReceivedSceneVersion = useRef(-1);
const handleChange = (_elements) => {
broadcastElements(_elements);
};
useEffect(() => {
handleRemoteSceneUpdate(_reconcileElements(sceneContent?.elements));
}, [sceneContent]);
useEffect(() => {
const handleHotkeySave = (event) => {
if (isHotkey('mod+s', event)) {
@@ -43,12 +53,61 @@ const SimpleEditor = ({
};
}, [excalidrawAPI, onSaveContent]);
const hasChanged = (prev, current) => {
if (prev.length !== current.length) return true;
return current.some((element, index) => {
return element.version !== prev[index]?.version;
const handleRemoteSceneUpdate = (elements) => {
if (excalidrawAPI) {
excalidrawAPI.updateScene({
elements,
captureUpdate: CaptureUpdateAction.NEVER,
});
}
};
const _reconcileElements = (remoteElements) => {
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) {