feat: pause collab when user switches tabs in the browser
This commit is contained in:
parent
1badf14a93
commit
addf9d71fa
@ -12,6 +12,8 @@ export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
|
|||||||
// 1 year (https://stackoverflow.com/a/25201898/927631)
|
// 1 year (https://stackoverflow.com/a/25201898/927631)
|
||||||
export const FILE_CACHE_MAX_AGE_SEC = 31536000;
|
export const FILE_CACHE_MAX_AGE_SEC = 31536000;
|
||||||
|
|
||||||
|
export const PAUSE_COLLABORATION_TIMEOUT = 30000;
|
||||||
|
|
||||||
export const WS_EVENTS = {
|
export const WS_EVENTS = {
|
||||||
SERVER_VOLATILE: "server-volatile-broadcast",
|
SERVER_VOLATILE: "server-volatile-broadcast",
|
||||||
SERVER: "server-broadcast",
|
SERVER: "server-broadcast",
|
||||||
|
@ -76,6 +76,7 @@ export const collabAPIAtom = atom<CollabAPI | null>(null);
|
|||||||
export const collabDialogShownAtom = atom(false);
|
export const collabDialogShownAtom = atom(false);
|
||||||
export const isCollaboratingAtom = atom(false);
|
export const isCollaboratingAtom = atom(false);
|
||||||
export const isOfflineAtom = atom(false);
|
export const isOfflineAtom = atom(false);
|
||||||
|
export const isCollaborationPausedAtom = atom(false);
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
@ -91,9 +92,12 @@ export interface CollabAPI {
|
|||||||
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
||||||
startCollaboration: CollabInstance["startCollaboration"];
|
startCollaboration: CollabInstance["startCollaboration"];
|
||||||
stopCollaboration: CollabInstance["stopCollaboration"];
|
stopCollaboration: CollabInstance["stopCollaboration"];
|
||||||
|
pauseCollaboration: CollabInstance["pauseCollaboration"];
|
||||||
|
resumeCollaboration: CollabInstance["resumeCollaboration"];
|
||||||
syncElements: CollabInstance["syncElements"];
|
syncElements: CollabInstance["syncElements"];
|
||||||
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
||||||
setUsername: (username: string) => void;
|
setUsername: (username: string) => void;
|
||||||
|
isPaused: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublicProps {
|
interface PublicProps {
|
||||||
@ -167,6 +171,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
|
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
|
||||||
stopCollaboration: this.stopCollaboration,
|
stopCollaboration: this.stopCollaboration,
|
||||||
setUsername: this.setUsername,
|
setUsername: this.setUsername,
|
||||||
|
pauseCollaboration: this.pauseCollaboration,
|
||||||
|
resumeCollaboration: this.resumeCollaboration,
|
||||||
|
isPaused: this.isPaused,
|
||||||
};
|
};
|
||||||
|
|
||||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
@ -310,6 +317,37 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pauseCollaboration = (callback?: () => void) => {
|
||||||
|
if (this.portal.socket) {
|
||||||
|
this.reportIdle();
|
||||||
|
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.setIsCollaborationPaused(false);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
isPaused = () => appJotaiStore.get(isCollaborationPausedAtom)!;
|
||||||
|
|
||||||
|
setIsCollaborationPaused = (isPaused: boolean) => {
|
||||||
|
appJotaiStore.set(isCollaborationPausedAtom, isPaused);
|
||||||
|
};
|
||||||
|
|
||||||
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
private destroySocketClient = (opts?: { isUnload: boolean }) => {
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
||||||
this.portal.close();
|
this.portal.close();
|
||||||
|
@ -37,6 +37,29 @@ class Portal {
|
|||||||
this.roomId = id;
|
this.roomId = id;
|
||||||
this.roomKey = key;
|
this.roomKey = key;
|
||||||
|
|
||||||
|
this.initializeSocketListeners();
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.queueFileUpload.flush();
|
||||||
|
this.socket.close();
|
||||||
|
this.socket = null;
|
||||||
|
this.roomId = null;
|
||||||
|
this.roomKey = null;
|
||||||
|
this.socketInitialized = false;
|
||||||
|
this.broadcastedElementVersions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSocketListeners() {
|
||||||
|
if (!this.socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize socket listeners
|
// Initialize socket listeners
|
||||||
this.socket.on("init-room", () => {
|
this.socket.on("init-room", () => {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
@ -54,21 +77,6 @@ class Portal {
|
|||||||
this.socket.on("room-user-change", (clients: string[]) => {
|
this.socket.on("room-user-change", (clients: string[]) => {
|
||||||
this.collab.setCollaborators(clients);
|
this.collab.setCollaborators(clients);
|
||||||
});
|
});
|
||||||
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
if (!this.socket) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.queueFileUpload.flush();
|
|
||||||
this.socket.close();
|
|
||||||
this.socket = null;
|
|
||||||
this.roomId = null;
|
|
||||||
this.roomKey = null;
|
|
||||||
this.socketInitialized = false;
|
|
||||||
this.broadcastedElementVersions = new Map();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isOpen() {
|
isOpen() {
|
||||||
|
@ -46,6 +46,7 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
|
PAUSE_COLLABORATION_TIMEOUT,
|
||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
SYNC_BROWSER_TABS_TIMEOUT,
|
||||||
} from "./app_constants";
|
} from "./app_constants";
|
||||||
@ -293,6 +294,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
getInitialLibraryItems: getLibraryItemsFromStorage,
|
getInitialLibraryItems: getLibraryItemsFromStorage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pauseCollaborationTimeoutRef = useRef<ReturnType<
|
||||||
|
typeof setTimeout
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
||||||
return;
|
return;
|
||||||
@ -471,6 +476,45 @@ const ExcalidrawWrapper = () => {
|
|||||||
event.type === EVENT.FOCUS
|
event.type === EVENT.FOCUS
|
||||||
) {
|
) {
|
||||||
syncData();
|
syncData();
|
||||||
|
|
||||||
|
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: 100000,
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -411,7 +411,8 @@
|
|||||||
"fileSavedToFilename": "Saved to {filename}",
|
"fileSavedToFilename": "Saved to {filename}",
|
||||||
"canvas": "canvas",
|
"canvas": "canvas",
|
||||||
"selection": "selection",
|
"selection": "selection",
|
||||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor"
|
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
|
||||||
|
"reconnectRoomServer": "Reconnecting to server"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "Transparent",
|
"transparent": "Transparent",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user