|
@@ -1,36 +1,41 @@
|
|
import throttle from "lodash.throttle";
|
|
import throttle from "lodash.throttle";
|
|
import { PureComponent } from "react";
|
|
import { PureComponent } from "react";
|
|
-import { ExcalidrawImperativeAPI } from "../../src/types";
|
|
|
|
-import { ErrorDialog } from "../../src/components/ErrorDialog";
|
|
|
|
-import { APP_NAME, ENV, EVENT } from "../../src/constants";
|
|
|
|
-import { ImportedDataState } from "../../src/data/types";
|
|
|
|
|
|
+import {
|
|
|
|
+ ExcalidrawImperativeAPI,
|
|
|
|
+ SocketId,
|
|
|
|
+} from "../../packages/excalidraw/types";
|
|
|
|
+import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
|
|
|
+import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
|
|
|
+import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
|
import {
|
|
import {
|
|
ExcalidrawElement,
|
|
ExcalidrawElement,
|
|
InitializedExcalidrawImageElement,
|
|
InitializedExcalidrawImageElement,
|
|
-} from "../../src/element/types";
|
|
|
|
|
|
+} from "../../packages/excalidraw/element/types";
|
|
import {
|
|
import {
|
|
getSceneVersion,
|
|
getSceneVersion,
|
|
restoreElements,
|
|
restoreElements,
|
|
-} from "../../src/packages/excalidraw/index";
|
|
|
|
-import { Collaborator, Gesture } from "../../src/types";
|
|
|
|
|
|
+ zoomToFitBounds,
|
|
|
|
+} from "../../packages/excalidraw/index";
|
|
|
|
+import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
|
import {
|
|
import {
|
|
|
|
+ assertNever,
|
|
preventUnload,
|
|
preventUnload,
|
|
resolvablePromise,
|
|
resolvablePromise,
|
|
- withBatchedUpdates,
|
|
|
|
-} from "../../src/utils";
|
|
|
|
|
|
+ throttleRAF,
|
|
|
|
+} from "../../packages/excalidraw/utils";
|
|
import {
|
|
import {
|
|
CURSOR_SYNC_TIMEOUT,
|
|
CURSOR_SYNC_TIMEOUT,
|
|
FILE_UPLOAD_MAX_BYTES,
|
|
FILE_UPLOAD_MAX_BYTES,
|
|
FIREBASE_STORAGE_PREFIXES,
|
|
FIREBASE_STORAGE_PREFIXES,
|
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
|
LOAD_IMAGES_TIMEOUT,
|
|
LOAD_IMAGES_TIMEOUT,
|
|
- WS_SCENE_EVENT_TYPES,
|
|
|
|
|
|
+ WS_SUBTYPES,
|
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
|
|
|
+ WS_EVENTS,
|
|
} from "../app_constants";
|
|
} from "../app_constants";
|
|
import {
|
|
import {
|
|
generateCollaborationLinkData,
|
|
generateCollaborationLinkData,
|
|
getCollaborationLink,
|
|
getCollaborationLink,
|
|
- getCollabServer,
|
|
|
|
getSyncableElements,
|
|
getSyncableElements,
|
|
SocketUpdateDataSource,
|
|
SocketUpdateDataSource,
|
|
SyncableExcalidrawElement,
|
|
SyncableExcalidrawElement,
|
|
@@ -47,42 +52,48 @@ import {
|
|
saveUsernameToLocalStorage,
|
|
saveUsernameToLocalStorage,
|
|
} from "../data/localStorage";
|
|
} from "../data/localStorage";
|
|
import Portal from "./Portal";
|
|
import Portal from "./Portal";
|
|
-import RoomDialog from "./RoomDialog";
|
|
|
|
-import { t } from "../../src/i18n";
|
|
|
|
-import { UserIdleState } from "../../src/types";
|
|
|
|
-import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants";
|
|
|
|
|
|
+import { t } from "../../packages/excalidraw/i18n";
|
|
|
|
+import { UserIdleState } from "../../packages/excalidraw/types";
|
|
|
|
+import {
|
|
|
|
+ IDLE_THRESHOLD,
|
|
|
|
+ ACTIVE_THRESHOLD,
|
|
|
|
+} from "../../packages/excalidraw/constants";
|
|
import {
|
|
import {
|
|
encodeFilesForUpload,
|
|
encodeFilesForUpload,
|
|
FileManager,
|
|
FileManager,
|
|
updateStaleImageStatuses,
|
|
updateStaleImageStatuses,
|
|
} from "../data/FileManager";
|
|
} from "../data/FileManager";
|
|
-import { AbortError } from "../../src/errors";
|
|
|
|
|
|
+import { AbortError } from "../../packages/excalidraw/errors";
|
|
import {
|
|
import {
|
|
isImageElement,
|
|
isImageElement,
|
|
isInitializedImageElement,
|
|
isInitializedImageElement,
|
|
-} from "../../src/element/typeChecks";
|
|
|
|
-import { newElementWith } from "../../src/element/mutateElement";
|
|
|
|
|
|
+} from "../../packages/excalidraw/element/typeChecks";
|
|
|
|
+import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
|
import {
|
|
import {
|
|
ReconciledElements,
|
|
ReconciledElements,
|
|
reconcileElements as _reconcileElements,
|
|
reconcileElements as _reconcileElements,
|
|
} from "./reconciliation";
|
|
} from "./reconciliation";
|
|
-import { decryptData } from "../../src/data/encryption";
|
|
|
|
|
|
+import { decryptData } from "../../packages/excalidraw/data/encryption";
|
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
|
import { LocalData } from "../data/LocalData";
|
|
import { LocalData } from "../data/LocalData";
|
|
-import { atom, useAtom } from "jotai";
|
|
|
|
|
|
+import { atom } from "jotai";
|
|
import { appJotaiStore } from "../app-jotai";
|
|
import { appJotaiStore } from "../app-jotai";
|
|
|
|
+import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
|
|
|
+import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
|
|
|
+import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
|
|
|
|
|
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
|
-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);
|
|
|
|
|
|
interface CollabState {
|
|
interface CollabState {
|
|
- errorMessage: string;
|
|
|
|
|
|
+ errorMessage: string | null;
|
|
username: string;
|
|
username: string;
|
|
- activeRoomLink: string;
|
|
|
|
|
|
+ activeRoomLink: string | null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+export const activeRoomLinkAtom = atom<string | null>(null);
|
|
|
|
+
|
|
type CollabInstance = InstanceType<typeof Collab>;
|
|
type CollabInstance = InstanceType<typeof Collab>;
|
|
|
|
|
|
export interface CollabAPI {
|
|
export interface CollabAPI {
|
|
@@ -93,32 +104,33 @@ export interface CollabAPI {
|
|
stopCollaboration: CollabInstance["stopCollaboration"];
|
|
stopCollaboration: CollabInstance["stopCollaboration"];
|
|
syncElements: CollabInstance["syncElements"];
|
|
syncElements: CollabInstance["syncElements"];
|
|
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
|
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
|
|
- setUsername: (username: string) => void;
|
|
|
|
|
|
+ setUsername: CollabInstance["setUsername"];
|
|
|
|
+ getUsername: CollabInstance["getUsername"];
|
|
|
|
+ getActiveRoomLink: CollabInstance["getActiveRoomLink"];
|
|
|
|
+ setErrorMessage: CollabInstance["setErrorMessage"];
|
|
}
|
|
}
|
|
|
|
|
|
-interface PublicProps {
|
|
|
|
|
|
+interface CollabProps {
|
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
|
}
|
|
}
|
|
|
|
|
|
-type Props = PublicProps & { modalIsShown: boolean };
|
|
|
|
-
|
|
|
|
-class Collab extends PureComponent<Props, CollabState> {
|
|
|
|
|
|
+class Collab extends PureComponent<CollabProps, CollabState> {
|
|
portal: Portal;
|
|
portal: Portal;
|
|
fileManager: FileManager;
|
|
fileManager: FileManager;
|
|
- excalidrawAPI: Props["excalidrawAPI"];
|
|
|
|
|
|
+ excalidrawAPI: CollabProps["excalidrawAPI"];
|
|
activeIntervalId: number | null;
|
|
activeIntervalId: number | null;
|
|
idleTimeoutId: number | null;
|
|
idleTimeoutId: number | null;
|
|
|
|
|
|
private socketInitializationTimer?: number;
|
|
private socketInitializationTimer?: number;
|
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
|
- private collaborators = new Map<string, Collaborator>();
|
|
|
|
|
|
+ private collaborators = new Map<SocketId, Collaborator>();
|
|
|
|
|
|
- constructor(props: Props) {
|
|
|
|
|
|
+ constructor(props: CollabProps) {
|
|
super(props);
|
|
super(props);
|
|
this.state = {
|
|
this.state = {
|
|
- errorMessage: "",
|
|
|
|
|
|
+ errorMessage: null,
|
|
username: importUsernameFromLocalStorage() || "",
|
|
username: importUsernameFromLocalStorage() || "",
|
|
- activeRoomLink: "",
|
|
|
|
|
|
+ activeRoomLink: null,
|
|
};
|
|
};
|
|
this.portal = new Portal(this);
|
|
this.portal = new Portal(this);
|
|
this.fileManager = new FileManager({
|
|
this.fileManager = new FileManager({
|
|
@@ -151,12 +163,28 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
this.idleTimeoutId = null;
|
|
this.idleTimeoutId = null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private onUmmount: (() => void) | null = null;
|
|
|
|
+
|
|
componentDidMount() {
|
|
componentDidMount() {
|
|
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
|
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
|
window.addEventListener("online", this.onOfflineStatusToggle);
|
|
window.addEventListener("online", this.onOfflineStatusToggle);
|
|
window.addEventListener("offline", this.onOfflineStatusToggle);
|
|
window.addEventListener("offline", this.onOfflineStatusToggle);
|
|
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
|
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
|
|
|
|
|
|
|
+ const unsubOnUserFollow = this.excalidrawAPI.onUserFollow((payload) => {
|
|
|
|
+ this.portal.socket && this.portal.broadcastUserFollowed(payload);
|
|
|
|
+ });
|
|
|
|
+ const throttledRelayUserViewportBounds = throttleRAF(
|
|
|
|
+ this.relayVisibleSceneBounds,
|
|
|
|
+ );
|
|
|
|
+ const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() =>
|
|
|
|
+ throttledRelayUserViewportBounds(),
|
|
|
|
+ );
|
|
|
|
+ this.onUmmount = () => {
|
|
|
|
+ unsubOnUserFollow();
|
|
|
|
+ unsubOnScrollChange();
|
|
|
|
+ };
|
|
|
|
+
|
|
this.onOfflineStatusToggle();
|
|
this.onOfflineStatusToggle();
|
|
|
|
|
|
const collabAPI: CollabAPI = {
|
|
const collabAPI: CollabAPI = {
|
|
@@ -167,6 +195,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,
|
|
|
|
+ getUsername: this.getUsername,
|
|
|
|
+ getActiveRoomLink: this.getActiveRoomLink,
|
|
|
|
+ setErrorMessage: this.setErrorMessage,
|
|
};
|
|
};
|
|
|
|
|
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
|
@@ -204,6 +235,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
window.clearTimeout(this.idleTimeoutId);
|
|
window.clearTimeout(this.idleTimeoutId);
|
|
this.idleTimeoutId = null;
|
|
this.idleTimeoutId = null;
|
|
}
|
|
}
|
|
|
|
+ this.onUmmount?.();
|
|
}
|
|
}
|
|
|
|
|
|
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
|
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
|
@@ -313,9 +345,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
this.fileManager.reset();
|
|
this.fileManager.reset();
|
|
if (!opts?.isUnload) {
|
|
if (!opts?.isUnload) {
|
|
this.setIsCollaborating(false);
|
|
this.setIsCollaborating(false);
|
|
- this.setState({
|
|
|
|
- activeRoomLink: "",
|
|
|
|
- });
|
|
|
|
|
|
+ this.setActiveRoomLink(null);
|
|
this.collaborators = new Map();
|
|
this.collaborators = new Map();
|
|
this.excalidrawAPI.updateScene({
|
|
this.excalidrawAPI.updateScene({
|
|
collaborators: this.collaborators,
|
|
collaborators: this.collaborators,
|
|
@@ -356,7 +386,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
iv: Uint8Array,
|
|
iv: Uint8Array,
|
|
encryptedData: ArrayBuffer,
|
|
encryptedData: ArrayBuffer,
|
|
decryptionKey: string,
|
|
decryptionKey: string,
|
|
- ) => {
|
|
|
|
|
|
+ ): Promise<ValueOf<SocketUpdateDataSource>> => {
|
|
try {
|
|
try {
|
|
const decrypted = await decryptData(iv, encryptedData, decryptionKey);
|
|
const decrypted = await decryptData(iv, encryptedData, decryptionKey);
|
|
|
|
|
|
@@ -368,7 +398,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
window.alert(t("alerts.decryptFailed"));
|
|
window.alert(t("alerts.decryptFailed"));
|
|
console.error(error);
|
|
console.error(error);
|
|
return {
|
|
return {
|
|
- type: "INVALID_RESPONSE",
|
|
|
|
|
|
+ type: WS_SUBTYPES.INVALID_RESPONSE,
|
|
};
|
|
};
|
|
}
|
|
}
|
|
};
|
|
};
|
|
@@ -381,7 +411,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
if (!this.state.username) {
|
|
if (!this.state.username) {
|
|
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
|
|
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
|
|
const username = getRandomUsername();
|
|
const username = getRandomUsername();
|
|
- this.onUsernameChange(username);
|
|
|
|
|
|
+ this.setUsername(username);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
@@ -423,13 +453,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
|
this.fallbackInitializationHandler = fallbackInitializationHandler;
|
|
|
|
|
|
try {
|
|
try {
|
|
- const socketServerData = await getCollabServer();
|
|
|
|
-
|
|
|
|
this.portal.socket = this.portal.open(
|
|
this.portal.socket = this.portal.open(
|
|
- socketIOClient(socketServerData.url, {
|
|
|
|
- transports: socketServerData.polling
|
|
|
|
- ? ["websocket", "polling"]
|
|
|
|
- : ["websocket"],
|
|
|
|
|
|
+ socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, {
|
|
|
|
+ transports: ["websocket", "polling"],
|
|
}),
|
|
}),
|
|
roomId,
|
|
roomId,
|
|
roomKey,
|
|
roomKey,
|
|
@@ -484,9 +510,9 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
);
|
|
);
|
|
|
|
|
|
switch (decryptedData.type) {
|
|
switch (decryptedData.type) {
|
|
- case "INVALID_RESPONSE":
|
|
|
|
|
|
+ case WS_SUBTYPES.INVALID_RESPONSE:
|
|
return;
|
|
return;
|
|
- case WS_SCENE_EVENT_TYPES.INIT: {
|
|
|
|
|
|
+ case WS_SUBTYPES.INIT: {
|
|
if (!this.portal.socketInitialized) {
|
|
if (!this.portal.socketInitialized) {
|
|
this.initializeRoom({ fetchScene: false });
|
|
this.initializeRoom({ fetchScene: false });
|
|
const remoteElements = decryptedData.payload.elements;
|
|
const remoteElements = decryptedData.payload.elements;
|
|
@@ -502,42 +528,76 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
}
|
|
}
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
- case WS_SCENE_EVENT_TYPES.UPDATE:
|
|
|
|
|
|
+ case WS_SUBTYPES.UPDATE:
|
|
this.handleRemoteSceneUpdate(
|
|
this.handleRemoteSceneUpdate(
|
|
this.reconcileElements(decryptedData.payload.elements),
|
|
this.reconcileElements(decryptedData.payload.elements),
|
|
);
|
|
);
|
|
break;
|
|
break;
|
|
- case "MOUSE_LOCATION": {
|
|
|
|
|
|
+ case WS_SUBTYPES.MOUSE_LOCATION: {
|
|
const { pointer, button, username, selectedElementIds } =
|
|
const { pointer, button, username, selectedElementIds } =
|
|
decryptedData.payload;
|
|
decryptedData.payload;
|
|
|
|
+
|
|
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
|
const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
|
|
decryptedData.payload.socketId ||
|
|
decryptedData.payload.socketId ||
|
|
// @ts-ignore legacy, see #2094 (#2097)
|
|
// @ts-ignore legacy, see #2094 (#2097)
|
|
decryptedData.payload.socketID;
|
|
decryptedData.payload.socketID;
|
|
|
|
|
|
- const collaborators = new Map(this.collaborators);
|
|
|
|
- const user = collaborators.get(socketId) || {}!;
|
|
|
|
- user.pointer = pointer;
|
|
|
|
- user.button = button;
|
|
|
|
- user.selectedElementIds = selectedElementIds;
|
|
|
|
- user.username = username;
|
|
|
|
- collaborators.set(socketId, user);
|
|
|
|
|
|
+ this.updateCollaborator(socketId, {
|
|
|
|
+ pointer,
|
|
|
|
+ button,
|
|
|
|
+ selectedElementIds,
|
|
|
|
+ username,
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ case WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS: {
|
|
|
|
+ const { sceneBounds, socketId } = decryptedData.payload;
|
|
|
|
+
|
|
|
|
+ const appState = this.excalidrawAPI.getAppState();
|
|
|
|
+
|
|
|
|
+ // we're not following the user
|
|
|
|
+ // (shouldn't happen, but could be late message or bug upstream)
|
|
|
|
+ if (appState.userToFollow?.socketId !== socketId) {
|
|
|
|
+ console.warn(
|
|
|
|
+ `receiving remote client's (from ${socketId}) viewport bounds even though we're not subscribed to it!`,
|
|
|
|
+ );
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // cross-follow case, ignore updates in this case
|
|
|
|
+ if (
|
|
|
|
+ appState.userToFollow &&
|
|
|
|
+ appState.followedBy.has(appState.userToFollow.socketId)
|
|
|
|
+ ) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
this.excalidrawAPI.updateScene({
|
|
this.excalidrawAPI.updateScene({
|
|
- collaborators,
|
|
|
|
|
|
+ appState: zoomToFitBounds({
|
|
|
|
+ appState,
|
|
|
|
+ bounds: sceneBounds,
|
|
|
|
+ fitToViewport: true,
|
|
|
|
+ viewportZoomFactor: 1,
|
|
|
|
+ }).appState,
|
|
});
|
|
});
|
|
|
|
+
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
- case "IDLE_STATUS": {
|
|
|
|
|
|
+
|
|
|
|
+ case WS_SUBTYPES.IDLE_STATUS: {
|
|
const { userState, socketId, username } = decryptedData.payload;
|
|
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,
|
|
|
|
|
|
+ this.updateCollaborator(socketId, {
|
|
|
|
+ userState,
|
|
|
|
+ username,
|
|
});
|
|
});
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ default: {
|
|
|
|
+ assertNever(decryptedData, null);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
},
|
|
},
|
|
);
|
|
);
|
|
@@ -553,11 +613,20 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
scenePromise.resolve(sceneData);
|
|
scenePromise.resolve(sceneData);
|
|
});
|
|
});
|
|
|
|
|
|
|
|
+ this.portal.socket.on(
|
|
|
|
+ WS_EVENTS.USER_FOLLOW_ROOM_CHANGE,
|
|
|
|
+ (followedBy: SocketId[]) => {
|
|
|
|
+ this.excalidrawAPI.updateScene({
|
|
|
|
+ appState: { followedBy: new Set(followedBy) },
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ this.relayVisibleSceneBounds({ force: true });
|
|
|
|
+ },
|
|
|
|
+ );
|
|
|
|
+
|
|
this.initializeIdleDetector();
|
|
this.initializeIdleDetector();
|
|
|
|
|
|
- this.setState({
|
|
|
|
- activeRoomLink: window.location.href,
|
|
|
|
- });
|
|
|
|
|
|
+ this.setActiveRoomLink(window.location.href);
|
|
|
|
|
|
return scenePromise;
|
|
return scenePromise;
|
|
};
|
|
};
|
|
@@ -721,20 +790,39 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
|
|
document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange);
|
|
};
|
|
};
|
|
|
|
|
|
- setCollaborators(sockets: string[]) {
|
|
|
|
|
|
+ setCollaborators(sockets: SocketId[]) {
|
|
const collaborators: InstanceType<typeof Collab>["collaborators"] =
|
|
const collaborators: InstanceType<typeof Collab>["collaborators"] =
|
|
new Map();
|
|
new Map();
|
|
for (const socketId of sockets) {
|
|
for (const socketId of sockets) {
|
|
- if (this.collaborators.has(socketId)) {
|
|
|
|
- collaborators.set(socketId, this.collaborators.get(socketId)!);
|
|
|
|
- } else {
|
|
|
|
- collaborators.set(socketId, {});
|
|
|
|
- }
|
|
|
|
|
|
+ collaborators.set(
|
|
|
|
+ socketId,
|
|
|
|
+ Object.assign({}, this.collaborators.get(socketId), {
|
|
|
|
+ isCurrentUser: socketId === this.portal.socket?.id,
|
|
|
|
+ }),
|
|
|
|
+ );
|
|
}
|
|
}
|
|
this.collaborators = collaborators;
|
|
this.collaborators = collaborators;
|
|
this.excalidrawAPI.updateScene({ collaborators });
|
|
this.excalidrawAPI.updateScene({ collaborators });
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ updateCollaborator = (socketId: SocketId, updates: Partial<Collaborator>) => {
|
|
|
|
+ const collaborators = new Map(this.collaborators);
|
|
|
|
+ const user: Mutable<Collaborator> = Object.assign(
|
|
|
|
+ {},
|
|
|
|
+ collaborators.get(socketId),
|
|
|
|
+ updates,
|
|
|
|
+ {
|
|
|
|
+ isCurrentUser: socketId === this.portal.socket?.id,
|
|
|
|
+ },
|
|
|
|
+ );
|
|
|
|
+ collaborators.set(socketId, user);
|
|
|
|
+ this.collaborators = collaborators;
|
|
|
|
+
|
|
|
|
+ this.excalidrawAPI.updateScene({
|
|
|
|
+ collaborators,
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
|
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
|
|
this.lastBroadcastedOrReceivedSceneVersion = version;
|
|
this.lastBroadcastedOrReceivedSceneVersion = version;
|
|
};
|
|
};
|
|
@@ -760,6 +848,19 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
CURSOR_SYNC_TIMEOUT,
|
|
CURSOR_SYNC_TIMEOUT,
|
|
);
|
|
);
|
|
|
|
|
|
|
|
+ relayVisibleSceneBounds = (props?: { force: boolean }) => {
|
|
|
|
+ const appState = this.excalidrawAPI.getAppState();
|
|
|
|
+
|
|
|
|
+ if (this.portal.socket && (appState.followedBy.size > 0 || props?.force)) {
|
|
|
|
+ this.portal.broadcastVisibleSceneBounds(
|
|
|
|
+ {
|
|
|
|
+ sceneBounds: getVisibleSceneBounds(appState),
|
|
|
|
+ },
|
|
|
|
+ `follow@${this.portal.socket.id}`,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
onIdleStateChange = (userState: UserIdleState) => {
|
|
onIdleStateChange = (userState: UserIdleState) => {
|
|
this.portal.broadcastIdleChange(userState);
|
|
this.portal.broadcastIdleChange(userState);
|
|
};
|
|
};
|
|
@@ -769,7 +870,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
getSceneVersion(elements) >
|
|
getSceneVersion(elements) >
|
|
this.getLastBroadcastedOrReceivedSceneVersion()
|
|
this.getLastBroadcastedOrReceivedSceneVersion()
|
|
) {
|
|
) {
|
|
- this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.UPDATE, elements, false);
|
|
|
|
|
|
+ this.portal.broadcastScene(WS_SUBTYPES.UPDATE, elements, false);
|
|
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
|
|
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
|
|
this.queueBroadcastAllElements();
|
|
this.queueBroadcastAllElements();
|
|
}
|
|
}
|
|
@@ -782,7 +883,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
|
|
|
|
queueBroadcastAllElements = throttle(() => {
|
|
queueBroadcastAllElements = throttle(() => {
|
|
this.portal.broadcastScene(
|
|
this.portal.broadcastScene(
|
|
- WS_SCENE_EVENT_TYPES.UPDATE,
|
|
|
|
|
|
+ WS_SUBTYPES.UPDATE,
|
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
|
true,
|
|
true,
|
|
);
|
|
);
|
|
@@ -808,41 +909,31 @@ class Collab extends PureComponent<Props, CollabState> {
|
|
{ leading: false },
|
|
{ leading: false },
|
|
);
|
|
);
|
|
|
|
|
|
- handleClose = () => {
|
|
|
|
- appJotaiStore.set(collabDialogShownAtom, false);
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
setUsername = (username: string) => {
|
|
setUsername = (username: string) => {
|
|
this.setState({ username });
|
|
this.setState({ username });
|
|
|
|
+ saveUsernameToLocalStorage(username);
|
|
};
|
|
};
|
|
|
|
|
|
- onUsernameChange = (username: string) => {
|
|
|
|
- this.setUsername(username);
|
|
|
|
- saveUsernameToLocalStorage(username);
|
|
|
|
|
|
+ getUsername = () => this.state.username;
|
|
|
|
+
|
|
|
|
+ setActiveRoomLink = (activeRoomLink: string | null) => {
|
|
|
|
+ this.setState({ activeRoomLink });
|
|
|
|
+ appJotaiStore.set(activeRoomLinkAtom, activeRoomLink);
|
|
};
|
|
};
|
|
|
|
|
|
- render() {
|
|
|
|
- const { username, errorMessage, activeRoomLink } = this.state;
|
|
|
|
|
|
+ getActiveRoomLink = () => this.state.activeRoomLink;
|
|
|
|
+
|
|
|
|
+ setErrorMessage = (errorMessage: string | null) => {
|
|
|
|
+ this.setState({ errorMessage });
|
|
|
|
+ };
|
|
|
|
|
|
- const { modalIsShown } = this.props;
|
|
|
|
|
|
+ render() {
|
|
|
|
+ const { errorMessage } = this.state;
|
|
|
|
|
|
return (
|
|
return (
|
|
<>
|
|
<>
|
|
- {modalIsShown && (
|
|
|
|
- <RoomDialog
|
|
|
|
- handleClose={this.handleClose}
|
|
|
|
- activeRoomLink={activeRoomLink}
|
|
|
|
- username={username}
|
|
|
|
- onUsernameChange={this.onUsernameChange}
|
|
|
|
- onRoomCreate={() => this.startCollaboration(null)}
|
|
|
|
- onRoomDestroy={this.stopCollaboration}
|
|
|
|
- setErrorMessage={(errorMessage) => {
|
|
|
|
- this.setState({ errorMessage });
|
|
|
|
- }}
|
|
|
|
- />
|
|
|
|
- )}
|
|
|
|
- {errorMessage && (
|
|
|
|
- <ErrorDialog onClose={() => this.setState({ errorMessage: "" })}>
|
|
|
|
|
|
+ {errorMessage != null && (
|
|
|
|
+ <ErrorDialog onClose={() => this.setState({ errorMessage: null })}>
|
|
{errorMessage}
|
|
{errorMessage}
|
|
</ErrorDialog>
|
|
</ErrorDialog>
|
|
)}
|
|
)}
|
|
@@ -861,11 +952,6 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
|
window.collab = window.collab || ({} as Window["collab"]);
|
|
window.collab = window.collab || ({} as Window["collab"]);
|
|
}
|
|
}
|
|
|
|
|
|
-const _Collab: React.FC<PublicProps> = (props) => {
|
|
|
|
- const [collabDialogShown] = useAtom(collabDialogShownAtom);
|
|
|
|
- return <Collab {...props} modalIsShown={collabDialogShown} />;
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
-export default _Collab;
|
|
|
|
|
|
+export default Collab;
|
|
|
|
|
|
export type TCollabClass = Collab;
|
|
export type TCollabClass = Collab;
|