mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-23 12:27:48 +00:00
exdraw collaboration
This commit is contained in:
3801
frontend/src/pages/excalidraw-editor/collab/restore.js
Normal file
3801
frontend/src/pages/excalidraw-editor/collab/restore.js
Normal file
File diff suppressed because it is too large
Load Diff
32
frontend/src/pages/excalidraw-editor/collab/utils.js
Normal file
32
frontend/src/pages/excalidraw-editor/collab/utils.js
Normal 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;
|
||||||
|
};
|
@@ -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}
|
||||||
|
@@ -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'>
|
||||||
|
Reference in New Issue
Block a user