|
|
|
@ -34,6 +34,7 @@ import {
|
|
|
|
|
import {
|
|
|
|
|
debounce,
|
|
|
|
|
getVersion,
|
|
|
|
|
isTestEnv,
|
|
|
|
|
preventUnload,
|
|
|
|
|
ResolvablePromise,
|
|
|
|
|
resolvablePromise,
|
|
|
|
@ -41,6 +42,8 @@ import {
|
|
|
|
|
import {
|
|
|
|
|
FIREBASE_STORAGE_PREFIXES,
|
|
|
|
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
|
|
|
STORAGE_KEYS,
|
|
|
|
|
SYNC_BROWSER_TABS_TIMEOUT,
|
|
|
|
|
} from "./app_constants";
|
|
|
|
|
import CollabWrapper, {
|
|
|
|
|
CollabAPI,
|
|
|
|
@ -50,9 +53,10 @@ import CollabWrapper, {
|
|
|
|
|
import { LanguageList } from "./components/LanguageList";
|
|
|
|
|
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
|
|
|
|
import {
|
|
|
|
|
getLibraryItemsFromStorage,
|
|
|
|
|
importFromLocalStorage,
|
|
|
|
|
importUsernameFromLocalStorage,
|
|
|
|
|
saveToLocalStorage,
|
|
|
|
|
STORAGE_KEYS,
|
|
|
|
|
} from "./data/localStorage";
|
|
|
|
|
import CustomStats from "./CustomStats";
|
|
|
|
|
import { restoreAppState, RestoredDataState } from "../data/restore";
|
|
|
|
@ -67,6 +71,10 @@ import { FileManager, updateStaleImageStatuses } from "./data/FileManager";
|
|
|
|
|
import { newElementWith } from "../element/mutateElement";
|
|
|
|
|
import { isInitializedImageElement } from "../element/typeChecks";
|
|
|
|
|
import { loadFilesFromFirebase } from "./data/firebase";
|
|
|
|
|
import {
|
|
|
|
|
isBrowserStorageStateNewer,
|
|
|
|
|
updateBrowserStateVersion,
|
|
|
|
|
} from "./data/tabSync";
|
|
|
|
|
|
|
|
|
|
const filesStore = createStore("files-db", "files-store");
|
|
|
|
|
|
|
|
|
@ -104,6 +112,11 @@ const localFileStorage = new FileManager({
|
|
|
|
|
const savedFiles = 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(
|
|
|
|
|
[...addedFiles].map(async ([id, fileData]) => {
|
|
|
|
|
try {
|
|
|
|
@ -142,7 +155,6 @@ const saveDebounced = debounce(
|
|
|
|
|
elements,
|
|
|
|
|
files,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onFilesSaved();
|
|
|
|
|
},
|
|
|
|
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
|
|
@ -278,7 +290,6 @@ const ExcalidrawWrapper = () => {
|
|
|
|
|
currentLangCode = currentLangCode[0];
|
|
|
|
|
}
|
|
|
|
|
const [langCode, setLangCode] = useState(currentLangCode);
|
|
|
|
|
|
|
|
|
|
// 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) => {
|
|
|
|
@ -415,13 +419,71 @@ const ExcalidrawWrapper = () => {
|
|
|
|
|
() => (document.title = APP_NAME),
|
|
|
|
|
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.UNLOAD, onBlur, false);
|
|
|
|
|
window.addEventListener(EVENT.BLUR, onBlur, false);
|
|
|
|
|
document.addEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
|
|
|
|
|
window.addEventListener(EVENT.FOCUS, syncData, false);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
|
|
|
|
window.removeEventListener(EVENT.UNLOAD, onBlur, false);
|
|
|
|
|
window.removeEventListener(EVENT.BLUR, onBlur, false);
|
|
|
|
|
window.removeEventListener(EVENT.FOCUS, syncData, false);
|
|
|
|
|
document.removeEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
|
|
|
|
|
clearTimeout(titleTimeout);
|
|
|
|
|
};
|
|
|
|
|
}, [collabAPI, excalidrawAPI]);
|
|
|
|
|