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 { 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}
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user