Kaynağa Gözat

App mitosis begins (#1047)

Kent Beck 5 yıl önce
ebeveyn
işleme
ba3cec8d0d
1 değiştirilmiş dosya ile 472 ekleme ve 466 silme
  1. 472 466
      src/components/App.tsx

+ 472 - 466
src/components/App.tsx

@@ -167,6 +167,84 @@ export class App extends React.Component<any, AppState> {
     this.actionManager.registerAction(createRedoAction(history));
   }
 
+  public render() {
+    const canvasDOMWidth = window.innerWidth;
+    const canvasDOMHeight = window.innerHeight;
+
+    const canvasScale = window.devicePixelRatio;
+
+    const canvasWidth = canvasDOMWidth * canvasScale;
+    const canvasHeight = canvasDOMHeight * canvasScale;
+
+    return (
+      <div className="container">
+        <LayerUI
+          canvas={this.canvas}
+          appState={this.state}
+          setAppState={this.setAppState}
+          actionManager={this.actionManager}
+          elements={globalSceneState.getAllElements().filter(element => {
+            return !element.isDeleted;
+          })}
+          setElements={this.setElements}
+          language={getLanguage()}
+          onRoomCreate={this.createRoom}
+          onRoomDestroy={this.destroyRoom}
+          onToggleLock={this.toggleLock}
+        />
+        <main>
+          <canvas
+            id="canvas"
+            style={{
+              width: canvasDOMWidth,
+              height: canvasDOMHeight,
+            }}
+            width={canvasWidth}
+            height={canvasHeight}
+            ref={canvas => {
+              // canvas is null when unmounting
+              if (canvas !== null) {
+                this.canvas = canvas;
+                this.rc = rough.canvas(this.canvas);
+
+                this.canvas.addEventListener("wheel", this.handleWheel, {
+                  passive: false,
+                });
+              } else {
+                this.canvas?.removeEventListener("wheel", this.handleWheel);
+              }
+            }}
+            onContextMenu={this.handleCanvasContextMenu}
+            onPointerDown={this.handleCanvasPointerDown}
+            onDoubleClick={this.handleCanvasDoubleClick}
+            onPointerMove={this.handleCanvasPointerMove}
+            onPointerUp={this.removePointer}
+            onPointerCancel={this.removePointer}
+            onDrop={event => {
+              const file = event.dataTransfer.files[0];
+              if (
+                file?.type === "application/json" ||
+                file?.name.endsWith(".excalidraw")
+              ) {
+                loadFromBlob(file)
+                  .then(({ elements, appState }) =>
+                    this.syncActionResult({
+                      elements,
+                      appState,
+                      commitToHistory: false,
+                    }),
+                  )
+                  .catch(error => console.error(error));
+              }
+            }}
+          >
+            {t("labels.drawingCanvas")}
+          </canvas>
+        </main>
+      </div>
+    );
+  }
+
   private syncActionResult = withBatchedUpdates((res: ActionResult) => {
     if (this.unmounted) {
       return;
@@ -190,37 +268,374 @@ export class App extends React.Component<any, AppState> {
     }
   });
 
-  private onCut = withBatchedUpdates((event: ClipboardEvent) => {
-    if (isWritableElement(event.target)) {
-      return;
-    }
-    copyToAppClipboard(globalSceneState.getAllElements(), this.state);
-    const { elements: nextElements, appState } = deleteSelectedElements(
-      globalSceneState.getAllElements(),
-      this.state,
+  // Lifecycle
+
+  private onUnload = withBatchedUpdates(() => {
+    isHoldingSpace = false;
+    this.saveDebounced();
+    this.saveDebounced.flush();
+  });
+
+  private disableEvent: EventHandlerNonNull = event => {
+    event.preventDefault();
+  };
+  private unmounted = false;
+
+  public async componentDidMount() {
+    if (
+      process.env.NODE_ENV === "test" ||
+      process.env.NODE_ENV === "development"
+    ) {
+      const setState = this.setState.bind(this);
+      Object.defineProperties(window.h, {
+        state: {
+          configurable: true,
+          get: () => {
+            return this.state;
+          },
+        },
+        setState: {
+          configurable: true,
+          value: (...args: Parameters<typeof setState>) => {
+            return this.setState(...args);
+          },
+        },
+      });
+    }
+
+    this.removeSceneCallback = globalSceneState.addCallback(
+      this.onSceneUpdated,
+    );
+
+    document.addEventListener("copy", this.onCopy);
+    document.addEventListener("paste", this.pasteFromClipboard);
+    document.addEventListener("cut", this.onCut);
+
+    document.addEventListener("keydown", this.onKeyDown, false);
+    document.addEventListener("keyup", this.onKeyUp, { passive: true });
+    document.addEventListener("mousemove", this.updateCurrentCursorPosition);
+    window.addEventListener("resize", this.onResize, false);
+    window.addEventListener("unload", this.onUnload, false);
+    window.addEventListener("blur", this.onUnload, false);
+    window.addEventListener("dragover", this.disableEvent, false);
+    window.addEventListener("drop", this.disableEvent, false);
+
+    // Safari-only desktop pinch zoom
+    document.addEventListener(
+      "gesturestart",
+      this.onGestureStart as any,
+      false,
+    );
+    document.addEventListener(
+      "gesturechange",
+      this.onGestureChange as any,
+      false,
+    );
+    document.addEventListener("gestureend", this.onGestureEnd as any, false);
+
+    const searchParams = new URLSearchParams(window.location.search);
+    const id = searchParams.get("id");
+
+    if (id) {
+      // Backwards compatibility with legacy url format
+      const scene = await loadScene(id);
+      this.syncActionResult(scene);
+    }
+
+    const jsonMatch = window.location.hash.match(
+      /^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
+    );
+    if (jsonMatch) {
+      const scene = await loadScene(jsonMatch[1], jsonMatch[2]);
+      this.syncActionResult(scene);
+      return;
+    }
+
+    const roomMatch = getCollaborationLinkData(window.location.href);
+    if (roomMatch) {
+      this.initializeSocketClient();
+      return;
+    }
+    const scene = await loadScene(null);
+    this.syncActionResult(scene);
+
+    window.addEventListener("beforeunload", this.beforeUnload);
+  }
+
+  public componentWillUnmount() {
+    this.unmounted = true;
+    this.removeSceneCallback!();
+
+    document.removeEventListener("copy", this.onCopy);
+    document.removeEventListener("paste", this.pasteFromClipboard);
+    document.removeEventListener("cut", this.onCut);
+
+    document.removeEventListener("keydown", this.onKeyDown, false);
+    document.removeEventListener(
+      "mousemove",
+      this.updateCurrentCursorPosition,
+      false,
+    );
+    document.removeEventListener("keyup", this.onKeyUp);
+    window.removeEventListener("resize", this.onResize, false);
+    window.removeEventListener("unload", this.onUnload, false);
+    window.removeEventListener("blur", this.onUnload, false);
+    window.removeEventListener("dragover", this.disableEvent, false);
+    window.removeEventListener("drop", this.disableEvent, false);
+
+    document.removeEventListener(
+      "gesturestart",
+      this.onGestureStart as any,
+      false,
+    );
+    document.removeEventListener(
+      "gesturechange",
+      this.onGestureChange as any,
+      false,
+    );
+    document.removeEventListener("gestureend", this.onGestureEnd as any, false);
+    window.removeEventListener("beforeunload", this.beforeUnload);
+  }
+  private onResize = withBatchedUpdates(() => {
+    globalSceneState
+      .getAllElements()
+      .forEach(element => invalidateShapeForElement(element));
+    this.setState({});
+  });
+
+  private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
+    if (
+      this.state.isCollaborating &&
+      hasNonDeletedElements(globalSceneState.getAllElements())
+    ) {
+      event.preventDefault();
+      // NOTE: modern browsers no longer allow showing a custom message here
+      event.returnValue = "";
+    }
+  });
+
+  componentDidUpdate() {
+    if (this.state.isCollaborating && !this.socket) {
+      this.initializeSocketClient();
+    }
+    const pointerViewportCoords: {
+      [id: string]: { x: number; y: number };
+    } = {};
+    this.state.collaborators.forEach((user, socketID) => {
+      if (!user.pointer) {
+        return;
+      }
+      pointerViewportCoords[socketID] = sceneCoordsToViewportCoords(
+        {
+          sceneX: user.pointer.x,
+          sceneY: user.pointer.y,
+        },
+        this.state,
+        this.canvas,
+        window.devicePixelRatio,
+      );
+    });
+    const { atLeastOneVisibleElement, scrollBars } = renderScene(
+      globalSceneState.getAllElements(),
+      this.state,
+      this.state.selectionElement,
+      window.devicePixelRatio,
+      this.rc!,
+      this.canvas!,
+      {
+        scrollX: this.state.scrollX,
+        scrollY: this.state.scrollY,
+        viewBackgroundColor: this.state.viewBackgroundColor,
+        zoom: this.state.zoom,
+        remotePointerViewportCoords: pointerViewportCoords,
+      },
+      {
+        renderOptimizations: true,
+      },
+    );
+    if (scrollBars) {
+      currentScrollBars = scrollBars;
+    }
+    const scrolledOutside =
+      !atLeastOneVisibleElement &&
+      hasNonDeletedElements(globalSceneState.getAllElements());
+    if (this.state.scrolledOutside !== scrolledOutside) {
+      this.setState({ scrolledOutside: scrolledOutside });
+    }
+    this.saveDebounced();
+
+    if (
+      getDrawingVersion(globalSceneState.getAllElements()) >
+      this.lastBroadcastedOrReceivedSceneVersion
+    ) {
+      this.broadcastSceneUpdate();
+    }
+
+    history.record(this.state, globalSceneState.getAllElements());
+  }
+
+  // Copy/paste
+
+  private onCut = withBatchedUpdates((event: ClipboardEvent) => {
+    if (isWritableElement(event.target)) {
+      return;
+    }
+    copyToAppClipboard(globalSceneState.getAllElements(), this.state);
+    const { elements: nextElements, appState } = deleteSelectedElements(
+      globalSceneState.getAllElements(),
+      this.state,
+    );
+    globalSceneState.replaceAllElements(nextElements);
+    history.resumeRecording();
+    this.setState({ ...appState });
+    event.preventDefault();
+  });
+
+  private onCopy = withBatchedUpdates((event: ClipboardEvent) => {
+    if (isWritableElement(event.target)) {
+      return;
+    }
+    copyToAppClipboard(globalSceneState.getAllElements(), this.state);
+    event.preventDefault();
+  });
+  private copyToAppClipboard = () => {
+    copyToAppClipboard(globalSceneState.getAllElements(), this.state);
+  };
+
+  private copyToClipboardAsPng = () => {
+    const selectedElements = getSelectedElements(
+      globalSceneState.getAllElements(),
+      this.state,
+    );
+    exportCanvas(
+      "clipboard",
+      selectedElements.length
+        ? selectedElements
+        : globalSceneState.getAllElements(),
+      this.state,
+      this.canvas!,
+      this.state,
+    );
+  };
+
+  private pasteFromClipboard = withBatchedUpdates(
+    async (event: ClipboardEvent | null) => {
+      // #686
+      const target = document.activeElement;
+      const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
+      if (
+        // if no ClipboardEvent supplied, assume we're pasting via contextMenu
+        //  thus these checks don't make sense
+        !event ||
+        (elementUnderCursor instanceof HTMLCanvasElement &&
+          !isWritableElement(target))
+      ) {
+        const data = await getClipboardContent(event);
+        if (data.elements) {
+          this.addElementsFromPaste(data.elements);
+        } else if (data.text) {
+          const { x, y } = viewportCoordsToSceneCoords(
+            { clientX: cursorX, clientY: cursorY },
+            this.state,
+            this.canvas,
+            window.devicePixelRatio,
+          );
+
+          const element = newTextElement({
+            x: x,
+            y: y,
+            strokeColor: this.state.currentItemStrokeColor,
+            backgroundColor: this.state.currentItemBackgroundColor,
+            fillStyle: this.state.currentItemFillStyle,
+            strokeWidth: this.state.currentItemStrokeWidth,
+            roughness: this.state.currentItemRoughness,
+            opacity: this.state.currentItemOpacity,
+            text: data.text,
+            font: this.state.currentItemFont,
+          });
+
+          globalSceneState.replaceAllElements([
+            ...globalSceneState.getAllElements(),
+            element,
+          ]);
+          this.setState({ selectedElementIds: { [element.id]: true } });
+          history.resumeRecording();
+        }
+        this.selectShapeTool("selection");
+        event?.preventDefault();
+      }
+    },
+  );
+
+  private addElementsFromPaste = (
+    clipboardElements: readonly ExcalidrawElement[],
+  ) => {
+    const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements);
+
+    const elementsCenterX = distance(minX, maxX) / 2;
+    const elementsCenterY = distance(minY, maxY) / 2;
+
+    const { x, y } = viewportCoordsToSceneCoords(
+      { clientX: cursorX, clientY: cursorY },
+      this.state,
+      this.canvas,
+      window.devicePixelRatio,
+    );
+
+    const dx = x - elementsCenterX;
+    const dy = y - elementsCenterY;
+
+    const newElements = clipboardElements.map(element =>
+      duplicateElement(element, {
+        x: element.x + dx - minX,
+        y: element.y + dy - minY,
+      }),
+    );
+
+    globalSceneState.replaceAllElements([
+      ...globalSceneState.getAllElements(),
+      ...newElements,
+    ]);
+    history.resumeRecording();
+    this.setState({
+      selectedElementIds: newElements.reduce((map, element) => {
+        map[element.id] = true;
+        return map;
+      }, {} as any),
+    });
+  };
+
+  // Collaboration
+
+  setAppState = (obj: any) => {
+    this.setState(obj);
+  };
+
+  removePointer = (event: React.PointerEvent<HTMLElement>) => {
+    gesture.pointers.delete(event.pointerId);
+  };
+
+  createRoom = async () => {
+    window.history.pushState(
+      {},
+      "Excalidraw",
+      await generateCollaborationLink(),
     );
-    globalSceneState.replaceAllElements(nextElements);
-    history.resumeRecording();
-    this.setState({ ...appState });
-    event.preventDefault();
-  });
-
-  private onCopy = withBatchedUpdates((event: ClipboardEvent) => {
-    if (isWritableElement(event.target)) {
-      return;
-    }
-    copyToAppClipboard(globalSceneState.getAllElements(), this.state);
-    event.preventDefault();
-  });
+    this.initializeSocketClient();
+  };
 
-  private onUnload = withBatchedUpdates(() => {
-    isHoldingSpace = false;
-    this.saveDebounced();
-    this.saveDebounced.flush();
-  });
+  destroyRoom = () => {
+    window.history.pushState({}, "Excalidraw", window.location.origin);
+    this.destroySocketClient();
+  };
 
-  private disableEvent: EventHandlerNonNull = event => {
-    event.preventDefault();
+  toggleLock = () => {
+    this.setState(prevState => ({
+      elementLocked: !prevState.elementLocked,
+      elementType: prevState.elementLocked
+        ? "selection"
+        : prevState.elementType,
+    }));
   };
 
   private destroySocketClient = () => {
@@ -448,132 +863,8 @@ export class App extends React.Component<any, AppState> {
     this.setState({});
   };
 
-  private unmounted = false;
-  public async componentDidMount() {
-    if (
-      process.env.NODE_ENV === "test" ||
-      process.env.NODE_ENV === "development"
-    ) {
-      const setState = this.setState.bind(this);
-      Object.defineProperties(window.h, {
-        state: {
-          configurable: true,
-          get: () => {
-            return this.state;
-          },
-        },
-        setState: {
-          configurable: true,
-          value: (...args: Parameters<typeof setState>) => {
-            return this.setState(...args);
-          },
-        },
-      });
-    }
-
-    this.removeSceneCallback = globalSceneState.addCallback(
-      this.onSceneUpdated,
-    );
-
-    document.addEventListener("copy", this.onCopy);
-    document.addEventListener("paste", this.pasteFromClipboard);
-    document.addEventListener("cut", this.onCut);
-
-    document.addEventListener("keydown", this.onKeyDown, false);
-    document.addEventListener("keyup", this.onKeyUp, { passive: true });
-    document.addEventListener("mousemove", this.updateCurrentCursorPosition);
-    window.addEventListener("resize", this.onResize, false);
-    window.addEventListener("unload", this.onUnload, false);
-    window.addEventListener("blur", this.onUnload, false);
-    window.addEventListener("dragover", this.disableEvent, false);
-    window.addEventListener("drop", this.disableEvent, false);
-
-    // Safari-only desktop pinch zoom
-    document.addEventListener(
-      "gesturestart",
-      this.onGestureStart as any,
-      false,
-    );
-    document.addEventListener(
-      "gesturechange",
-      this.onGestureChange as any,
-      false,
-    );
-    document.addEventListener("gestureend", this.onGestureEnd as any, false);
-
-    const searchParams = new URLSearchParams(window.location.search);
-    const id = searchParams.get("id");
-
-    if (id) {
-      // Backwards compatibility with legacy url format
-      const scene = await loadScene(id);
-      this.syncActionResult(scene);
-    }
-
-    const jsonMatch = window.location.hash.match(
-      /^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
-    );
-    if (jsonMatch) {
-      const scene = await loadScene(jsonMatch[1], jsonMatch[2]);
-      this.syncActionResult(scene);
-      return;
-    }
-
-    const roomMatch = getCollaborationLinkData(window.location.href);
-    if (roomMatch) {
-      this.initializeSocketClient();
-      return;
-    }
-    const scene = await loadScene(null);
-    this.syncActionResult(scene);
-
-    window.addEventListener("beforeunload", this.beforeUnload);
-  }
-
-  public componentWillUnmount() {
-    this.unmounted = true;
-    this.removeSceneCallback!();
-
-    document.removeEventListener("copy", this.onCopy);
-    document.removeEventListener("paste", this.pasteFromClipboard);
-    document.removeEventListener("cut", this.onCut);
-
-    document.removeEventListener("keydown", this.onKeyDown, false);
-    document.removeEventListener(
-      "mousemove",
-      this.updateCurrentCursorPosition,
-      false,
-    );
-    document.removeEventListener("keyup", this.onKeyUp);
-    window.removeEventListener("resize", this.onResize, false);
-    window.removeEventListener("unload", this.onUnload, false);
-    window.removeEventListener("blur", this.onUnload, false);
-    window.removeEventListener("dragover", this.disableEvent, false);
-    window.removeEventListener("drop", this.disableEvent, false);
-
-    document.removeEventListener(
-      "gesturestart",
-      this.onGestureStart as any,
-      false,
-    );
-    document.removeEventListener(
-      "gesturechange",
-      this.onGestureChange as any,
-      false,
-    );
-    document.removeEventListener("gestureend", this.onGestureEnd as any, false);
-    window.removeEventListener("beforeunload", this.beforeUnload);
-  }
-
   public state: AppState = getDefaultAppState();
 
-  private onResize = withBatchedUpdates(() => {
-    globalSceneState
-      .getAllElements()
-      .forEach(element => invalidateShapeForElement(element));
-    this.setState({});
-  });
-
   private updateCurrentCursorPosition = withBatchedUpdates(
     (event: MouseEvent) => {
       cursorX = event.x;
@@ -581,6 +872,8 @@ export class App extends React.Component<any, AppState> {
     },
   );
 
+  // Input handling
+
   private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => {
     if (
       (isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
@@ -628,104 +921,35 @@ export class App extends React.Component<any, AppState> {
     } else if (
       !event.ctrlKey &&
       !event.altKey &&
-      !event.metaKey &&
-      this.state.draggingElement === null
-    ) {
-      if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
-        this.selectShapeTool(shape);
-      } else if (event.key === "q") {
-        this.toggleLock();
-      }
-    }
-    if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
-      isHoldingSpace = true;
-      document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
-    }
-  });
-
-  private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
-    if (event.key === KEYS.SPACE) {
-      if (this.state.elementType === "selection") {
-        resetCursor();
-      } else {
-        document.documentElement.style.cursor =
-          this.state.elementType === "text"
-            ? CURSOR_TYPE.TEXT
-            : CURSOR_TYPE.CROSSHAIR;
-        this.setState({ selectedElementIds: {} });
-      }
-      isHoldingSpace = false;
-    }
-  });
-
-  private copyToAppClipboard = () => {
-    copyToAppClipboard(globalSceneState.getAllElements(), this.state);
-  };
-
-  private copyToClipboardAsPng = () => {
-    const selectedElements = getSelectedElements(
-      globalSceneState.getAllElements(),
-      this.state,
-    );
-    exportCanvas(
-      "clipboard",
-      selectedElements.length
-        ? selectedElements
-        : globalSceneState.getAllElements(),
-      this.state,
-      this.canvas!,
-      this.state,
-    );
-  };
-
-  private pasteFromClipboard = withBatchedUpdates(
-    async (event: ClipboardEvent | null) => {
-      // #686
-      const target = document.activeElement;
-      const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
-      if (
-        // if no ClipboardEvent supplied, assume we're pasting via contextMenu
-        //  thus these checks don't make sense
-        !event ||
-        (elementUnderCursor instanceof HTMLCanvasElement &&
-          !isWritableElement(target))
-      ) {
-        const data = await getClipboardContent(event);
-        if (data.elements) {
-          this.addElementsFromPaste(data.elements);
-        } else if (data.text) {
-          const { x, y } = viewportCoordsToSceneCoords(
-            { clientX: cursorX, clientY: cursorY },
-            this.state,
-            this.canvas,
-            window.devicePixelRatio,
-          );
-
-          const element = newTextElement({
-            x: x,
-            y: y,
-            strokeColor: this.state.currentItemStrokeColor,
-            backgroundColor: this.state.currentItemBackgroundColor,
-            fillStyle: this.state.currentItemFillStyle,
-            strokeWidth: this.state.currentItemStrokeWidth,
-            roughness: this.state.currentItemRoughness,
-            opacity: this.state.currentItemOpacity,
-            text: data.text,
-            font: this.state.currentItemFont,
-          });
+      !event.metaKey &&
+      this.state.draggingElement === null
+    ) {
+      if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
+        this.selectShapeTool(shape);
+      } else if (event.key === "q") {
+        this.toggleLock();
+      }
+    }
+    if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
+      isHoldingSpace = true;
+      document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
+    }
+  });
 
-          globalSceneState.replaceAllElements([
-            ...globalSceneState.getAllElements(),
-            element,
-          ]);
-          this.setState({ selectedElementIds: { [element.id]: true } });
-          history.resumeRecording();
-        }
-        this.selectShapeTool("selection");
-        event?.preventDefault();
+  private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
+    if (event.key === KEYS.SPACE) {
+      if (this.state.elementType === "selection") {
+        resetCursor();
+      } else {
+        document.documentElement.style.cursor =
+          this.state.elementType === "text"
+            ? CURSOR_TYPE.TEXT
+            : CURSOR_TYPE.CROSSHAIR;
+        this.setState({ selectedElementIds: {} });
       }
-    },
-  );
+      isHoldingSpace = false;
+    }
+  });
 
   private selectShapeTool(elementType: AppState["elementType"]) {
     if (!isHoldingSpace) {
@@ -759,119 +983,10 @@ export class App extends React.Component<any, AppState> {
     gesture.initialScale = null;
   });
 
-  setAppState = (obj: any) => {
-    this.setState(obj);
-  };
-
-  removePointer = (event: React.PointerEvent<HTMLElement>) => {
-    gesture.pointers.delete(event.pointerId);
-  };
-
-  createRoom = async () => {
-    window.history.pushState(
-      {},
-      "Excalidraw",
-      await generateCollaborationLink(),
-    );
-    this.initializeSocketClient();
-  };
-
-  destroyRoom = () => {
-    window.history.pushState({}, "Excalidraw", window.location.origin);
-    this.destroySocketClient();
-  };
-
-  toggleLock = () => {
-    this.setState(prevState => ({
-      elementLocked: !prevState.elementLocked,
-      elementType: prevState.elementLocked
-        ? "selection"
-        : prevState.elementType,
-    }));
-  };
-
   private setElements = (elements: readonly ExcalidrawElement[]) => {
     globalSceneState.replaceAllElements(elements);
   };
 
-  public render() {
-    const canvasDOMWidth = window.innerWidth;
-    const canvasDOMHeight = window.innerHeight;
-
-    const canvasScale = window.devicePixelRatio;
-
-    const canvasWidth = canvasDOMWidth * canvasScale;
-    const canvasHeight = canvasDOMHeight * canvasScale;
-
-    return (
-      <div className="container">
-        <LayerUI
-          canvas={this.canvas}
-          appState={this.state}
-          setAppState={this.setAppState}
-          actionManager={this.actionManager}
-          elements={globalSceneState.getAllElements().filter(element => {
-            return !element.isDeleted;
-          })}
-          setElements={this.setElements}
-          language={getLanguage()}
-          onRoomCreate={this.createRoom}
-          onRoomDestroy={this.destroyRoom}
-          onToggleLock={this.toggleLock}
-        />
-        <main>
-          <canvas
-            id="canvas"
-            style={{
-              width: canvasDOMWidth,
-              height: canvasDOMHeight,
-            }}
-            width={canvasWidth}
-            height={canvasHeight}
-            ref={canvas => {
-              // canvas is null when unmounting
-              if (canvas !== null) {
-                this.canvas = canvas;
-                this.rc = rough.canvas(this.canvas);
-
-                this.canvas.addEventListener("wheel", this.handleWheel, {
-                  passive: false,
-                });
-              } else {
-                this.canvas?.removeEventListener("wheel", this.handleWheel);
-              }
-            }}
-            onContextMenu={this.handleCanvasContextMenu}
-            onPointerDown={this.handleCanvasPointerDown}
-            onDoubleClick={this.handleCanvasDoubleClick}
-            onPointerMove={this.handleCanvasPointerMove}
-            onPointerUp={this.removePointer}
-            onPointerCancel={this.removePointer}
-            onDrop={event => {
-              const file = event.dataTransfer.files[0];
-              if (
-                file?.type === "application/json" ||
-                file?.name.endsWith(".excalidraw")
-              ) {
-                loadFromBlob(file)
-                  .then(({ elements, appState }) =>
-                    this.syncActionResult({
-                      elements,
-                      appState,
-                      commitToHistory: false,
-                    }),
-                  )
-                  .catch(error => console.error(error));
-              }
-            }}
-          >
-            {t("labels.drawingCanvas")}
-          </canvas>
-        </main>
-      </div>
-    );
-  }
-
   private handleCanvasDoubleClick = (
     event: React.MouseEvent<HTMLCanvasElement>,
   ) => {
@@ -2287,55 +2402,6 @@ export class App extends React.Component<any, AppState> {
     }));
   });
 
-  private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
-    if (
-      this.state.isCollaborating &&
-      hasNonDeletedElements(globalSceneState.getAllElements())
-    ) {
-      event.preventDefault();
-      // NOTE: modern browsers no longer allow showing a custom message here
-      event.returnValue = "";
-    }
-  });
-
-  private addElementsFromPaste = (
-    clipboardElements: readonly ExcalidrawElement[],
-  ) => {
-    const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements);
-
-    const elementsCenterX = distance(minX, maxX) / 2;
-    const elementsCenterY = distance(minY, maxY) / 2;
-
-    const { x, y } = viewportCoordsToSceneCoords(
-      { clientX: cursorX, clientY: cursorY },
-      this.state,
-      this.canvas,
-      window.devicePixelRatio,
-    );
-
-    const dx = x - elementsCenterX;
-    const dy = y - elementsCenterY;
-
-    const newElements = clipboardElements.map(element =>
-      duplicateElement(element, {
-        x: element.x + dx - minX,
-        y: element.y + dy - minY,
-      }),
-    );
-
-    globalSceneState.replaceAllElements([
-      ...globalSceneState.getAllElements(),
-      ...newElements,
-    ]);
-    history.resumeRecording();
-    this.setState({
-      selectedElementIds: newElements.reduce((map, element) => {
-        map[element.id] = true;
-        return map;
-      }, {} as any),
-    });
-  };
-
   private getTextWysiwygSnappedToCenterPosition(x: number, y: number) {
     const elementClickedInside = getElementContainingPosition(
       globalSceneState.getAllElements(),
@@ -2378,66 +2444,6 @@ export class App extends React.Component<any, AppState> {
   private saveDebounced = debounce(() => {
     saveToLocalStorage(globalSceneState.getAllElements(), this.state);
   }, 300);
-
-  componentDidUpdate() {
-    if (this.state.isCollaborating && !this.socket) {
-      this.initializeSocketClient();
-    }
-    const pointerViewportCoords: {
-      [id: string]: { x: number; y: number };
-    } = {};
-    this.state.collaborators.forEach((user, socketID) => {
-      if (!user.pointer) {
-        return;
-      }
-      pointerViewportCoords[socketID] = sceneCoordsToViewportCoords(
-        {
-          sceneX: user.pointer.x,
-          sceneY: user.pointer.y,
-        },
-        this.state,
-        this.canvas,
-        window.devicePixelRatio,
-      );
-    });
-    const { atLeastOneVisibleElement, scrollBars } = renderScene(
-      globalSceneState.getAllElements(),
-      this.state,
-      this.state.selectionElement,
-      window.devicePixelRatio,
-      this.rc!,
-      this.canvas!,
-      {
-        scrollX: this.state.scrollX,
-        scrollY: this.state.scrollY,
-        viewBackgroundColor: this.state.viewBackgroundColor,
-        zoom: this.state.zoom,
-        remotePointerViewportCoords: pointerViewportCoords,
-      },
-      {
-        renderOptimizations: true,
-      },
-    );
-    if (scrollBars) {
-      currentScrollBars = scrollBars;
-    }
-    const scrolledOutside =
-      !atLeastOneVisibleElement &&
-      hasNonDeletedElements(globalSceneState.getAllElements());
-    if (this.state.scrolledOutside !== scrolledOutside) {
-      this.setState({ scrolledOutside: scrolledOutside });
-    }
-    this.saveDebounced();
-
-    if (
-      getDrawingVersion(globalSceneState.getAllElements()) >
-      this.lastBroadcastedOrReceivedSceneVersion
-    ) {
-      this.broadcastSceneUpdate();
-    }
-
-    history.record(this.state, globalSceneState.getAllElements());
-  }
 }
 
 // -----------------------------------------------------------------------------