diff --git a/src/components/App.tsx b/src/components/App.tsx index 7eba91f4..7172d1eb 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3,7 +3,6 @@ import React from "react"; import rough from "roughjs/bin/rough"; import { RoughCanvas } from "roughjs/bin/canvas"; import { simplify, Point } from "points-on-curve"; -import { SocketUpdateData } from "../types"; import { newElement, @@ -279,9 +278,8 @@ export type ExcalidrawImperativeAPI = class App extends React.Component { canvas: HTMLCanvasElement | null = null; rc: RoughCanvas | null = null; - portal: Portal = new Portal(this); - lastBroadcastedOrReceivedSceneVersion: number = -1; - broadcastedElementVersions: Map = new Map(); + portal: Portal; + private lastBroadcastedOrReceivedSceneVersion: number = -1; unmounted: boolean = false; actionManager: ActionManager; private excalidrawRef: any; @@ -312,6 +310,8 @@ class App extends React.Component { }; } this.scene = new Scene(); + this.portal = new Portal(this); + this.excalidrawRef = React.createRef(); this.actionManager = new ActionManager( this.syncActionResult, @@ -396,6 +396,18 @@ class App extends React.Component { ); } + public setLastBroadcastedOrReceivedSceneVersion = (version: number) => { + this.lastBroadcastedOrReceivedSceneVersion = version; + }; + + public getLastBroadcastedOrReceivedSceneVersion = () => { + return this.lastBroadcastedOrReceivedSceneVersion; + }; + + public getSceneElementsIncludingDeleted = () => { + return this.scene.getElementsIncludingDeleted(); + }; + private syncActionResult = withBatchedUpdates( (actionResult: ActionResult) => { if (this.unmounted || actionResult === false) { @@ -823,7 +835,7 @@ class App extends React.Component { }); queueBroadcastAllElements = throttle(() => { - this.broadcastScene(SCENE.UPDATE, /* syncAll */ true); + this.portal.broadcastScene(SCENE.UPDATE, /* syncAll */ true); }, SYNC_FULL_SCENE_INTERVAL_MS); componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) { @@ -949,7 +961,7 @@ class App extends React.Component { getSceneVersion(this.scene.getElementsIncludingDeleted()) > this.lastBroadcastedOrReceivedSceneVersion ) { - this.broadcastScene(SCENE.UPDATE, /* syncAll */ false); + this.portal.broadcastScene(SCENE.UPDATE, /* syncAll */ false); this.queueBroadcastAllElements(); } @@ -1363,7 +1375,10 @@ class App extends React.Component { // we just received! // Note: this needs to be set before replaceAllElements as it // syncronously calls render. - this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(newElements); + + this.setLastBroadcastedOrReceivedSceneVersion( + getSceneVersion(newElements), + ); this.scene.replaceAllElements(newElements); } @@ -1511,28 +1526,6 @@ class App extends React.Component { }); } - private broadcastMouseLocation = (payload: { - pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"]; - button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; - }) => { - if (this.portal.socket?.id) { - const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { - type: "MOUSE_LOCATION", - payload: { - socketId: this.portal.socket.id, - pointer: payload.pointer, - button: payload.button || "up", - selectedElementIds: this.state.selectedElementIds, - username: this.state.username, - }, - }; - return this.portal._broadcastSocketData( - data as SocketUpdateData, - true, // volatile - ); - } - }; - saveCollabRoomToFirebase = async ( syncableElements: ExcalidrawElement[] = getSyncableElements( this.scene.getElementsIncludingDeleted(), @@ -1545,62 +1538,6 @@ class App extends React.Component { } }; - // maybe should move to Portal - broadcastScene = async ( - sceneType: SCENE.INIT | SCENE.UPDATE, - syncAll: boolean, - ) => { - if (sceneType === SCENE.INIT && !syncAll) { - throw new Error("syncAll must be true when sending SCENE.INIT"); - } - - let syncableElements = getSyncableElements( - this.scene.getElementsIncludingDeleted(), - ); - - if (!syncAll) { - // sync out only the elements we think we need to to save bandwidth. - // periodically we'll resync the whole thing to make sure no one diverges - // due to a dropped message (server goes down etc). - syncableElements = syncableElements.filter( - (syncableElement) => - !this.broadcastedElementVersions.has(syncableElement.id) || - syncableElement.version > - this.broadcastedElementVersions.get(syncableElement.id)!, - ); - } - - const data: SocketUpdateDataSource[typeof sceneType] = { - type: sceneType, - payload: { - elements: syncableElements, - }, - }; - this.lastBroadcastedOrReceivedSceneVersion = Math.max( - this.lastBroadcastedOrReceivedSceneVersion, - getSceneVersion(this.scene.getElementsIncludingDeleted()), - ); - for (const syncableElement of syncableElements) { - this.broadcastedElementVersions.set( - syncableElement.id, - syncableElement.version, - ); - } - - const broadcastPromise = this.portal._broadcastSocketData( - data as SocketUpdateData, - ); - - if (syncAll && this.state.isCollaborating) { - await Promise.all([ - broadcastPromise, - this.saveCollabRoomToFirebase(syncableElements), - ]); - } else { - await broadcastPromise; - } - }; - private onSceneUpdated = () => { this.setState({}); }; @@ -4078,7 +4015,7 @@ class App extends React.Component { this.portal.socket && // do not broadcast when more than 1 pointer since that shows flickering on the other side gesture.pointers.size < 2 && - this.broadcastMouseLocation({ + this.portal.broadcastMouseLocation({ pointer, button, }); diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx index 8e1fd6a2..d6de2fe6 100644 --- a/src/components/Portal.tsx +++ b/src/components/Portal.tsx @@ -1,8 +1,9 @@ -import { encryptAESGEM } from "../data"; +import { encryptAESGEM, SocketUpdateDataSource } from "../data"; import { SocketUpdateData } from "../types"; import { BROADCAST, SCENE } from "../constants"; import App from "./App"; +import { getSceneVersion, getSyncableElements } from "../element"; class Portal { app: App; @@ -10,6 +11,7 @@ class Portal { socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized roomID: string | null = null; roomKey: string | null = null; + broadcastedElementVersions: Map = new Map(); constructor(app: App) { this.app = app; @@ -27,7 +29,7 @@ class Portal { } }); this.socket.on("new-user", async (_socketId: string) => { - this.app.broadcastScene(SCENE.INIT, /* syncAll */ true); + this.broadcastScene(SCENE.INIT, /* syncAll */ true); }); this.socket.on("room-user-change", (clients: string[]) => { this.app.setCollaborators(clients); @@ -69,5 +71,86 @@ class Portal { ); } } + + broadcastScene = async ( + sceneType: SCENE.INIT | SCENE.UPDATE, + syncAll: boolean, + ) => { + if (sceneType === SCENE.INIT && !syncAll) { + throw new Error("syncAll must be true when sending SCENE.INIT"); + } + + let syncableElements = getSyncableElements( + this.app.getSceneElementsIncludingDeleted(), + ); + + if (!syncAll) { + // sync out only the elements we think we need to to save bandwidth. + // periodically we'll resync the whole thing to make sure no one diverges + // due to a dropped message (server goes down etc). + syncableElements = syncableElements.filter( + (syncableElement) => + !this.broadcastedElementVersions.has(syncableElement.id) || + syncableElement.version > + this.broadcastedElementVersions.get(syncableElement.id)!, + ); + } + + const data: SocketUpdateDataSource[typeof sceneType] = { + type: sceneType, + payload: { + elements: syncableElements, + }, + }; + const currentVersion = this.app.getLastBroadcastedOrReceivedSceneVersion(); + const newVersion = Math.max( + currentVersion, + getSceneVersion(this.app.getSceneElementsIncludingDeleted()), + ); + this.app.setLastBroadcastedOrReceivedSceneVersion(newVersion); + + for (const syncableElement of syncableElements) { + this.broadcastedElementVersions.set( + syncableElement.id, + syncableElement.version, + ); + } + + const broadcastPromise = this._broadcastSocketData( + data as SocketUpdateData, + ); + + if (syncAll && this.app.state.isCollaborating) { + await Promise.all([ + broadcastPromise, + this.app.saveCollabRoomToFirebase(syncableElements), + ]); + } else { + await broadcastPromise; + } + }; + + broadcastMouseLocation = (payload: { + pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"]; + button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; + }) => { + if (this.socket?.id) { + const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { + type: "MOUSE_LOCATION", + payload: { + socketId: this.socket.id, + pointer: payload.pointer, + button: payload.button || "up", + selectedElementIds: this.app.state.selectedElementIds, + username: this.app.state.username, + }, + }; + return this._broadcastSocketData( + data as SocketUpdateData, + true, // volatile + ); + } + }; } + export default Portal;