mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Sync local storage state across tabs when out of sync (#4545)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
18c526d877
commit
ca89d47d4c
7 changed files with 161 additions and 37 deletions
|
@ -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]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue