feat: pause collab when user switches tabs in the browser

This commit is contained in:
Arnošt Pleskot 2023-06-01 21:00:08 +02:00
parent 1badf14a93
commit addf9d71fa
No known key found for this signature in database
5 changed files with 109 additions and 16 deletions

View File

@ -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",

View File

@ -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();

View File

@ -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() {

View File

@ -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;
}
} }
}; };

View File

@ -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",