|
@@ -34,6 +34,7 @@ import {
|
|
import {
|
|
import {
|
|
debounce,
|
|
debounce,
|
|
getVersion,
|
|
getVersion,
|
|
|
|
+ isTestEnv,
|
|
preventUnload,
|
|
preventUnload,
|
|
ResolvablePromise,
|
|
ResolvablePromise,
|
|
resolvablePromise,
|
|
resolvablePromise,
|
|
@@ -41,6 +42,8 @@ import {
|
|
import {
|
|
import {
|
|
FIREBASE_STORAGE_PREFIXES,
|
|
FIREBASE_STORAGE_PREFIXES,
|
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
|
|
+ STORAGE_KEYS,
|
|
|
|
+ SYNC_BROWSER_TABS_TIMEOUT,
|
|
} from "./app_constants";
|
|
} from "./app_constants";
|
|
import CollabWrapper, {
|
|
import CollabWrapper, {
|
|
CollabAPI,
|
|
CollabAPI,
|
|
@@ -50,9 +53,10 @@ import CollabWrapper, {
|
|
import { LanguageList } from "./components/LanguageList";
|
|
import { LanguageList } from "./components/LanguageList";
|
|
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
|
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
|
import {
|
|
import {
|
|
|
|
+ getLibraryItemsFromStorage,
|
|
importFromLocalStorage,
|
|
importFromLocalStorage,
|
|
|
|
+ importUsernameFromLocalStorage,
|
|
saveToLocalStorage,
|
|
saveToLocalStorage,
|
|
- STORAGE_KEYS,
|
|
|
|
} from "./data/localStorage";
|
|
} from "./data/localStorage";
|
|
import CustomStats from "./CustomStats";
|
|
import CustomStats from "./CustomStats";
|
|
import { restoreAppState, RestoredDataState } from "../data/restore";
|
|
import { restoreAppState, RestoredDataState } from "../data/restore";
|
|
@@ -67,6 +71,10 @@ import { FileManager, updateStaleImageStatuses } from "./data/FileManager";
|
|
import { newElementWith } from "../element/mutateElement";
|
|
import { newElementWith } from "../element/mutateElement";
|
|
import { isInitializedImageElement } from "../element/typeChecks";
|
|
import { isInitializedImageElement } from "../element/typeChecks";
|
|
import { loadFilesFromFirebase } from "./data/firebase";
|
|
import { loadFilesFromFirebase } from "./data/firebase";
|
|
|
|
+import {
|
|
|
|
+ isBrowserStorageStateNewer,
|
|
|
|
+ updateBrowserStateVersion,
|
|
|
|
+} from "./data/tabSync";
|
|
|
|
|
|
const filesStore = createStore("files-db", "files-store");
|
|
const filesStore = createStore("files-db", "files-store");
|
|
|
|
|
|
@@ -104,6 +112,11 @@ const localFileStorage = new FileManager({
|
|
const savedFiles = new Map<FileId, true>();
|
|
const savedFiles = new Map<FileId, true>();
|
|
const erroredFiles = new Map<FileId, true>();
|
|
const erroredFiles = new Map<FileId, true>();
|
|
|
|
|
|
|
|
+ // before we use `storage` event synchronization, let's update the flag
|
|
|
|
+ // optimistically. Hopefully nothing fails, and an IDB read executed
|
|
|
|
+ // before an IDB write finishes will read the latest value.
|
|
|
|
+ updateBrowserStateVersion(STORAGE_KEYS.VERSION_FILES);
|
|
|
|
+
|
|
await Promise.all(
|
|
await Promise.all(
|
|
[...addedFiles].map(async ([id, fileData]) => {
|
|
[...addedFiles].map(async ([id, fileData]) => {
|
|
try {
|
|
try {
|
|
@@ -142,7 +155,6 @@ const saveDebounced = debounce(
|
|
elements,
|
|
elements,
|
|
files,
|
|
files,
|
|
});
|
|
});
|
|
-
|
|
|
|
onFilesSaved();
|
|
onFilesSaved();
|
|
},
|
|
},
|
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
@@ -278,7 +290,6 @@ const ExcalidrawWrapper = () => {
|
|
currentLangCode = currentLangCode[0];
|
|
currentLangCode = currentLangCode[0];
|
|
}
|
|
}
|
|
const [langCode, setLangCode] = useState(currentLangCode);
|
|
const [langCode, setLangCode] = useState(currentLangCode);
|
|
-
|
|
|
|
// initial state
|
|
// initial state
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
@@ -372,14 +383,7 @@ const ExcalidrawWrapper = () => {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- try {
|
|
|
|
- data.scene.libraryItems =
|
|
|
|
- JSON.parse(
|
|
|
|
- localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY) as string,
|
|
|
|
- ) || [];
|
|
|
|
- } catch (error: any) {
|
|
|
|
- console.error(error);
|
|
|
|
- }
|
|
|
|
|
|
+ data.scene.libraryItems = getLibraryItemsFromStorage();
|
|
};
|
|
};
|
|
|
|
|
|
initializeScene({ collabAPI }).then((data) => {
|
|
initializeScene({ collabAPI }).then((data) => {
|
|
@@ -415,13 +419,71 @@ const ExcalidrawWrapper = () => {
|
|
() => (document.title = APP_NAME),
|
|
() => (document.title = APP_NAME),
|
|
TITLE_TIMEOUT,
|
|
TITLE_TIMEOUT,
|
|
);
|
|
);
|
|
|
|
+
|
|
|
|
+ const syncData = debounce(() => {
|
|
|
|
+ if (isTestEnv()) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ if (!document.hidden && !collabAPI.isCollaborating()) {
|
|
|
|
+ // don't sync if local state is newer or identical to browser state
|
|
|
|
+ if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
|
|
|
+ const localDataState = importFromLocalStorage();
|
|
|
|
+ const username = importUsernameFromLocalStorage();
|
|
|
|
+ let langCode = languageDetector.detect() || defaultLang.code;
|
|
|
|
+ if (Array.isArray(langCode)) {
|
|
|
|
+ langCode = langCode[0];
|
|
|
|
+ }
|
|
|
|
+ setLangCode(langCode);
|
|
|
|
+ excalidrawAPI.updateScene({
|
|
|
|
+ ...localDataState,
|
|
|
|
+ libraryItems: getLibraryItemsFromStorage(),
|
|
|
|
+ });
|
|
|
|
+ collabAPI.setUsername(username || "");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
|
|
|
|
+ const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
|
|
|
|
+ const currFiles = excalidrawAPI.getFiles();
|
|
|
|
+ const fileIds =
|
|
|
|
+ elements?.reduce((acc, element) => {
|
|
|
|
+ if (
|
|
|
|
+ isInitializedImageElement(element) &&
|
|
|
|
+ // only load and update images that aren't already loaded
|
|
|
|
+ !currFiles[element.fileId]
|
|
|
|
+ ) {
|
|
|
|
+ return acc.concat(element.fileId);
|
|
|
|
+ }
|
|
|
|
+ return acc;
|
|
|
|
+ }, [] as FileId[]) || [];
|
|
|
|
+ if (fileIds.length) {
|
|
|
|
+ localFileStorage
|
|
|
|
+ .getFiles(fileIds)
|
|
|
|
+ .then(({ loadedFiles, erroredFiles }) => {
|
|
|
|
+ if (loadedFiles.length) {
|
|
|
|
+ excalidrawAPI.addFiles(loadedFiles);
|
|
|
|
+ }
|
|
|
|
+ updateStaleImageStatuses({
|
|
|
|
+ excalidrawAPI,
|
|
|
|
+ erroredFiles,
|
|
|
|
+ elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }, SYNC_BROWSER_TABS_TIMEOUT);
|
|
|
|
+
|
|
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
|
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
|
window.addEventListener(EVENT.UNLOAD, onBlur, false);
|
|
window.addEventListener(EVENT.UNLOAD, onBlur, false);
|
|
window.addEventListener(EVENT.BLUR, onBlur, false);
|
|
window.addEventListener(EVENT.BLUR, onBlur, false);
|
|
|
|
+ document.addEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
|
|
|
|
+ window.addEventListener(EVENT.FOCUS, syncData, false);
|
|
return () => {
|
|
return () => {
|
|
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
|
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
|
window.removeEventListener(EVENT.UNLOAD, onBlur, false);
|
|
window.removeEventListener(EVENT.UNLOAD, onBlur, false);
|
|
window.removeEventListener(EVENT.BLUR, onBlur, false);
|
|
window.removeEventListener(EVENT.BLUR, onBlur, false);
|
|
|
|
+ window.removeEventListener(EVENT.FOCUS, syncData, false);
|
|
|
|
+ document.removeEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
|
|
clearTimeout(titleTimeout);
|
|
clearTimeout(titleTimeout);
|
|
};
|
|
};
|
|
}, [collabAPI, excalidrawAPI]);
|
|
}, [collabAPI, excalidrawAPI]);
|