|
@@ -38,11 +38,14 @@ import Portal from "./Portal";
|
|
import RoomDialog from "./RoomDialog";
|
|
import RoomDialog from "./RoomDialog";
|
|
import { createInverseContext } from "../../createInverseContext";
|
|
import { createInverseContext } from "../../createInverseContext";
|
|
import { t } from "../../i18n";
|
|
import { t } from "../../i18n";
|
|
|
|
+import { UserIdleState } from "./types";
|
|
|
|
+import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
|
|
|
|
|
interface CollabState {
|
|
interface CollabState {
|
|
modalIsShown: boolean;
|
|
modalIsShown: boolean;
|
|
errorMessage: string;
|
|
errorMessage: string;
|
|
username: string;
|
|
username: string;
|
|
|
|
+ userState: UserIdleState;
|
|
activeRoomLink: string;
|
|
activeRoomLink: string;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -52,6 +55,7 @@ export interface CollabAPI {
|
|
/** function so that we can access the latest value from stale callbacks */
|
|
/** function so that we can access the latest value from stale callbacks */
|
|
isCollaborating: () => boolean;
|
|
isCollaborating: () => boolean;
|
|
username: CollabState["username"];
|
|
username: CollabState["username"];
|
|
|
|
+ userState: CollabState["userState"];
|
|
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
|
onPointerUpdate: CollabInstance["onPointerUpdate"];
|
|
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
|
initializeSocketClient: CollabInstance["initializeSocketClient"];
|
|
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
|
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
|
|
@@ -78,6 +82,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
portal: Portal;
|
|
portal: Portal;
|
|
excalidrawAPI: Props["excalidrawAPI"];
|
|
excalidrawAPI: Props["excalidrawAPI"];
|
|
isCollaborating: boolean = false;
|
|
isCollaborating: boolean = false;
|
|
|
|
+ activeIntervalId: number | null;
|
|
|
|
+ idleTimeoutId: number | null;
|
|
|
|
|
|
private socketInitializationTimer?: NodeJS.Timeout;
|
|
private socketInitializationTimer?: NodeJS.Timeout;
|
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
|
@@ -89,10 +95,13 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
modalIsShown: false,
|
|
modalIsShown: false,
|
|
errorMessage: "",
|
|
errorMessage: "",
|
|
username: importUsernameFromLocalStorage() || "",
|
|
username: importUsernameFromLocalStorage() || "",
|
|
|
|
+ userState: UserIdleState.ACTIVE,
|
|
activeRoomLink: "",
|
|
activeRoomLink: "",
|
|
};
|
|
};
|
|
this.portal = new Portal(this);
|
|
this.portal = new Portal(this);
|
|
this.excalidrawAPI = props.excalidrawAPI;
|
|
this.excalidrawAPI = props.excalidrawAPI;
|
|
|
|
+ this.activeIntervalId = null;
|
|
|
|
+ this.idleTimeoutId = null;
|
|
}
|
|
}
|
|
|
|
|
|
componentDidMount() {
|
|
componentDidMount() {
|
|
@@ -116,6 +125,19 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
componentWillUnmount() {
|
|
componentWillUnmount() {
|
|
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
|
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
|
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
|
|
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
|
|
|
|
+ window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
|
|
|
|
+ window.removeEventListener(
|
|
|
|
+ EVENT.VISIBILITY_CHANGE,
|
|
|
|
+ this.onVisibilityChange,
|
|
|
|
+ );
|
|
|
|
+ if (this.activeIntervalId) {
|
|
|
|
+ window.clearInterval(this.activeIntervalId);
|
|
|
|
+ this.activeIntervalId = null;
|
|
|
|
+ }
|
|
|
|
+ if (this.idleTimeoutId) {
|
|
|
|
+ window.clearTimeout(this.idleTimeoutId);
|
|
|
|
+ this.idleTimeoutId = null;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
private onUnload = () => {
|
|
private onUnload = () => {
|
|
@@ -318,6 +340,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
});
|
|
});
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
|
|
+ case "IDLE_STATUS": {
|
|
|
|
+ const { userState, socketId, username } = decryptedData.payload;
|
|
|
|
+ const collaborators = new Map(this.collaborators);
|
|
|
|
+ const user = collaborators.get(socketId) || {}!;
|
|
|
|
+ user.userState = userState;
|
|
|
|
+ user.username = username;
|
|
|
|
+ this.excalidrawAPI.updateScene({
|
|
|
|
+ collaborators,
|
|
|
|
+ });
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
},
|
|
},
|
|
);
|
|
);
|
|
@@ -330,6 +363,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
scenePromise.resolve(null);
|
|
scenePromise.resolve(null);
|
|
});
|
|
});
|
|
|
|
|
|
|
|
+ this.initializeIdleDetector();
|
|
|
|
+
|
|
this.setState({
|
|
this.setState({
|
|
activeRoomLink: window.location.href,
|
|
activeRoomLink: window.location.href,
|
|
});
|
|
});
|
|
@@ -398,7 +433,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
// Avoid broadcasting to the rest of the collaborators the scene
|
|
// Avoid broadcasting to the rest of the collaborators the scene
|
|
// we just received!
|
|
// we just received!
|
|
// Note: this needs to be set before updating the scene as it
|
|
// Note: this needs to be set before updating the scene as it
|
|
- // syncronously calls render.
|
|
|
|
|
|
+ // synchronously calls render.
|
|
this.setLastBroadcastedOrReceivedSceneVersion(getSceneVersion(newElements));
|
|
this.setLastBroadcastedOrReceivedSceneVersion(getSceneVersion(newElements));
|
|
|
|
|
|
return newElements as ReconciledElements;
|
|
return newElements as ReconciledElements;
|
|
@@ -427,6 +462,58 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
this.excalidrawAPI.history.clear();
|
|
this.excalidrawAPI.history.clear();
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ private onPointerMove = () => {
|
|
|
|
+ if (this.idleTimeoutId) {
|
|
|
|
+ window.clearTimeout(this.idleTimeoutId);
|
|
|
|
+ this.idleTimeoutId = null;
|
|
|
|
+ }
|
|
|
|
+ this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
|
|
|
|
+ if (!this.activeIntervalId) {
|
|
|
|
+ this.activeIntervalId = window.setInterval(
|
|
|
|
+ this.reportActive,
|
|
|
|
+ ACTIVE_THRESHOLD,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private onVisibilityChange = () => {
|
|
|
|
+ if (document.hidden) {
|
|
|
|
+ if (this.idleTimeoutId) {
|
|
|
|
+ window.clearTimeout(this.idleTimeoutId);
|
|
|
|
+ this.idleTimeoutId = null;
|
|
|
|
+ }
|
|
|
|
+ if (this.activeIntervalId) {
|
|
|
|
+ window.clearInterval(this.activeIntervalId);
|
|
|
|
+ this.activeIntervalId = null;
|
|
|
|
+ }
|
|
|
|
+ this.onIdleStateChange(UserIdleState.AWAY);
|
|
|
|
+ } else {
|
|
|
|
+ this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
|
|
|
|
+ this.activeIntervalId = window.setInterval(
|
|
|
|
+ this.reportActive,
|
|
|
|
+ ACTIVE_THRESHOLD,
|
|
|
|
+ );
|
|
|
|
+ this.onIdleStateChange(UserIdleState.ACTIVE);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private reportIdle = () => {
|
|
|
|
+ this.onIdleStateChange(UserIdleState.IDLE);
|
|
|
|
+ if (this.activeIntervalId) {
|
|
|
|
+ window.clearInterval(this.activeIntervalId);
|
|
|
|
+ this.activeIntervalId = null;
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private reportActive = () => {
|
|
|
|
+ this.onIdleStateChange(UserIdleState.ACTIVE);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private initializeIdleDetector = () => {
|
|
|
|
+ document.addEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
|
|
|
|
+ document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
|
|
|
|
+ };
|
|
|
|
+
|
|
setCollaborators(sockets: string[]) {
|
|
setCollaborators(sockets: string[]) {
|
|
this.setState((state) => {
|
|
this.setState((state) => {
|
|
const collaborators: InstanceType<
|
|
const collaborators: InstanceType<
|
|
@@ -466,6 +553,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|
this.portal.broadcastMouseLocation(payload);
|
|
this.portal.broadcastMouseLocation(payload);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ onIdleStateChange = (userState: UserIdleState) => {
|
|
|
|
+ this.setState({ userState });
|
|
|
|
+ this.portal.broadcastIdleChange(userState);
|
|
|
|
+ };
|
|
|
|
+
|
|
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
|
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
|
if (
|
|
if (
|
|
getSceneVersion(elements) >
|
|
getSceneVersion(elements) >
|