diff --git a/src/components/App.tsx b/src/components/App.tsx index f9ac523e..89e07a80 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -162,12 +162,34 @@ class Portal { this.roomID = null; this.roomKey = null; } + + isOpen() { + return this.socketInitialized && this.socket && this.roomID && this.roomKey; + } + + async _broadcastSocketData( + data: SocketUpdateDataSource[keyof SocketUpdateDataSource] & { + _brand: "socketUpdateData"; + }, + ) { + if (this.isOpen()) { + const json = JSON.stringify(data); + const encoded = new TextEncoder().encode(json); + const encrypted = await encryptAESGEM(encoded, this.roomKey!); + this.socket!.emit( + "server-broadcast", + this.roomID, + encrypted.data, + encrypted.iv, + ); + } + } } export class App extends React.Component { canvas: HTMLCanvasElement | null = null; rc: RoughCanvas | null = null; - room: Portal = new Portal(); + portal: Portal = new Portal(); lastBroadcastedOrReceivedSceneVersion: number = -1; removeSceneCallback: SceneStateCallbackRemover | null = null; @@ -444,7 +466,7 @@ export class App extends React.Component { }); componentDidUpdate() { - if (this.state.isCollaborating && !this.room.socket) { + if (this.state.isCollaborating && !this.portal.socket) { this.initializeSocketClient({ showLoadingState: true }); } @@ -740,17 +762,17 @@ export class App extends React.Component { isCollaborating: false, collaborators: new Map(), }); - this.room.close(); + this.portal.close(); }; private initializeSocketClient = (opts: { showLoadingState: boolean }) => { - if (this.room.socket) { + if (this.portal.socket) { return; } const roomMatch = getCollaborationLinkData(window.location.href); if (roomMatch) { const initialize = () => { - this.room.socketInitialized = true; + this.portal.socketInitialized = true; clearTimeout(initializationTimer); if (this.state.isLoading && !this.unmounted) { this.setState({ isLoading: false }); @@ -856,26 +878,30 @@ export class App extends React.Component { // undo, a user makes a change, and then try to redo, your element(s) will be lost. However, // right now we think this is the right tradeoff. history.clear(); - if (this.room.socketInitialized === false) { + if (this.portal.socketInitialized === false) { initialize(); } }; - this.room.open(socketIOClient(SOCKET_SERVER), roomMatch[1], roomMatch[2]); + this.portal.open( + socketIOClient(SOCKET_SERVER), + roomMatch[1], + roomMatch[2], + ); - this.room.socket!.on("init-room", () => { - this.room.socket && - this.room.socket.emit("join-room", this.room.roomID); + this.portal.socket!.on("init-room", () => { + this.portal.socket && + this.portal.socket.emit("join-room", this.portal.roomID); }); - this.room.socket!.on( + this.portal.socket!.on( "client-broadcast", async (encryptedData: ArrayBuffer, iv: Uint8Array) => { - if (!this.room.roomKey) { + if (!this.portal.roomKey) { return; } const decryptedData = await decryptAESGEM( encryptedData, - this.room.roomKey, + this.portal.roomKey, iv, ); @@ -883,7 +909,7 @@ export class App extends React.Component { case "INVALID_RESPONSE": return; case "SCENE_INIT": { - if (!this.room.socketInitialized) { + if (!this.portal.socketInitialized) { updateScene(decryptedData, { scrollToContent: true }); } break; @@ -916,13 +942,13 @@ export class App extends React.Component { } }, ); - this.room.socket!.on("first-in-room", () => { - if (this.room.socket) { - this.room.socket.off("first-in-room"); + this.portal.socket!.on("first-in-room", () => { + if (this.portal.socket) { + this.portal.socket.off("first-in-room"); } initialize(); }); - this.room.socket!.on("room-user-change", (clients: string[]) => { + this.portal.socket!.on("room-user-change", (clients: string[]) => { this.setState((state) => { const collaborators: typeof state.collaborators = new Map(); for (const socketID of clients) { @@ -938,7 +964,7 @@ export class App extends React.Component { }; }); }); - this.room.socket!.on("new-user", async (_socketID: string) => { + this.portal.socket!.on("new-user", async (_socketID: string) => { this.broadcastScene("SCENE_INIT"); }); @@ -953,18 +979,18 @@ export class App extends React.Component { pointerCoords: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointerCoords"]; button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; }) => { - if (this.room.socket?.id) { + if (this.portal.socket?.id) { const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { type: "MOUSE_LOCATION", payload: { - socketID: this.room.socket.id, + socketID: this.portal.socket.id, pointerCoords: payload.pointerCoords, button: payload.button || "up", selectedElementIds: this.state.selectedElementIds, username: this.state.username, }, }; - return this._broadcastSocketData( + return this.portal._broadcastSocketData( data as typeof data & { _brand: "socketUpdateData" }, ); } @@ -983,35 +1009,11 @@ export class App extends React.Component { this.lastBroadcastedOrReceivedSceneVersion, getDrawingVersion(globalSceneState.getElementsIncludingDeleted()), ); - return this._broadcastSocketData( + return this.portal._broadcastSocketData( data as typeof data & { _brand: "socketUpdateData" }, ); }; - // Low-level. Use type-specific broadcast* method. - private async _broadcastSocketData( - data: SocketUpdateDataSource[keyof SocketUpdateDataSource] & { - _brand: "socketUpdateData"; - }, - ) { - if ( - this.room.socketInitialized && - this.room.socket && - this.room.roomID && - this.room.roomKey - ) { - const json = JSON.stringify(data); - const encoded = new TextEncoder().encode(json); - const encrypted = await encryptAESGEM(encoded, this.room.roomKey); - this.room.socket.emit( - "server-broadcast", - this.room.roomID, - encrypted.data, - encrypted.iv, - ); - } - } - private onSceneUpdated = () => { this.setState({}); }; @@ -2500,7 +2502,7 @@ export class App extends React.Component { // sometimes the pointer goes off screen return; } - this.room.socket && + this.portal.socket && this.broadcastMouseLocation({ pointerCoords, button,