فهرست منبع

Add loading state (#1027)

* add loading state

* update snapshots

* add border radius

* fix comment breaking build jsx
David Luzar 5 سال پیش
والد
کامیت
cac2dda5ac

+ 25 - 1
public/index.html

@@ -130,6 +130,26 @@
       media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)"
       rel="apple-touch-startup-image"
     />
+    <style>
+      .LoadingMessage {
+        position: fixed;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        left: 0;
+        z-index: 999;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        pointer-events: none;
+      }
+      .LoadingMessage span {
+        background-color: rgba(255, 255, 255, 0.8);
+        border-radius: 5px;
+        padding: 0.8em 1.2em;
+        font-size: 1.3em;
+      }
+    </style>
     <script
       async
       src="https://www.googletagmanager.com/gtag/js?id=UA-387204-13"
@@ -151,7 +171,11 @@
     <header>
       <h1 class="visually-hidden">Excalidraw</h1>
     </header>
-    <div id="root"></div>
+    <div id="root">
+      <div class="LoadingMessage">
+        <span>Loading scene...</span>
+      </div>
+    </div>
     <aside>
       <!-- https://github.com/tholman/github-corners -->
       <svg

+ 2 - 0
src/appState.ts

@@ -5,6 +5,7 @@ export const DEFAULT_FONT = "20px Virgil";
 
 export function getDefaultAppState(): AppState {
   return {
+    isLoading: false,
     draggingElement: null,
     resizingElement: null,
     multiElement: null,
@@ -47,6 +48,7 @@ export function clearAppStateForLocalStorage(appState: AppState) {
     isResizing,
     collaborators,
     isCollaborating,
+    isLoading,
     ...exportedState
   } = appState;
   return exportedState;

+ 84 - 43
src/components/App.tsx

@@ -154,6 +154,12 @@ export class App extends React.Component<any, AppState> {
 
   actionManager: ActionManager;
   canvasOnlyActions = ["selectAll"];
+
+  public state: AppState = {
+    ...getDefaultAppState(),
+    isLoading: true,
+  };
+
   constructor(props: any) {
     super(props);
     this.actionManager = new ActionManager(
@@ -226,15 +232,22 @@ export class App extends React.Component<any, AppState> {
                 file?.type === "application/json" ||
                 file?.name.endsWith(".excalidraw")
               ) {
+                this.setState({ isLoading: true });
                 loadFromBlob(file)
                   .then(({ elements, appState }) =>
                     this.syncActionResult({
                       elements,
-                      appState,
+                      appState: {
+                        ...(appState || this.state),
+                        isLoading: false,
+                      },
                       commitToHistory: false,
                     }),
                   )
-                  .catch((error) => console.error(error));
+                  .catch((error) => {
+                    console.error(error);
+                    this.setState({ isLoading: false });
+                  });
               }
             }}
           >
@@ -270,15 +283,55 @@ export class App extends React.Component<any, AppState> {
 
   // Lifecycle
 
-  private onUnload = withBatchedUpdates(() => {
+  private onBlur = withBatchedUpdates(() => {
     isHoldingSpace = false;
     this.saveDebounced();
     this.saveDebounced.flush();
   });
 
+  private onUnload = () => {
+    this.destroySocketClient();
+    this.onBlur();
+  };
+
   private disableEvent: EventHandlerNonNull = (event) => {
     event.preventDefault();
   };
+
+  private initializeScene = async () => {
+    const searchParams = new URLSearchParams(window.location.search);
+    const id = searchParams.get("id");
+    const jsonMatch = window.location.hash.match(
+      /^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
+    );
+
+    const isCollaborationScene = getCollaborationLinkData(window.location.href);
+
+    if (!isCollaborationScene) {
+      let scene: ResolutionType<typeof loadScene> | undefined;
+      // Backwards compatibility with legacy url format
+      if (id) {
+        scene = await loadScene(id);
+      } else if (jsonMatch) {
+        scene = await loadScene(jsonMatch[1], jsonMatch[2]);
+      } else {
+        scene = await loadScene(null);
+      }
+      if (scene) {
+        this.syncActionResult(scene);
+      }
+    }
+
+    if (this.state.isLoading) {
+      this.setState({ isLoading: false });
+    }
+
+    // run this last else the `isLoading` state
+    if (isCollaborationScene) {
+      this.initializeSocketClient({ showLoadingState: true });
+    }
+  };
+
   private unmounted = false;
 
   public async componentDidMount() {
@@ -320,7 +373,7 @@ export class App extends React.Component<any, AppState> {
     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("blur", this.onBlur, false);
     window.addEventListener("dragover", this.disableEvent, false);
     window.addEventListener("drop", this.disableEvent, false);
 
@@ -338,32 +391,7 @@ export class App extends React.Component<any, AppState> {
     document.addEventListener("gestureend", this.onGestureEnd as any, false);
     window.addEventListener("beforeunload", this.beforeUnload);
 
-    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);
+    this.initializeScene();
   }
 
   public componentWillUnmount() {
@@ -383,7 +411,7 @@ export class App extends React.Component<any, AppState> {
     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("blur", this.onBlur, false);
     window.removeEventListener("dragover", this.disableEvent, false);
     window.removeEventListener("drop", this.disableEvent, false);
 
@@ -420,7 +448,7 @@ export class App extends React.Component<any, AppState> {
 
   componentDidUpdate() {
     if (this.state.isCollaborating && !this.socket) {
-      this.initializeSocketClient();
+      this.initializeSocketClient({ showLoadingState: true });
     }
     const pointerViewportCoords: {
       [id: string]: { x: number; y: number };
@@ -625,7 +653,7 @@ export class App extends React.Component<any, AppState> {
       "Excalidraw",
       await generateCollaborationLink(),
     );
-    this.initializeSocketClient();
+    this.initializeSocketClient({ showLoadingState: false });
   };
 
   destroyRoom = () => {
@@ -655,15 +683,23 @@ export class App extends React.Component<any, AppState> {
     }
   };
 
-  private initializeSocketClient = () => {
+  private initializeSocketClient = (opts: { showLoadingState: boolean }) => {
     if (this.socket) {
       return;
     }
     const roomMatch = getCollaborationLinkData(window.location.href);
     if (roomMatch) {
-      this.setState({
-        isCollaborating: true,
-      });
+      const initialize = () => {
+        this.socketInitialized = true;
+        clearTimeout(initializationTimer);
+        if (this.state.isLoading && !this.unmounted) {
+          this.setState({ isLoading: false });
+        }
+      };
+      // fallback in case you're not alone in the room but still don't receive
+      //  initial SCENE_UPDATE message
+      const initializationTimer = setTimeout(initialize, 5000);
+
       this.socket = socketIOClient(SOCKET_SERVER);
       this.roomID = roomMatch[1];
       this.roomKey = roomMatch[2];
@@ -685,7 +721,7 @@ export class App extends React.Component<any, AppState> {
           switch (decryptedData.type) {
             case "INVALID_RESPONSE":
               return;
-            case "SCENE_UPDATE":
+            case "SCENE_UPDATE": {
               const { elements: remoteElements } = decryptedData.payload;
               const restoredState = restore(remoteElements || [], null, {
                 scrollToContent: true,
@@ -765,10 +801,11 @@ export class App extends React.Component<any, AppState> {
               // right now we think this is the right tradeoff.
               history.clear();
               if (this.socketInitialized === false) {
-                this.socketInitialized = true;
+                initialize();
               }
               break;
-            case "MOUSE_LOCATION":
+            }
+            case "MOUSE_LOCATION": {
               const { socketID, pointerCoords } = decryptedData.payload;
               this.setState((state) => {
                 if (!state.collaborators.has(socketID)) {
@@ -780,6 +817,7 @@ export class App extends React.Component<any, AppState> {
                 return state;
               });
               break;
+            }
           }
         },
       );
@@ -787,7 +825,7 @@ export class App extends React.Component<any, AppState> {
         if (this.socket) {
           this.socket.off("first-in-room");
         }
-        this.socketInitialized = true;
+        initialize();
       });
       this.socket.on("room-user-change", (clients: string[]) => {
         this.setState((state) => {
@@ -808,6 +846,11 @@ export class App extends React.Component<any, AppState> {
       this.socket.on("new-user", async (socketID: string) => {
         this.broadcastSceneUpdate();
       });
+
+      this.setState({
+        isCollaborating: true,
+        isLoading: opts.showLoadingState ? true : this.state.isLoading,
+      });
     }
   };
 
@@ -867,8 +910,6 @@ export class App extends React.Component<any, AppState> {
     this.setState({});
   };
 
-  public state: AppState = getDefaultAppState();
-
   private updateCurrentCursorPosition = withBatchedUpdates(
     (event: MouseEvent) => {
       cursorX = event.x;

+ 2 - 0
src/components/LayerUI.tsx

@@ -22,6 +22,7 @@ import { MobileMenu } from "./MobileMenu";
 import { ZoomActions, SelectedShapeActions, ShapesSwitcher } from "./Actions";
 import { Section } from "./Section";
 import { RoomDialog } from "./RoomDialog";
+import { LoadingMessage } from "./LoadingMessage";
 
 interface LayerUIProps {
   actionManager: ActionManager;
@@ -105,6 +106,7 @@ export const LayerUI = React.memo(
       />
     ) : (
       <>
+        {appState.isLoading && <LoadingMessage />}
         <FixedSideContainer side="top">
           <HintViewer appState={appState} elements={elements} />
           <div className="App-menu App-menu_top">

+ 10 - 0
src/components/LoadingMessage.tsx

@@ -0,0 +1,10 @@
+import React from "react";
+
+export const LoadingMessage = () => {
+  // !! KEEP THIS IN SYNC WITH index.html !!
+  return (
+    <div className="LoadingMessage">
+      <span>Loading scene...</span>
+    </div>
+  );
+};

+ 2 - 0
src/components/MobileMenu.tsx

@@ -15,6 +15,7 @@ import { Section } from "./Section";
 import { RoomDialog } from "./RoomDialog";
 import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
 import { LockIcon } from "./LockIcon";
+import { LoadingMessage } from "./LoadingMessage";
 
 type MobileMenuProps = {
   appState: AppState;
@@ -41,6 +42,7 @@ export function MobileMenu({
 }: MobileMenuProps) {
   return (
     <>
+      {appState.isLoading && <LoadingMessage />}
       <FixedSideContainer side="top">
         <Section heading="shapes">
           {(heading) => (

+ 6 - 0
src/global.d.ts

@@ -9,3 +9,9 @@ interface Clipboard extends EventTarget {
 type Mutable<T> = {
   -readonly [P in keyof T]: T[P];
 };
+
+type ResolutionType<T extends (...args: any) => any> = T extends (
+  ...args: any
+) => Promise<infer R>
+  ? R
+  : any;

+ 82 - 41
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -18,6 +18,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -176,7 +177,7 @@ Object {
 
 exports[`regression tests alt-drag duplicates an element: [end of test] number of elements 1`] = `2`;
 
-exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `8`;
+exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `9`;
 
 exports[`regression tests arrow keys: [end of test] appState 1`] = `
 Object {
@@ -196,6 +197,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -280,7 +282,7 @@ Object {
 
 exports[`regression tests arrow keys: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests arrow keys: [end of test] number of renders 1`] = `11`;
+exports[`regression tests arrow keys: [end of test] number of renders 1`] = `12`;
 
 exports[`regression tests change the properties of a shape: [end of test] appState 1`] = `
 Object {
@@ -300,6 +302,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -520,7 +523,7 @@ Object {
 
 exports[`regression tests change the properties of a shape: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests change the properties of a shape: [end of test] number of renders 1`] = `9`;
+exports[`regression tests change the properties of a shape: [end of test] number of renders 1`] = `10`;
 
 exports[`regression tests click on an element and drag it: [dragged] appState 1`] = `
 Object {
@@ -540,6 +543,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -659,7 +663,7 @@ Object {
 
 exports[`regression tests click on an element and drag it: [dragged] number of elements 1`] = `1`;
 
-exports[`regression tests click on an element and drag it: [dragged] number of renders 1`] = `8`;
+exports[`regression tests click on an element and drag it: [dragged] number of renders 1`] = `9`;
 
 exports[`regression tests click on an element and drag it: [end of test] appState 1`] = `
 Object {
@@ -679,6 +683,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -833,7 +838,7 @@ Object {
 
 exports[`regression tests click on an element and drag it: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `11`;
+exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `12`;
 
 exports[`regression tests click to select a shape: [end of test] appState 1`] = `
 Object {
@@ -853,6 +858,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -1011,7 +1017,7 @@ Object {
 
 exports[`regression tests click to select a shape: [end of test] number of elements 1`] = `2`;
 
-exports[`regression tests click to select a shape: [end of test] number of renders 1`] = `11`;
+exports[`regression tests click to select a shape: [end of test] number of renders 1`] = `12`;
 
 exports[`regression tests click-drag to select a group: [end of test] appState 1`] = `
 Object {
@@ -1031,6 +1037,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -1281,7 +1288,7 @@ Object {
 
 exports[`regression tests click-drag to select a group: [end of test] number of elements 1`] = `3`;
 
-exports[`regression tests click-drag to select a group: [end of test] number of renders 1`] = `16`;
+exports[`regression tests click-drag to select a group: [end of test] number of renders 1`] = `17`;
 
 exports[`regression tests draw every type of shape: [end of test] appState 1`] = `
 Object {
@@ -1301,6 +1308,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -1853,7 +1861,7 @@ Object {
 
 exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `5`;
 
-exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `33`;
+exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `34`;
 
 exports[`regression tests hotkey 2 selects rectangle tool: [end of test] appState 1`] = `
 Object {
@@ -1873,6 +1881,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -1957,7 +1966,7 @@ Object {
 
 exports[`regression tests hotkey 2 selects rectangle tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey 2 selects rectangle tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey 2 selects rectangle tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey 3 selects diamond tool: [end of test] appState 1`] = `
 Object {
@@ -1977,6 +1986,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2061,7 +2071,7 @@ Object {
 
 exports[`regression tests hotkey 3 selects diamond tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey 3 selects diamond tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey 3 selects diamond tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey 4 selects ellipse tool: [end of test] appState 1`] = `
 Object {
@@ -2081,6 +2091,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2165,7 +2176,7 @@ Object {
 
 exports[`regression tests hotkey 4 selects ellipse tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey 4 selects ellipse tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey 4 selects ellipse tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey 5 selects arrow tool: [end of test] appState 1`] = `
 Object {
@@ -2185,6 +2196,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2291,7 +2303,7 @@ Object {
 
 exports[`regression tests hotkey 5 selects arrow tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey 5 selects arrow tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey 5 selects arrow tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey 6 selects line tool: [end of test] appState 1`] = `
 Object {
@@ -2311,6 +2323,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2417,7 +2430,7 @@ Object {
 
 exports[`regression tests hotkey 6 selects line tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey 6 selects line tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey 6 selects line tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey a selects arrow tool: [end of test] appState 1`] = `
 Object {
@@ -2437,6 +2450,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2543,7 +2557,7 @@ Object {
 
 exports[`regression tests hotkey a selects arrow tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey a selects arrow tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey a selects arrow tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey d selects diamond tool: [end of test] appState 1`] = `
 Object {
@@ -2563,6 +2577,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2647,7 +2662,7 @@ Object {
 
 exports[`regression tests hotkey d selects diamond tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey d selects diamond tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey d selects diamond tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey e selects ellipse tool: [end of test] appState 1`] = `
 Object {
@@ -2667,6 +2682,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2751,7 +2767,7 @@ Object {
 
 exports[`regression tests hotkey e selects ellipse tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey e selects ellipse tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey e selects ellipse tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey l selects line tool: [end of test] appState 1`] = `
 Object {
@@ -2771,6 +2787,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2877,7 +2894,7 @@ Object {
 
 exports[`regression tests hotkey l selects line tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey l selects line tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey l selects line tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests hotkey r selects rectangle tool: [end of test] appState 1`] = `
 Object {
@@ -2897,6 +2914,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -2981,7 +2999,7 @@ Object {
 
 exports[`regression tests hotkey r selects rectangle tool: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests hotkey r selects rectangle tool: [end of test] number of renders 1`] = `5`;
+exports[`regression tests hotkey r selects rectangle tool: [end of test] number of renders 1`] = `6`;
 
 exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = `
 Object {
@@ -3001,6 +3019,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "touch",
   "multiElement": null,
@@ -3029,7 +3048,7 @@ Object {
 
 exports[`regression tests pinch-to-zoom works: [end of test] number of elements 1`] = `0`;
 
-exports[`regression tests pinch-to-zoom works: [end of test] number of renders 1`] = `7`;
+exports[`regression tests pinch-to-zoom works: [end of test] number of renders 1`] = `8`;
 
 exports[`regression tests resize an element, trying every resize handle: [end of test] appState 1`] = `
 Object {
@@ -3049,6 +3068,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -3693,7 +3713,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [end of test] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [end of test] number of renders 1`] = `53`;
+exports[`regression tests resize an element, trying every resize handle: [end of test] number of renders 1`] = `54`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle ne (+5, +5)] appState 1`] = `
 Object {
@@ -3713,6 +3733,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -4042,7 +4063,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle ne (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle ne (+5, +5)] number of renders 1`] = `26`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle ne (+5, +5)] number of renders 1`] = `27`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle ne (-5, -5)] appState 1`] = `
 Object {
@@ -4062,6 +4083,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -4321,7 +4343,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle ne (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle ne (-5, -5)] number of renders 1`] = `20`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle ne (-5, -5)] number of renders 1`] = `21`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle nw (+5, +5)] appState 1`] = `
 Object {
@@ -4341,6 +4363,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -4530,7 +4553,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle nw (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle nw (+5, +5)] number of renders 1`] = `14`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle nw (+5, +5)] number of renders 1`] = `15`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle nw (-5, -5)] appState 1`] = `
 Object {
@@ -4550,6 +4573,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -4669,7 +4693,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle nw (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle nw (-5, -5)] number of renders 1`] = `8`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle nw (-5, -5)] number of renders 1`] = `9`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle se (+5, +5)] appState 1`] = `
 Object {
@@ -4689,6 +4713,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -5298,7 +5323,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle se (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle se (+5, +5)] number of renders 1`] = `50`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle se (+5, +5)] number of renders 1`] = `51`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle se (-5, -5)] appState 1`] = `
 Object {
@@ -5318,6 +5343,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -5857,7 +5883,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle se (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle se (-5, -5)] number of renders 1`] = `44`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle se (-5, -5)] number of renders 1`] = `45`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle sw (+5, +5)] appState 1`] = `
 Object {
@@ -5877,6 +5903,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -6346,7 +6373,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle sw (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle sw (+5, +5)] number of renders 1`] = `38`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle sw (+5, +5)] number of renders 1`] = `39`;
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle sw (-5, -5)] appState 1`] = `
 Object {
@@ -6366,6 +6393,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -6765,7 +6793,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [resize handle sw (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [resize handle sw (-5, -5)] number of renders 1`] = `32`;
+exports[`regression tests resize an element, trying every resize handle: [resize handle sw (-5, -5)] number of renders 1`] = `33`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (+5, +5)] appState 1`] = `
 Object {
@@ -6785,6 +6813,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -7149,7 +7178,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (+5, +5)] number of renders 1`] = `29`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (+5, +5)] number of renders 1`] = `30`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (-5, -5)] appState 1`] = `
 Object {
@@ -7169,6 +7198,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -7463,7 +7493,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (-5, -5)] number of renders 1`] = `23`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle ne (-5, -5)] number of renders 1`] = `24`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (+5, +5)] appState 1`] = `
 Object {
@@ -7483,6 +7513,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -7707,7 +7738,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (+5, +5)] number of renders 1`] = `17`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (+5, +5)] number of renders 1`] = `18`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (-5, -5)] appState 1`] = `
 Object {
@@ -7727,6 +7758,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -7881,7 +7913,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (-5, -5)] number of renders 1`] = `11`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle nw (-5, -5)] number of renders 1`] = `12`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle se (+5, +5)] appState 1`] = `
 Object {
@@ -7901,6 +7933,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -8545,7 +8578,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle se (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle se (+5, +5)] number of renders 1`] = `53`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle se (+5, +5)] number of renders 1`] = `54`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle se (-5, -5)] appState 1`] = `
 Object {
@@ -8565,6 +8598,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -9139,7 +9173,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle se (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle se (-5, -5)] number of renders 1`] = `47`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle se (-5, -5)] number of renders 1`] = `48`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (+5, +5)] appState 1`] = `
 Object {
@@ -9159,6 +9193,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -9663,7 +9698,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (+5, +5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (+5, +5)] number of renders 1`] = `41`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (+5, +5)] number of renders 1`] = `42`;
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (-5, -5)] appState 1`] = `
 Object {
@@ -9683,6 +9718,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -10117,7 +10153,7 @@ Object {
 
 exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (-5, -5)] number of elements 1`] = `1`;
 
-exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (-5, -5)] number of renders 1`] = `35`;
+exports[`regression tests resize an element, trying every resize handle: [unresize handle sw (-5, -5)] number of renders 1`] = `36`;
 
 exports[`regression tests shift-click to select a group, then drag: [end of test] appState 1`] = `
 Object {
@@ -10137,6 +10173,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -10350,7 +10387,7 @@ Object {
 
 exports[`regression tests shift-click to select a group, then drag: [end of test] number of elements 1`] = `2`;
 
-exports[`regression tests shift-click to select a group, then drag: [end of test] number of renders 1`] = `16`;
+exports[`regression tests shift-click to select a group, then drag: [end of test] number of renders 1`] = `17`;
 
 exports[`regression tests spacebar + drag scrolls the canvas: [end of test] appState 1`] = `
 Object {
@@ -10370,6 +10407,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -10396,7 +10434,7 @@ Object {
 
 exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of elements 1`] = `0`;
 
-exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of renders 1`] = `3`;
+exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of renders 1`] = `4`;
 
 exports[`regression tests two-finger scroll works: [end of test] appState 1`] = `
 Object {
@@ -10416,6 +10454,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -10444,7 +10483,7 @@ Object {
 
 exports[`regression tests two-finger scroll works: [end of test] number of elements 1`] = `0`;
 
-exports[`regression tests two-finger scroll works: [end of test] number of renders 1`] = `9`;
+exports[`regression tests two-finger scroll works: [end of test] number of renders 1`] = `10`;
 
 exports[`regression tests undo/redo drawing an element: [end of test] appState 1`] = `
 Object {
@@ -10464,6 +10503,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -10713,7 +10753,7 @@ Object {
 
 exports[`regression tests undo/redo drawing an element: [end of test] number of elements 1`] = `3`;
 
-exports[`regression tests undo/redo drawing an element: [end of test] number of renders 1`] = `16`;
+exports[`regression tests undo/redo drawing an element: [end of test] number of renders 1`] = `17`;
 
 exports[`regression tests zoom hotkeys: [end of test] appState 1`] = `
 Object {
@@ -10733,6 +10773,7 @@ Object {
   "elementType": "selection",
   "exportBackground": true,
   "isCollaborating": false,
+  "isLoading": false,
   "isResizing": false,
   "lastPointerDownWith": "mouse",
   "multiElement": null,
@@ -10759,4 +10800,4 @@ Object {
 
 exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`;
 
-exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `3`;
+exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `4`;

+ 1 - 0
src/types.ts

@@ -10,6 +10,7 @@ export type FlooredNumber = number & { _brand: "FlooredNumber" };
 export type Point = Readonly<RoughPoint>;
 
 export type AppState = {
+  isLoading: boolean;
   draggingElement: ExcalidrawElement | null;
   resizingElement: ExcalidrawElement | null;
   multiElement: ExcalidrawLinearElement | null;