diff --git a/src/components/App.tsx b/src/components/App.tsx index d1b28b455c..e637e44563 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -44,7 +44,6 @@ import { } from "../scene"; import { decryptAESGEM, - saveToLocalStorage, loadScene, loadFromBlob, SOCKET_SERVER, @@ -146,11 +145,7 @@ import { isBindableElement, } from "../element/typeChecks"; import { actionFinalize, actionDeleteSelected } from "../actions"; -import { - restoreUsernameFromLocalStorage, - saveUsernameToLocalStorage, - loadLibrary, -} from "../data/localStorage"; +import { loadLibrary } from "../data/localStorage"; import throttle from "lodash.throttle"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -310,6 +305,7 @@ class App extends React.Component { offsetLeft, } = this.state; + const { onUsernameChange } = this.props; const canvasScale = window.devicePixelRatio; const canvasWidth = canvasDOMWidth * canvasScale; @@ -335,13 +331,8 @@ class App extends React.Component { onRoomCreate={this.openPortal} onRoomDestroy={this.closePortal} onUsernameChange={(username) => { - if (this.props.onUsernameChange) { - this.props.onUsernameChange(username); - } - saveUsernameToLocalStorage(username); - this.setState({ - username, - }); + onUsernameChange && onUsernameChange(username); + this.setState({ username }); }} onLockToggle={this.toggleLock} onInsertShape={(elements) => @@ -434,8 +425,6 @@ class App extends React.Component { private onBlur = withBatchedUpdates(() => { isHoldingSpace = false; this.setState({ isBindingEnabled: true }); - this.saveDebounced(); - this.saveDebounced.flush(); }); private onUnload = () => { @@ -510,7 +499,7 @@ class App extends React.Component { let isCollaborationScene = !!getCollaborationLinkData(window.location.href); const isExternalScene = !!(id || jsonMatch || isCollaborationScene); - if (isExternalScene && !this.props.initialData) { + if (isExternalScene) { if ( this.shouldForceLoadScene(scene) || window.confirm(t("alerts.loadSceneOverridePrompt")) @@ -840,7 +829,6 @@ class App extends React.Component { if (this.state.scrolledOutside !== scrolledOutside) { this.setState({ scrolledOutside: scrolledOutside }); } - this.saveDebounced(); if ( getDrawingVersion(this.scene.getElementsIncludingDeleted()) > @@ -1433,16 +1421,6 @@ class App extends React.Component { }, ); - restoreUserName() { - const username = restoreUsernameFromLocalStorage(); - - if (username !== null) { - this.setState({ - username, - }); - } - } - // Input handling private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => { @@ -3679,10 +3657,6 @@ class App extends React.Component { this.setState({ shouldCacheIgnoreZoom: false }); }, 300); - private saveDebounced = debounce(() => { - saveToLocalStorage(this.scene.getElementsIncludingDeleted(), this.state); - }, 300); - private getCanvasOffsets() { if (this.excalidrawRef?.current) { const parentElement = this.excalidrawRef.current.parentElement; diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx index ac2936c124..8e1fd6a2e8 100644 --- a/src/components/Portal.tsx +++ b/src/components/Portal.tsx @@ -24,8 +24,6 @@ class Portal { this.socket.on("init-room", () => { if (this.socket) { this.socket.emit("join-room", this.roomID); - - this.app.restoreUserName(); } }); this.socket.on("new-user", async (_socketId: string) => { diff --git a/src/data/index.ts b/src/data/index.ts index 66d9340df0..fd6e02d574 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -18,12 +18,10 @@ import { serializeAsJSON } from "./json"; import { ExportType } from "../scene/types"; import { restore } from "./restore"; -import { restoreFromLocalStorage } from "./localStorage"; import { DataState } from "./types"; export { loadFromBlob } from "./blob"; export { saveAsJSON, loadFromJSON } from "./json"; -export { saveToLocalStorage } from "./localStorage"; const BACKEND_GET = process.env.REACT_APP_BACKEND_V1_GET_URL; @@ -233,7 +231,7 @@ export const exportToBackend = async ( } }; -export const importFromBackend = async ( +const importFromBackend = async ( id: string | null, privateKey?: string | null, ) => { @@ -246,7 +244,7 @@ export const importFromBackend = async ( ); if (!response.ok) { window.alert(t("alerts.importBackendFailed")); - return restore(elements, appState); + return { elements, appState }; } let data; if (privateKey) { @@ -277,7 +275,7 @@ export const importFromBackend = async ( window.alert(t("alerts.importBackendFailed")); console.error(error); } finally { - return restore(elements, appState); + return { elements, appState }; } }; @@ -374,9 +372,13 @@ export const loadScene = async ( if (id != null) { // the private key is used to decrypt the content from the server, take // extra care not to leak it - data = await importFromBackend(id, privateKey); + const { elements, appState } = await importFromBackend(id, privateKey); + data = restore(elements, appState); } else { - data = initialData || restoreFromLocalStorage(); + data = restore( + initialData?.elements ?? [], + initialData?.appState ?? getDefaultAppState(), + ); } return { diff --git a/src/data/localStorage.ts b/src/data/localStorage.ts index 04a391e6b9..c38db71e4b 100644 --- a/src/data/localStorage.ts +++ b/src/data/localStorage.ts @@ -62,7 +62,7 @@ export const saveUsernameToLocalStorage = (username: string) => { } }; -export const restoreUsernameFromLocalStorage = (): string | null => { +export const importUsernameFromLocalStorage = (): string | null => { try { const data = localStorage.getItem(LOCAL_STORAGE_KEY_COLLAB); if (data) { @@ -95,7 +95,7 @@ export const saveToLocalStorage = ( } }; -export const restoreFromLocalStorage = () => { +export const importFromLocalStorage = () => { let savedElements = null; let savedState = null; @@ -131,5 +131,5 @@ export const restoreFromLocalStorage = () => { // Do nothing because appState is already null } } - return restore(elements, appState); + return { elements, appState }; }; diff --git a/src/index.tsx b/src/index.tsx index 197db34dd2..614ef5cdd4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useLayoutEffect } from "react"; +import React, { useState, useLayoutEffect, useEffect } from "react"; import ReactDOM from "react-dom"; import * as Sentry from "@sentry/browser"; import * as SentryIntegrations from "@sentry/integrations"; @@ -9,6 +9,19 @@ import Excalidraw from "./excalidraw-embed/index"; import { register as registerServiceWorker } from "./serviceWorker"; import { loadFromBlob } from "./data"; +import { debounce } from "./utils"; +import { + importFromLocalStorage, + importUsernameFromLocalStorage, + saveUsernameToLocalStorage, + saveToLocalStorage, +} from "./data/localStorage"; + +import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./time_constants"; +import { DataState } from "./data/types"; +import { LoadingMessage } from "./components/LoadingMessage"; +import { ExcalidrawElement } from "./element/types"; +import { AppState } from "./types"; // On Apple mobile devices add the proprietary app icon and splashscreen markup. // No one should have to do this manually, and eventually this annoyance will @@ -60,29 +73,89 @@ Sentry.init({ window.__EXCALIDRAW_SHA__ = REACT_APP_GIT_SHA; +const saveDebounced = debounce( + (elements: readonly ExcalidrawElement[], state: AppState) => { + saveToLocalStorage(elements, state); + }, + SAVE_TO_LOCAL_STORAGE_TIMEOUT, +); + +const onUsernameChange = (username: string) => { + saveUsernameToLocalStorage(username); +}; + +const onBlur = () => { + saveDebounced.flush(); +}; + function ExcalidrawApp() { + // dimensions + // --------------------------------------------------------------------------- + const [dimensions, setDimensions] = useState({ width: window.innerWidth, height: window.innerHeight, }); - - const onResize = () => { - setDimensions({ - width: window.innerWidth, - height: window.innerHeight, - }); - }; - useLayoutEffect(() => { + const onResize = () => { + setDimensions({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); - const { width, height } = dimensions; + // initial state + // --------------------------------------------------------------------------- + + const [initialState, setInitialState] = useState<{ + data: DataState; + user: { + name: string | null; + }; + } | null>(null); + + useEffect(() => { + setInitialState({ + data: importFromLocalStorage(), + user: { + name: importUsernameFromLocalStorage(), + }, + }); + }, []); + + // blur/unload + // --------------------------------------------------------------------------- + + useEffect(() => { + window.addEventListener(EVENT.UNLOAD, onBlur, false); + window.addEventListener(EVENT.BLUR, onBlur, false); + return () => { + window.removeEventListener(EVENT.UNLOAD, onBlur, false); + window.removeEventListener(EVENT.BLUR, onBlur, false); + }; + }, []); + + // --------------------------------------------------------------------------- + + if (!initialState) { + return ; + } + return ( - + ); } diff --git a/src/time_constants.ts b/src/time_constants.ts index 27423b8f6e..5e4099d5e3 100644 --- a/src/time_constants.ts +++ b/src/time_constants.ts @@ -3,3 +3,4 @@ export const TAP_TWICE_TIMEOUT = 300; export const INITIAL_SCENE_UPDATE_TIMEOUT = 5000; export const SYNC_FULL_SCENE_INTERVAL_MS = 20000; export const TOUCH_CTX_MENU_TIMEOUT = 500; +export const SAVE_TO_LOCAL_STORAGE_TIMEOUT = 300;