diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index bb62a0e96..0f7f1fca1 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -482,7 +482,7 @@ const ExcalidrawWrapper = () => { collabAPI?.isCollaborating() && !isCollaborationLink(window.location.href) ) { - collabAPI.stopCollaboration(false); + await collabAPI.stopCollaboration(false); } excalidrawAPI.updateScene({ appState: { isLoading: true } }); @@ -980,9 +980,9 @@ const ExcalidrawWrapper = () => { "exit", "collaboration", ], - perform: () => { + perform: async () => { if (collabAPI) { - collabAPI.stopCollaboration(); + await collabAPI.stopCollaboration(); if (!collabAPI.isCollaborating()) { setShareDialogState({ isOpen: false }); } diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 98c66e425..f5b488c06 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -352,13 +352,14 @@ class Collab extends PureComponent { } }; - stopCollaboration = (keepRemoteState = true) => { + stopCollaboration = async (keepRemoteState = true) => { this.queueBroadcastAllElements.cancel(); this.queueSaveToFirebase.cancel(); this.loadImageFiles.cancel(); this.resetErrorIndicator(true); - this.saveCollabRoomToFirebase( + // Ensure we save the latest state before stopping collaboration + await this.saveCollabRoomToFirebase( getSyncableElements( this.excalidrawAPI.getSceneElementsIncludingDeleted(), ), @@ -379,6 +380,9 @@ class Collab extends PureComponent { // that could have been saved in other tabs while we were collaborating resetBrowserStateVersions(); + // Ensure all pending changes are saved before pushing new state + this.queueSaveToFirebase.flush(); + window.history.pushState({}, APP_NAME, window.location.origin); this.destroySocketClient(); @@ -950,9 +954,9 @@ class Collab extends PureComponent { }, SYNC_FULL_SCENE_INTERVAL_MS); queueSaveToFirebase = throttle( - () => { + async () => { if (this.portal.socketInitialized) { - this.saveCollabRoomToFirebase( + await this.saveCollabRoomToFirebase( getSyncableElements( this.excalidrawAPI.getSceneElementsIncludingDeleted(), ), diff --git a/excalidraw-app/share/ShareDialog.tsx b/excalidraw-app/share/ShareDialog.tsx index 884d64680..c2a688b8d 100644 --- a/excalidraw-app/share/ShareDialog.tsx +++ b/excalidraw-app/share/ShareDialog.tsx @@ -165,10 +165,9 @@ const ActiveRoomDialog = ({ icon={playerStopFilledIcon} onClick={() => { trackEvent("share", "room closed"); - collabAPI.stopCollaboration(); - if (!collabAPI.isCollaborating()) { + collabAPI.stopCollaboration().then(() => { handleClose(); - } + }); }} /> diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index ddb071981..3fb08c944 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -2333,7 +2333,7 @@ class App extends React.Component { this.setState((state) => ({ ...getDefaultAppState(), isLoading: opts?.resetLoadingState ? false : state.isLoading, - theme: this.state.theme, + theme: state.theme, })); this.resetStore(); this.resetHistory(); @@ -2956,8 +2956,21 @@ class App extends React.Component { // init, which would trigger onChange with empty elements, which would then // override whatever is in localStorage currently. if (!this.state.isLoading) { - this.props.onChange?.(elements, this.state, this.files); - this.onChangeEmitter.trigger(elements, this.state, this.files); + // Only trigger onChange when there are actual changes to elements or important app state + const prevElements = this.scene.getElementsIncludingDeleted(); + const hasElementChanges = elements.length !== prevElements.length || + elements.some((element, index) => element !== prevElements[index]); + + const hasImportantStateChanges = + this.state.selectedElementIds !== prevState.selectedElementIds || + this.state.width !== prevState.width || + this.state.height !== prevState.height || + this.state.editingTextElement !== prevState.editingTextElement; + + if (hasElementChanges || hasImportantStateChanges) { + this.props.onChange?.(elements, this.state, this.files); + this.onChangeEmitter.trigger(elements, this.state, this.files); + } } }