diff --git a/src/excalidraw-app/app_constants.ts b/src/excalidraw-app/app_constants.ts index a06201fe4..e8a2516ce 100644 --- a/src/excalidraw-app/app_constants.ts +++ b/src/excalidraw-app/app_constants.ts @@ -7,13 +7,12 @@ export const SYNC_FULL_SCENE_INTERVAL_MS = 20000; export const SYNC_BROWSER_TABS_TIMEOUT = 50; export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day +export const PAUSE_COLLABORATION_TIMEOUT = 30000; export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB // 1 year (https://stackoverflow.com/a/25201898/927631) export const FILE_CACHE_MAX_AGE_SEC = 31536000; -export const PAUSE_COLLABORATION_TIMEOUT = 30000; - export const WS_EVENTS = { SERVER_VOLATILE: "server-volatile-broadcast", SERVER: "server-broadcast", diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index 7f6e86e09..3e13abe68 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -1,6 +1,6 @@ import throttle from "lodash.throttle"; import { PureComponent } from "react"; -import { ExcalidrawImperativeAPI } from "../../types"; +import { ExcalidrawImperativeAPI, PauseCollaborationState } from "../../types"; import { ErrorDialog } from "../../components/ErrorDialog"; import { APP_NAME, ENV, EVENT } from "../../constants"; import { ImportedDataState } from "../../data/types"; @@ -24,6 +24,7 @@ import { FIREBASE_STORAGE_PREFIXES, INITIAL_SCENE_UPDATE_TIMEOUT, LOAD_IMAGES_TIMEOUT, + PAUSE_COLLABORATION_TIMEOUT, WS_SCENE_EVENT_TYPES, SYNC_FULL_SCENE_INTERVAL_MS, } from "../app_constants"; @@ -92,8 +93,6 @@ export interface CollabAPI { onPointerUpdate: CollabInstance["onPointerUpdate"]; startCollaboration: CollabInstance["startCollaboration"]; stopCollaboration: CollabInstance["stopCollaboration"]; - pauseCollaboration: CollabInstance["pauseCollaboration"]; - resumeCollaboration: CollabInstance["resumeCollaboration"]; syncElements: CollabInstance["syncElements"]; fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; setUsername: (username: string) => void; @@ -112,6 +111,7 @@ class Collab extends PureComponent { excalidrawAPI: Props["excalidrawAPI"]; activeIntervalId: number | null; idleTimeoutId: number | null; + pauseTimeoutId: number | null; private socketInitializationTimer?: number; private lastBroadcastedOrReceivedSceneVersion: number = -1; @@ -153,6 +153,7 @@ class Collab extends PureComponent { this.excalidrawAPI = props.excalidrawAPI; this.activeIntervalId = null; this.idleTimeoutId = null; + this.pauseTimeoutId = null; } componentDidMount() { @@ -171,8 +172,6 @@ class Collab extends PureComponent { fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, stopCollaboration: this.stopCollaboration, setUsername: this.setUsername, - pauseCollaboration: this.pauseCollaboration, - resumeCollaboration: this.resumeCollaboration, isPaused: this.isPaused, }; @@ -214,6 +213,10 @@ class Collab extends PureComponent { window.clearTimeout(this.idleTimeoutId); this.idleTimeoutId = null; } + if (this.pauseTimeoutId) { + window.clearTimeout(this.pauseTimeoutId); + this.pauseTimeoutId = null; + } } isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; @@ -317,31 +320,44 @@ class Collab extends PureComponent { } }; - pauseCollaboration = (callback?: () => void) => { - if (this.portal.socket) { - this.reportIdle(); - this.portal.socket.disconnect(); - this.portal.socketInitialized = false; - this.setIsCollaborationPaused(true); + onPauseCollaborationChange = (state: PauseCollaborationState) => { + switch (state) { + case PauseCollaborationState.PAUSE: { + if (this.portal.socket) { + this.portal.socket.disconnect(); + this.portal.socketInitialized = false; + this.setIsCollaborationPaused(true); - if (callback) { - callback(); - } - } - }; - - resumeCollaboration = (callback?: () => void) => { - if (this.portal.socket) { - this.reportActive(); - this.portal.socket.connect(); - this.portal.socketInitialized = true; - this.portal.socket.emit(WS_SCENE_EVENT_TYPES.INIT); - this.portal.socket.once("client-broadcast", () => { - this.setIsCollaborationPaused(false); - if (callback) { - callback(); + this.excalidrawAPI.updateScene({ + appState: { viewModeEnabled: true }, + }); } - }); + break; + } + case PauseCollaborationState.RESUME: { + if (this.portal.socket && this.isPaused()) { + this.portal.socket.connect(); + this.portal.socketInitialized = true; + this.portal.socket.emit(WS_SCENE_EVENT_TYPES.INIT); + + this.excalidrawAPI.setToast({ + message: t("toast.reconnectRoomServer"), + duration: Infinity, + closable: false, + }); + } + break; + } + case PauseCollaborationState.SYNC: { + if (this.isPaused()) { + this.setIsCollaborationPaused(false); + + this.excalidrawAPI.updateScene({ + appState: { viewModeEnabled: false }, + }); + this.excalidrawAPI.setToast(null); + } + } } }; @@ -550,6 +566,7 @@ class Collab extends PureComponent { this.handleRemoteSceneUpdate( this.reconcileElements(decryptedData.payload.elements), ); + this.onPauseCollaborationChange(PauseCollaborationState.SYNC); break; case "MOUSE_LOCATION": { const { pointer, button, username, selectedElementIds } = @@ -737,6 +754,10 @@ class Collab extends PureComponent { window.clearInterval(this.activeIntervalId); this.activeIntervalId = null; } + this.pauseTimeoutId = window.setTimeout( + () => this.onPauseCollaborationChange(PauseCollaborationState.PAUSE), + PAUSE_COLLABORATION_TIMEOUT, + ); this.onIdleStateChange(UserIdleState.AWAY); } else { this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD); @@ -745,6 +766,11 @@ class Collab extends PureComponent { ACTIVE_THRESHOLD, ); this.onIdleStateChange(UserIdleState.ACTIVE); + if (this.pauseTimeoutId) { + window.clearTimeout(this.pauseTimeoutId); + this.onPauseCollaborationChange(PauseCollaborationState.RESUME); + this.pauseTimeoutId = null; + } } }; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 4cce2268b..860437f3c 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -46,7 +46,6 @@ import { } from "../utils"; import { FIREBASE_STORAGE_PREFIXES, - PAUSE_COLLABORATION_TIMEOUT, STORAGE_KEYS, SYNC_BROWSER_TABS_TIMEOUT, } from "./app_constants"; @@ -294,10 +293,6 @@ const ExcalidrawWrapper = () => { getInitialLibraryItems: getLibraryItemsFromStorage, }); - const pauseCollaborationTimeoutRef = useRef | null>(null); - useEffect(() => { if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) { return; @@ -477,47 +472,6 @@ const ExcalidrawWrapper = () => { ) { syncData(); } - - if (event.type === EVENT.VISIBILITY_CHANGE) { - switch (true) { - // user switches to another tab - case document.hidden && collabAPI.isCollaborating(): - if (!pauseCollaborationTimeoutRef.current) { - pauseCollaborationTimeoutRef.current = setTimeout(() => { - collabAPI.pauseCollaboration(() => - excalidrawAPI.updateScene({ - appState: { viewModeEnabled: true }, - }), - ); - }, PAUSE_COLLABORATION_TIMEOUT); - } - break; - - // user returns to the tab with Excalidraw - case !document.hidden && collabAPI.isPaused(): - excalidrawAPI.setToast({ - message: t("toast.reconnectRoomServer"), - duration: Infinity, - closable: true, - }); - - collabAPI.resumeCollaboration(() => { - excalidrawAPI.updateScene({ - appState: { viewModeEnabled: false }, - }); - excalidrawAPI.setToast(null); - }); - break; - - // user returns and timeout hasn't fired yet - case !document.hidden && Boolean(pauseCollaborationTimeoutRef): - if (pauseCollaborationTimeoutRef.current) { - clearTimeout(pauseCollaborationTimeoutRef.current); - pauseCollaborationTimeoutRef.current = null; - } - break; - } - } }; window.addEventListener(EVENT.HASHCHANGE, onHashChange, false); diff --git a/src/types.ts b/src/types.ts index 40f54831d..bfaf48bc3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -376,6 +376,12 @@ export enum UserIdleState { IDLE = "idle", } +export enum PauseCollaborationState { + PAUSE = "pause", + RESUME = "resume", + SYNC = "sync", +} + export type ExportOpts = { saveFileToDisk?: boolean; onExportToBackend?: (