123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829 |
- import throttle from "lodash.throttle";
- import { PureComponent } from "react";
- import { ExcalidrawImperativeAPI } from "../../types";
- import { ErrorDialog } from "../../components/ErrorDialog";
- import { APP_NAME, ENV, EVENT } from "../../constants";
- import { ImportedDataState } from "../../data/types";
- import {
- ExcalidrawElement,
- InitializedExcalidrawImageElement,
- } from "../../element/types";
- import { getSceneVersion } from "../../packages/excalidraw/index";
- import { Collaborator, Gesture } from "../../types";
- import {
- getFrame,
- preventUnload,
- resolvablePromise,
- withBatchedUpdates,
- } from "../../utils";
- import {
- CURSOR_SYNC_TIMEOUT,
- FILE_UPLOAD_MAX_BYTES,
- FIREBASE_STORAGE_PREFIXES,
- INITIAL_SCENE_UPDATE_TIMEOUT,
- LOAD_IMAGES_TIMEOUT,
- SCENE,
- STORAGE_KEYS,
- SYNC_FULL_SCENE_INTERVAL_MS,
- } from "../app_constants";
- import {
- generateCollaborationLinkData,
- getCollaborationLink,
- getCollabServer,
- SocketUpdateDataSource,
- } from "../data";
- import {
- isSavedToFirebase,
- loadFilesFromFirebase,
- loadFromFirebase,
- saveFilesToFirebase,
- saveToFirebase,
- } from "../data/firebase";
- import {
- importUsernameFromLocalStorage,
- saveUsernameToLocalStorage,
- } from "../data/localStorage";
- import Portal from "./Portal";
- import RoomDialog from "./RoomDialog";
- import { createInverseContext } from "../../createInverseContext";
- import { t } from "../../i18n";
- import { UserIdleState } from "../../types";
- import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
- import { trackEvent } from "../../analytics";
- import { isInvisiblySmallElement } from "../../element";
- import {
- encodeFilesForUpload,
- FileManager,
- updateStaleImageStatuses,
- } from "../data/FileManager";
- import { AbortError } from "../../errors";
- import {
- isImageElement,
- isInitializedImageElement,
- } from "../../element/typeChecks";
- import { newElementWith } from "../../element/mutateElement";
- import {
- ReconciledElements,
- reconcileElements as _reconcileElements,
- } from "./reconciliation";
- import { decryptData } from "../../data/encryption";
- import { resetBrowserStateVersions } from "../data/tabSync";
- import { LocalData } from "../data/LocalData";
- interface CollabState {
- modalIsShown: boolean;
- errorMessage: string;
- username: string;
- userState: UserIdleState;
- activeRoomLink: string;
- }
- type CollabInstance = InstanceType<typeof CollabWrapper>;
- export interface CollabAPI {
- /** function so that we can access the latest value from stale callbacks */
- isCollaborating: () => boolean;
- username: CollabState["username"];
- userState: CollabState["userState"];
- onPointerUpdate: CollabInstance["onPointerUpdate"];
- initializeSocketClient: CollabInstance["initializeSocketClient"];
- onCollabButtonClick: CollabInstance["onCollabButtonClick"];
- broadcastElements: CollabInstance["broadcastElements"];
- fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
- setUsername: (username: string) => void;
- }
- interface Props {
- excalidrawAPI: ExcalidrawImperativeAPI;
- onRoomClose?: () => void;
- }
- const {
- Context: CollabContext,
- Consumer: CollabContextConsumer,
- Provider: CollabContextProvider,
- } = createInverseContext<{ api: CollabAPI | null }>({ api: null });
- export { CollabContext, CollabContextConsumer };
- class CollabWrapper extends PureComponent<Props, CollabState> {
- portal: Portal;
- fileManager: FileManager;
- excalidrawAPI: Props["excalidrawAPI"];
- activeIntervalId: number | null;
- idleTimeoutId: number | null;
- // marked as private to ensure we don't change it outside this class
- private _isCollaborating: boolean = false;
- private socketInitializationTimer?: number;
- private lastBroadcastedOrReceivedSceneVersion: number = -1;
- private collaborators = new Map<string, Collaborator>();
- constructor(props: Props) {
- super(props);
- this.state = {
- modalIsShown: false,
- errorMessage: "",
- username: importUsernameFromLocalStorage() || "",
- userState: UserIdleState.ACTIVE,
- activeRoomLink: "",
- };
- this.portal = new Portal(this);
- this.fileManager = new FileManager({
- getFiles: async (fileIds) => {
- const { roomId, roomKey } = this.portal;
- if (!roomId || !roomKey) {
- throw new AbortError();
- }
- return loadFilesFromFirebase(`files/rooms/${roomId}`, roomKey, fileIds);
- },
- saveFiles: async ({ addedFiles }) => {
- const { roomId, roomKey } = this.portal;
- if (!roomId || !roomKey) {
- throw new AbortError();
- }
- return saveFilesToFirebase({
- prefix: `${FIREBASE_STORAGE_PREFIXES.collabFiles}/${roomId}`,
- files: await encodeFilesForUpload({
- files: addedFiles,
- encryptionKey: roomKey,
- maxBytes: FILE_UPLOAD_MAX_BYTES,
- }),
- });
- },
- });
- this.excalidrawAPI = props.excalidrawAPI;
- this.activeIntervalId = null;
- this.idleTimeoutId = null;
- }
- componentDidMount() {
- window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
- window.addEventListener(EVENT.UNLOAD, this.onUnload);
- if (
- process.env.NODE_ENV === ENV.TEST ||
- process.env.NODE_ENV === ENV.DEVELOPMENT
- ) {
- window.collab = window.collab || ({} as Window["collab"]);
- Object.defineProperties(window, {
- collab: {
- configurable: true,
- value: this,
- },
- });
- }
- }
- componentWillUnmount() {
- window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
- 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;
- }
- }
- isCollaborating = () => this._isCollaborating;
- private onUnload = () => {
- this.destroySocketClient({ isUnload: true });
- };
- private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
- const syncableElements = this.getSyncableElements(
- this.getSceneElementsIncludingDeleted(),
- );
- if (
- this._isCollaborating &&
- (this.fileManager.shouldPreventUnload(syncableElements) ||
- !isSavedToFirebase(this.portal, syncableElements))
- ) {
- // this won't run in time if user decides to leave the site, but
- // the purpose is to run in immediately after user decides to stay
- this.saveCollabRoomToFirebase(syncableElements);
- preventUnload(event);
- }
- if (this.isCollaborating || this.portal.roomId) {
- try {
- localStorage?.setItem(
- STORAGE_KEYS.LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
- JSON.stringify({
- timestamp: Date.now(),
- room: this.portal.roomId,
- }),
- );
- } catch {}
- }
- });
- saveCollabRoomToFirebase = async (
- syncableElements: readonly ExcalidrawElement[] = this.getSyncableElements(
- this.excalidrawAPI.getSceneElementsIncludingDeleted(),
- ),
- ) => {
- try {
- await saveToFirebase(this.portal, syncableElements);
- } catch (error: any) {
- console.error(error);
- }
- };
- openPortal = async () => {
- trackEvent("share", "room creation", `ui (${getFrame()})`);
- return this.initializeSocketClient(null);
- };
- closePortal = () => {
- this.queueBroadcastAllElements.cancel();
- this.loadImageFiles.cancel();
- this.saveCollabRoomToFirebase();
- if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
- // hack to ensure that we prefer we disregard any new browser state
- // that could have been saved in other tabs while we were collaborating
- resetBrowserStateVersions();
- window.history.pushState({}, APP_NAME, window.location.origin);
- this.destroySocketClient();
- trackEvent("share", "room closed");
- this.props.onRoomClose?.();
- const elements = this.excalidrawAPI
- .getSceneElementsIncludingDeleted()
- .map((element) => {
- if (isImageElement(element) && element.status === "saved") {
- return newElementWith(element, { status: "pending" });
- }
- return element;
- });
- this.excalidrawAPI.updateScene({
- elements,
- commitToHistory: false,
- });
- }
- };
- private destroySocketClient = (opts?: { isUnload: boolean }) => {
- if (!opts?.isUnload) {
- this.collaborators = new Map();
- this.excalidrawAPI.updateScene({
- collaborators: this.collaborators,
- });
- this.setState({
- activeRoomLink: "",
- });
- this._isCollaborating = false;
- LocalData.resumeSave("collaboration");
- }
- this.lastBroadcastedOrReceivedSceneVersion = -1;
- this.portal.close();
- this.fileManager.reset();
- };
- private fetchImageFilesFromFirebase = async (scene: {
- elements: readonly ExcalidrawElement[];
- }) => {
- const unfetchedImages = scene.elements
- .filter((element) => {
- return (
- isInitializedImageElement(element) &&
- !this.fileManager.isFileHandled(element.fileId) &&
- !element.isDeleted &&
- element.status === "saved"
- );
- })
- .map((element) => (element as InitializedExcalidrawImageElement).fileId);
- return await this.fileManager.getFiles(unfetchedImages);
- };
- private decryptPayload = async (
- iv: Uint8Array,
- encryptedData: ArrayBuffer,
- decryptionKey: string,
- ) => {
- try {
- const decrypted = await decryptData(iv, encryptedData, decryptionKey);
- const decodedData = new TextDecoder("utf-8").decode(
- new Uint8Array(decrypted),
- );
- return JSON.parse(decodedData);
- } catch (error) {
- window.alert(t("alerts.decryptFailed"));
- console.error(error);
- return {
- type: "INVALID_RESPONSE",
- };
- }
- };
- private initializeSocketClient = async (
- existingRoomLinkData: null | { roomId: string; roomKey: string },
- ): Promise<ImportedDataState | null> => {
- if (this.portal.socket) {
- return null;
- }
- let roomId;
- let roomKey;
- if (existingRoomLinkData) {
- ({ roomId, roomKey } = existingRoomLinkData);
- } else {
- ({ roomId, roomKey } = await generateCollaborationLinkData());
- window.history.pushState(
- {},
- APP_NAME,
- getCollaborationLink({ roomId, roomKey }),
- );
- }
- const scenePromise = resolvablePromise<ImportedDataState | null>();
- this._isCollaborating = true;
- LocalData.pauseSave("collaboration");
- const { default: socketIOClient } = await import(
- /* webpackChunkName: "socketIoClient" */ "socket.io-client"
- );
- try {
- const socketServerData = await getCollabServer();
- this.portal.socket = this.portal.open(
- socketIOClient(socketServerData.url, {
- transports: socketServerData.polling
- ? ["websocket", "polling"]
- : ["websocket"],
- }),
- roomId,
- roomKey,
- );
- } catch (error: any) {
- console.error(error);
- this.setState({ errorMessage: error.message });
- return null;
- }
- if (!existingRoomLinkData) {
- const elements = this.excalidrawAPI.getSceneElements().map((element) => {
- if (isImageElement(element) && element.status === "saved") {
- return newElementWith(element, { status: "pending" });
- }
- return element;
- });
- // remove deleted elements from elements array & history to ensure we don't
- // expose potentially sensitive user data in case user manually deletes
- // existing elements (or clears scene), which would otherwise be persisted
- // to database even if deleted before creating the room.
- this.excalidrawAPI.history.clear();
- this.excalidrawAPI.updateScene({
- elements,
- commitToHistory: true,
- });
- this.broadcastElements(elements);
- const syncableElements = this.getSyncableElements(elements);
- this.saveCollabRoomToFirebase(syncableElements);
- }
- // fallback in case you're not alone in the room but still don't receive
- // initial SCENE_INIT message
- this.socketInitializationTimer = window.setTimeout(() => {
- this.initializeRoom({
- roomLinkData: existingRoomLinkData,
- fetchScene: true,
- });
- scenePromise.resolve(null);
- }, INITIAL_SCENE_UPDATE_TIMEOUT);
- // All socket listeners are moving to Portal
- this.portal.socket.on(
- "client-broadcast",
- async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
- if (!this.portal.roomKey) {
- return;
- }
- const decryptedData = await this.decryptPayload(
- iv,
- encryptedData,
- this.portal.roomKey,
- );
- switch (decryptedData.type) {
- case "INVALID_RESPONSE":
- return;
- case SCENE.INIT: {
- if (!this.portal.socketInitialized) {
- this.initializeRoom({ fetchScene: false });
- const remoteElements = decryptedData.payload.elements;
- const reconciledElements = this.reconcileElements(remoteElements);
- this.handleRemoteSceneUpdate(reconciledElements, {
- init: true,
- });
- // noop if already resolved via init from firebase
- scenePromise.resolve({
- elements: reconciledElements,
- scrollToContent: true,
- });
- }
- break;
- }
- case SCENE.UPDATE:
- this.handleRemoteSceneUpdate(
- this.reconcileElements(decryptedData.payload.elements),
- );
- break;
- case "MOUSE_LOCATION": {
- const { pointer, button, username, selectedElementIds } =
- decryptedData.payload;
- const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] =
- decryptedData.payload.socketId ||
- // @ts-ignore legacy, see #2094 (#2097)
- 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.excalidrawAPI.updateScene({
- collaborators,
- });
- 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;
- }
- }
- },
- );
- this.portal.socket.on("first-in-room", async () => {
- if (this.portal.socket) {
- this.portal.socket.off("first-in-room");
- }
- const sceneData = await this.initializeRoom({
- fetchScene: true,
- roomLinkData: existingRoomLinkData,
- });
- scenePromise.resolve(sceneData);
- });
- this.initializeIdleDetector();
- this.setState({
- activeRoomLink: window.location.href,
- });
- return scenePromise;
- };
- private initializeRoom = async ({
- fetchScene,
- roomLinkData,
- }:
- | {
- fetchScene: true;
- roomLinkData: { roomId: string; roomKey: string } | null;
- }
- | { fetchScene: false; roomLinkData?: null }) => {
- clearTimeout(this.socketInitializationTimer!);
- if (fetchScene && roomLinkData && this.portal.socket) {
- this.excalidrawAPI.resetScene();
- try {
- const elements = await loadFromFirebase(
- roomLinkData.roomId,
- roomLinkData.roomKey,
- this.portal.socket,
- );
- if (elements) {
- this.setLastBroadcastedOrReceivedSceneVersion(
- getSceneVersion(elements),
- );
- return {
- elements,
- scrollToContent: true,
- };
- }
- } catch (error: any) {
- // log the error and move on. other peers will sync us the scene.
- console.error(error);
- } finally {
- this.portal.socketInitialized = true;
- }
- } else {
- this.portal.socketInitialized = true;
- }
- return null;
- };
- private reconcileElements = (
- remoteElements: readonly ExcalidrawElement[],
- ): ReconciledElements => {
- const localElements = this.getSceneElementsIncludingDeleted();
- const appState = this.excalidrawAPI.getAppState();
- const reconciledElements = _reconcileElements(
- localElements,
- remoteElements,
- appState,
- );
- // Avoid broadcasting to the rest of the collaborators the scene
- // we just received!
- // Note: this needs to be set before updating the scene as it
- // synchronously calls render.
- this.setLastBroadcastedOrReceivedSceneVersion(
- getSceneVersion(reconciledElements),
- );
- return reconciledElements;
- };
- private loadImageFiles = throttle(async () => {
- const { loadedFiles, erroredFiles } =
- await this.fetchImageFilesFromFirebase({
- elements: this.excalidrawAPI.getSceneElementsIncludingDeleted(),
- });
- this.excalidrawAPI.addFiles(loadedFiles);
- updateStaleImageStatuses({
- excalidrawAPI: this.excalidrawAPI,
- erroredFiles,
- elements: this.excalidrawAPI.getSceneElementsIncludingDeleted(),
- });
- }, LOAD_IMAGES_TIMEOUT);
- private handleRemoteSceneUpdate = (
- elements: ReconciledElements,
- { init = false }: { init?: boolean } = {},
- ) => {
- this.excalidrawAPI.updateScene({
- elements,
- commitToHistory: !!init,
- });
- // We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
- // when we receive any messages from another peer. This UX can be pretty rough -- if you
- // undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
- // right now we think this is the right tradeoff.
- this.excalidrawAPI.history.clear();
- this.loadImageFiles();
- };
- 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[]) {
- this.setState((state) => {
- const collaborators: InstanceType<typeof CollabWrapper>["collaborators"] =
- new Map();
- for (const socketId of sockets) {
- if (this.collaborators.has(socketId)) {
- collaborators.set(socketId, this.collaborators.get(socketId)!);
- } else {
- collaborators.set(socketId, {});
- }
- }
- this.collaborators = collaborators;
- this.excalidrawAPI.updateScene({ collaborators });
- });
- }
- public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
- this.lastBroadcastedOrReceivedSceneVersion = version;
- };
- public getLastBroadcastedOrReceivedSceneVersion = () => {
- return this.lastBroadcastedOrReceivedSceneVersion;
- };
- public getSceneElementsIncludingDeleted = () => {
- return this.excalidrawAPI.getSceneElementsIncludingDeleted();
- };
- onPointerUpdate = throttle(
- (payload: {
- pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
- button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
- pointersMap: Gesture["pointers"];
- }) => {
- payload.pointersMap.size < 2 &&
- this.portal.socket &&
- this.portal.broadcastMouseLocation(payload);
- },
- CURSOR_SYNC_TIMEOUT,
- );
- onIdleStateChange = (userState: UserIdleState) => {
- this.setState({ userState });
- this.portal.broadcastIdleChange(userState);
- };
- broadcastElements = (elements: readonly ExcalidrawElement[]) => {
- if (
- getSceneVersion(elements) >
- this.getLastBroadcastedOrReceivedSceneVersion()
- ) {
- this.portal.broadcastScene(SCENE.UPDATE, elements, false);
- this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
- this.queueBroadcastAllElements();
- }
- };
- queueBroadcastAllElements = throttle(() => {
- this.portal.broadcastScene(
- SCENE.UPDATE,
- this.excalidrawAPI.getSceneElementsIncludingDeleted(),
- true,
- );
- const currentVersion = this.getLastBroadcastedOrReceivedSceneVersion();
- const newVersion = Math.max(
- currentVersion,
- getSceneVersion(this.getSceneElementsIncludingDeleted()),
- );
- this.setLastBroadcastedOrReceivedSceneVersion(newVersion);
- }, SYNC_FULL_SCENE_INTERVAL_MS);
- handleClose = () => {
- this.setState({ modalIsShown: false });
- };
- setUsername = (username: string) => {
- this.setState({ username });
- };
- onUsernameChange = (username: string) => {
- this.setUsername(username);
- saveUsernameToLocalStorage(username);
- };
- onCollabButtonClick = () => {
- this.setState({
- modalIsShown: true,
- });
- };
- isSyncableElement = (element: ExcalidrawElement) => {
- return element.isDeleted || !isInvisiblySmallElement(element);
- };
- getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
- elements.filter((element) => this.isSyncableElement(element));
- /** PRIVATE. Use `this.getContextValue()` instead. */
- private contextValue: CollabAPI | null = null;
- /** Getter of context value. Returned object is stable. */
- getContextValue = (): CollabAPI => {
- if (!this.contextValue) {
- this.contextValue = {} as CollabAPI;
- }
- this.contextValue.isCollaborating = this.isCollaborating;
- this.contextValue.username = this.state.username;
- this.contextValue.onPointerUpdate = this.onPointerUpdate;
- this.contextValue.initializeSocketClient = this.initializeSocketClient;
- this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
- this.contextValue.broadcastElements = this.broadcastElements;
- this.contextValue.fetchImageFilesFromFirebase =
- this.fetchImageFilesFromFirebase;
- this.contextValue.setUsername = this.setUsername;
- return this.contextValue;
- };
- render() {
- const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
- return (
- <>
- {modalIsShown && (
- <RoomDialog
- handleClose={this.handleClose}
- activeRoomLink={activeRoomLink}
- username={username}
- onUsernameChange={this.onUsernameChange}
- onRoomCreate={this.openPortal}
- onRoomDestroy={this.closePortal}
- setErrorMessage={(errorMessage) => {
- this.setState({ errorMessage });
- }}
- theme={this.excalidrawAPI.getAppState().theme}
- />
- )}
- {errorMessage && (
- <ErrorDialog
- message={errorMessage}
- onClose={() => this.setState({ errorMessage: "" })}
- />
- )}
- <CollabContextProvider
- value={{
- api: this.getContextValue(),
- }}
- />
- </>
- );
- }
- }
- declare global {
- interface Window {
- collab: InstanceType<typeof CollabWrapper>;
- }
- }
- if (
- process.env.NODE_ENV === ENV.TEST ||
- process.env.NODE_ENV === ENV.DEVELOPMENT
- ) {
- window.collab = window.collab || ({} as Window["collab"]);
- }
- export default CollabWrapper;
|