| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166 | import {  Excalidraw,  LiveCollaborationTrigger,  TTDDialogTrigger,  CaptureUpdateAction,  reconcileElements,} from "@excalidraw/excalidraw";import { trackEvent } from "@excalidraw/excalidraw/analytics";import { getDefaultAppState } from "@excalidraw/excalidraw/appState";import {  CommandPalette,  DEFAULT_CATEGORIES,} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";import Trans from "@excalidraw/excalidraw/components/Trans";import {  APP_NAME,  EVENT,  THEME,  TITLE_TIMEOUT,  VERSION_TIMEOUT,  debounce,  getVersion,  getFrame,  isTestEnv,  preventUnload,  resolvablePromise,  isRunningInIframe,  isDevEnv,} from "@excalidraw/common";import polyfill from "@excalidraw/excalidraw/polyfill";import { useCallback, useEffect, useRef, useState } from "react";import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";import { t } from "@excalidraw/excalidraw/i18n";import {  GithubIcon,  XBrandIcon,  DiscordIcon,  ExcalLogo,  usersIcon,  exportToPlus,  share,  youtubeIcon,} from "@excalidraw/excalidraw/components/icons";import { isElementLink } from "@excalidraw/element";import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";import { newElementWith } from "@excalidraw/element";import { isInitializedImageElement } from "@excalidraw/element";import clsx from "clsx";import {  parseLibraryTokensFromUrl,  useHandleLibrary,} from "@excalidraw/excalidraw/data/library";import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";import type {  FileId,  NonDeletedExcalidrawElement,  OrderedExcalidrawElement,} from "@excalidraw/element/types";import type {  AppState,  ExcalidrawImperativeAPI,  BinaryFiles,  ExcalidrawInitialDataState,  UIAppState,} from "@excalidraw/excalidraw/types";import type { ResolutionType } from "@excalidraw/common/utility-types";import type { ResolvablePromise } from "@excalidraw/common/utils";import CustomStats from "./CustomStats";import {  Provider,  useAtom,  useAtomValue,  useAtomWithInitialValue,  appJotaiStore,} from "./app-jotai";import {  FIREBASE_STORAGE_PREFIXES,  isExcalidrawPlusSignedUser,  STORAGE_KEYS,  SYNC_BROWSER_TABS_TIMEOUT,} from "./app_constants";import Collab, {  collabAPIAtom,  isCollaboratingAtom,  isOfflineAtom,} from "./collab/Collab";import { AppFooter } from "./components/AppFooter";import { AppMainMenu } from "./components/AppMainMenu";import { AppWelcomeScreen } from "./components/AppWelcomeScreen";import {  ExportToExcalidrawPlus,  exportToExcalidrawPlus,} from "./components/ExportToExcalidrawPlus";import { TopErrorBoundary } from "./components/TopErrorBoundary";import {  exportToBackend,  getCollaborationLinkData,  isCollaborationLink,  loadScene,} from "./data";import { updateStaleImageStatuses } from "./data/FileManager";import {  importFromLocalStorage,  importUsernameFromLocalStorage,} from "./data/localStorage";import { loadFilesFromFirebase } from "./data/firebase";import {  LibraryIndexedDBAdapter,  LibraryLocalStorageMigrationAdapter,  LocalData,} from "./data/LocalData";import { isBrowserStorageStateNewer } from "./data/tabSync";import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";import { useHandleAppTheme } from "./useHandleAppTheme";import { getPreferredLanguage } from "./app-language/language-detector";import { useAppLangCode } from "./app-language/language-state";import DebugCanvas, {  debugRenderer,  isVisualDebuggerEnabled,  loadSavedDebugState,} from "./components/DebugCanvas";import { AIComponents } from "./components/AI";import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";import "./index.scss";import type { CollabAPI } from "./collab/Collab";polyfill();window.EXCALIDRAW_THROTTLE_RENDER = true;declare global {  interface BeforeInstallPromptEventChoiceResult {    outcome: "accepted" | "dismissed";  }  interface BeforeInstallPromptEvent extends Event {    prompt(): Promise<void>;    userChoice: Promise<BeforeInstallPromptEventChoiceResult>;  }  interface WindowEventMap {    beforeinstallprompt: BeforeInstallPromptEvent;  }}let pwaEvent: BeforeInstallPromptEvent | null = null;// Adding a listener outside of the component as it may (?) need to be// subscribed early to catch the event.//// Also note that it will fire only if certain heuristics are met (user has// used the app for some time, etc.)window.addEventListener(  "beforeinstallprompt",  (event: BeforeInstallPromptEvent) => {    // prevent Chrome <= 67 from automatically showing the prompt    event.preventDefault();    // cache for later use    pwaEvent = event;  },);let isSelfEmbedding = false;if (window.self !== window.top) {  try {    const parentUrl = new URL(document.referrer);    const currentUrl = new URL(window.location.href);    if (parentUrl.origin === currentUrl.origin) {      isSelfEmbedding = true;    }  } catch (error) {    // ignore  }}const shareableLinkConfirmDialog = {  title: t("overwriteConfirm.modal.shareableLink.title"),  description: (    <Trans      i18nKey="overwriteConfirm.modal.shareableLink.description"      bold={(text) => <strong>{text}</strong>}      br={() => <br />}    />  ),  actionLabel: t("overwriteConfirm.modal.shareableLink.button"),  color: "danger",} as const;const initializeScene = async (opts: {  collabAPI: CollabAPI | null;  excalidrawAPI: ExcalidrawImperativeAPI;}): Promise<  { scene: ExcalidrawInitialDataState | null } & (    | { isExternalScene: true; id: string; key: string }    | { isExternalScene: false; id?: null; key?: null }  )> => {  const searchParams = new URLSearchParams(window.location.search);  const id = searchParams.get("id");  const jsonBackendMatch = window.location.hash.match(    /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,  );  const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);  const localDataState = importFromLocalStorage();  let scene: RestoredDataState & {    scrollToContent?: boolean;  } = await loadScene(null, null, localDataState);  let roomLinkData = getCollaborationLinkData(window.location.href);  const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);  if (isExternalScene) {    if (      // don't prompt if scene is empty      !scene.elements.length ||      // don't prompt for collab scenes because we don't override local storage      roomLinkData ||      // otherwise, prompt whether user wants to override current scene      (await openConfirmModal(shareableLinkConfirmDialog))    ) {      if (jsonBackendMatch) {        scene = await loadScene(          jsonBackendMatch[1],          jsonBackendMatch[2],          localDataState,        );      }      scene.scrollToContent = true;      if (!roomLinkData) {        window.history.replaceState({}, APP_NAME, window.location.origin);      }    } else {      // https://github.com/excalidraw/excalidraw/issues/1919      if (document.hidden) {        return new Promise((resolve, reject) => {          window.addEventListener(            "focus",            () => initializeScene(opts).then(resolve).catch(reject),            {              once: true,            },          );        });      }      roomLinkData = null;      window.history.replaceState({}, APP_NAME, window.location.origin);    }  } else if (externalUrlMatch) {    window.history.replaceState({}, APP_NAME, window.location.origin);    const url = externalUrlMatch[1];    try {      const request = await fetch(window.decodeURIComponent(url));      const data = await loadFromBlob(await request.blob(), null, null);      if (        !scene.elements.length ||        (await openConfirmModal(shareableLinkConfirmDialog))      ) {        return { scene: data, isExternalScene };      }    } catch (error: any) {      return {        scene: {          appState: {            errorMessage: t("alerts.invalidSceneUrl"),          },        },        isExternalScene,      };    }  }  if (roomLinkData && opts.collabAPI) {    const { excalidrawAPI } = opts;    const scene = await opts.collabAPI.startCollaboration(roomLinkData);    return {      // when collaborating, the state may have already been updated at this      // point (we may have received updates from other clients), so reconcile      // elements and appState with existing state      scene: {        ...scene,        appState: {          ...restoreAppState(            {              ...scene?.appState,              theme: localDataState?.appState?.theme || scene?.appState?.theme,            },            excalidrawAPI.getAppState(),          ),          // necessary if we're invoking from a hashchange handler which doesn't          // go through App.initializeScene() that resets this flag          isLoading: false,        },        elements: reconcileElements(          scene?.elements || [],          excalidrawAPI.getSceneElementsIncludingDeleted() as RemoteExcalidrawElement[],          excalidrawAPI.getAppState(),        ),      },      isExternalScene: true,      id: roomLinkData.roomId,      key: roomLinkData.roomKey,    };  } else if (scene) {    return isExternalScene && jsonBackendMatch      ? {          scene,          isExternalScene,          id: jsonBackendMatch[1],          key: jsonBackendMatch[2],        }      : { scene, isExternalScene: false };  }  return { scene: null, isExternalScene: false };};const ExcalidrawWrapper = () => {  const [errorMessage, setErrorMessage] = useState("");  const isCollabDisabled = isRunningInIframe();  const { editorTheme, appTheme, setAppTheme } = useHandleAppTheme();  const [langCode, setLangCode] = useAppLangCode();  // initial state  // ---------------------------------------------------------------------------  const initialStatePromiseRef = useRef<{    promise: ResolvablePromise<ExcalidrawInitialDataState | null>;  }>({ promise: null! });  if (!initialStatePromiseRef.current.promise) {    initialStatePromiseRef.current.promise =      resolvablePromise<ExcalidrawInitialDataState | null>();  }  const debugCanvasRef = useRef<HTMLCanvasElement>(null);  useEffect(() => {    trackEvent("load", "frame", getFrame());    // Delayed so that the app has a time to load the latest SW    setTimeout(() => {      trackEvent("load", "version", getVersion());    }, VERSION_TIMEOUT);  }, []);  const [excalidrawAPI, excalidrawRefCallback] =    useCallbackRefState<ExcalidrawImperativeAPI>();  const [, setShareDialogState] = useAtom(shareDialogStateAtom);  const [collabAPI] = useAtom(collabAPIAtom);  const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {    return isCollaborationLink(window.location.href);  });  const collabError = useAtomValue(collabErrorIndicatorAtom);  useHandleLibrary({    excalidrawAPI,    adapter: LibraryIndexedDBAdapter,    // TODO maybe remove this in several months (shipped: 24-03-11)    migrationAdapter: LibraryLocalStorageMigrationAdapter,  });  const [, forceRefresh] = useState(false);  useEffect(() => {    if (isDevEnv()) {      const debugState = loadSavedDebugState();      if (debugState.enabled && !window.visualDebug) {        window.visualDebug = {          data: [],        };      } else {        delete window.visualDebug;      }      forceRefresh((prev) => !prev);    }  }, [excalidrawAPI]);  useEffect(() => {    if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {      return;    }    const loadImages = (      data: ResolutionType<typeof initializeScene>,      isInitialLoad = false,    ) => {      if (!data.scene) {        return;      }      if (collabAPI?.isCollaborating()) {        if (data.scene.elements) {          collabAPI            .fetchImageFilesFromFirebase({              elements: data.scene.elements,              forceFetchFiles: true,            })            .then(({ loadedFiles, erroredFiles }) => {              excalidrawAPI.addFiles(loadedFiles);              updateStaleImageStatuses({                excalidrawAPI,                erroredFiles,                elements: excalidrawAPI.getSceneElementsIncludingDeleted(),              });            });        }      } else {        const fileIds =          data.scene.elements?.reduce((acc, element) => {            if (isInitializedImageElement(element)) {              return acc.concat(element.fileId);            }            return acc;          }, [] as FileId[]) || [];        if (data.isExternalScene) {          loadFilesFromFirebase(            `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,            data.key,            fileIds,          ).then(({ loadedFiles, erroredFiles }) => {            excalidrawAPI.addFiles(loadedFiles);            updateStaleImageStatuses({              excalidrawAPI,              erroredFiles,              elements: excalidrawAPI.getSceneElementsIncludingDeleted(),            });          });        } else if (isInitialLoad) {          if (fileIds.length) {            LocalData.fileStorage              .getFiles(fileIds)              .then(({ loadedFiles, erroredFiles }) => {                if (loadedFiles.length) {                  excalidrawAPI.addFiles(loadedFiles);                }                updateStaleImageStatuses({                  excalidrawAPI,                  erroredFiles,                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),                });              });          }          // on fresh load, clear unused files from IDB (from previous          // session)          LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });        }      }    };    initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {      loadImages(data, /* isInitialLoad */ true);      initialStatePromiseRef.current.promise.resolve(data.scene);    });    const onHashChange = async (event: HashChangeEvent) => {      event.preventDefault();      const libraryUrlTokens = parseLibraryTokensFromUrl();      if (!libraryUrlTokens) {        if (          collabAPI?.isCollaborating() &&          !isCollaborationLink(window.location.href)        ) {          collabAPI.stopCollaboration(false);        }        excalidrawAPI.updateScene({ appState: { isLoading: true } });        initializeScene({ collabAPI, excalidrawAPI }).then((data) => {          loadImages(data);          if (data.scene) {            excalidrawAPI.updateScene({              ...data.scene,              ...restore(data.scene, null, null, { repairBindings: true }),              captureUpdate: CaptureUpdateAction.IMMEDIATELY,            });          }        });      }    };    const titleTimeout = setTimeout(      () => (document.title = APP_NAME),      TITLE_TIMEOUT,    );    const syncData = debounce(() => {      if (isTestEnv()) {        return;      }      if (        !document.hidden &&        ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)      ) {        // don't sync if local state is newer or identical to browser state        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {          const localDataState = importFromLocalStorage();          const username = importUsernameFromLocalStorage();          setLangCode(getPreferredLanguage());          excalidrawAPI.updateScene({            ...localDataState,            captureUpdate: CaptureUpdateAction.NEVER,          });          LibraryIndexedDBAdapter.load().then((data) => {            if (data) {              excalidrawAPI.updateLibrary({                libraryItems: data.libraryItems,              });            }          });          collabAPI?.setUsername(username || "");        }        if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {          const elements = excalidrawAPI.getSceneElementsIncludingDeleted();          const currFiles = excalidrawAPI.getFiles();          const fileIds =            elements?.reduce((acc, element) => {              if (                isInitializedImageElement(element) &&                // only load and update images that aren't already loaded                !currFiles[element.fileId]              ) {                return acc.concat(element.fileId);              }              return acc;            }, [] as FileId[]) || [];          if (fileIds.length) {            LocalData.fileStorage              .getFiles(fileIds)              .then(({ loadedFiles, erroredFiles }) => {                if (loadedFiles.length) {                  excalidrawAPI.addFiles(loadedFiles);                }                updateStaleImageStatuses({                  excalidrawAPI,                  erroredFiles,                  elements: excalidrawAPI.getSceneElementsIncludingDeleted(),                });              });          }        }      }    }, SYNC_BROWSER_TABS_TIMEOUT);    const onUnload = () => {      LocalData.flushSave();    };    const visibilityChange = (event: FocusEvent | Event) => {      if (event.type === EVENT.BLUR || document.hidden) {        LocalData.flushSave();      }      if (        event.type === EVENT.VISIBILITY_CHANGE ||        event.type === EVENT.FOCUS      ) {        syncData();      }    };    window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);    window.addEventListener(EVENT.UNLOAD, onUnload, false);    window.addEventListener(EVENT.BLUR, visibilityChange, false);    document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);    window.addEventListener(EVENT.FOCUS, visibilityChange, false);    return () => {      window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);      window.removeEventListener(EVENT.UNLOAD, onUnload, false);      window.removeEventListener(EVENT.BLUR, visibilityChange, false);      window.removeEventListener(EVENT.FOCUS, visibilityChange, false);      document.removeEventListener(        EVENT.VISIBILITY_CHANGE,        visibilityChange,        false,      );      clearTimeout(titleTimeout);    };  }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);  useEffect(() => {    const unloadHandler = (event: BeforeUnloadEvent) => {      LocalData.flushSave();      if (        excalidrawAPI &&        LocalData.fileStorage.shouldPreventUnload(          excalidrawAPI.getSceneElements(),        )      ) {        if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {          preventUnload(event);        } else {          console.warn(            "preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",          );        }      }    };    window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);    return () => {      window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);    };  }, [excalidrawAPI]);  const onChange = (    elements: readonly OrderedExcalidrawElement[],    appState: AppState,    files: BinaryFiles,  ) => {    if (collabAPI?.isCollaborating()) {      collabAPI.syncElements(elements);    }    // this check is redundant, but since this is a hot path, it's best    // not to evaludate the nested expression every time    if (!LocalData.isSavePaused()) {      LocalData.save(elements, appState, files, () => {        if (excalidrawAPI) {          let didChange = false;          const elements = excalidrawAPI            .getSceneElementsIncludingDeleted()            .map((element) => {              if (                LocalData.fileStorage.shouldUpdateImageElementStatus(element)              ) {                const newElement = newElementWith(element, { status: "saved" });                if (newElement !== element) {                  didChange = true;                }                return newElement;              }              return element;            });          if (didChange) {            excalidrawAPI.updateScene({              elements,              captureUpdate: CaptureUpdateAction.NEVER,            });          }        }      });    }    // Render the debug scene if the debug canvas is available    if (debugCanvasRef.current && excalidrawAPI) {      debugRenderer(        debugCanvasRef.current,        appState,        window.devicePixelRatio,        () => forceRefresh((prev) => !prev),      );    }  };  const [latestShareableLink, setLatestShareableLink] = useState<string | null>(    null,  );  const onExportToBackend = async (    exportedElements: readonly NonDeletedExcalidrawElement[],    appState: Partial<AppState>,    files: BinaryFiles,  ) => {    if (exportedElements.length === 0) {      throw new Error(t("alerts.cannotExportEmptyCanvas"));    }    try {      const { url, errorMessage } = await exportToBackend(        exportedElements,        {          ...appState,          viewBackgroundColor: appState.exportBackground            ? appState.viewBackgroundColor            : getDefaultAppState().viewBackgroundColor,        },        files,      );      if (errorMessage) {        throw new Error(errorMessage);      }      if (url) {        setLatestShareableLink(url);      }    } catch (error: any) {      if (error.name !== "AbortError") {        const { width, height } = appState;        console.error(error, {          width,          height,          devicePixelRatio: window.devicePixelRatio,        });        throw new Error(error.message);      }    }  };  const renderCustomStats = (    elements: readonly NonDeletedExcalidrawElement[],    appState: UIAppState,  ) => {    return (      <CustomStats        setToast={(message) => excalidrawAPI!.setToast({ message })}        appState={appState}        elements={elements}      />    );  };  const isOffline = useAtomValue(isOfflineAtom);  const onCollabDialogOpen = useCallback(    () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }),    [setShareDialogState],  );  // browsers generally prevent infinite self-embedding, there are  // cases where it still happens, and while we disallow self-embedding  // by not whitelisting our own origin, this serves as an additional guard  if (isSelfEmbedding) {    return (      <div        style={{          display: "flex",          alignItems: "center",          justifyContent: "center",          textAlign: "center",          height: "100%",        }}      >        <h1>I'm not a pretzel!</h1>      </div>    );  }  const ExcalidrawPlusCommand = {    label: "Excalidraw+",    category: DEFAULT_CATEGORIES.links,    predicate: true,    icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,    keywords: ["plus", "cloud", "server"],    perform: () => {      window.open(        `${          import.meta.env.VITE_APP_PLUS_LP        }/plus?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,        "_blank",      );    },  };  const ExcalidrawPlusAppCommand = {    label: "Sign up",    category: DEFAULT_CATEGORIES.links,    predicate: true,    icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,    keywords: [      "excalidraw",      "plus",      "cloud",      "server",      "signin",      "login",      "signup",    ],    perform: () => {      window.open(        `${          import.meta.env.VITE_APP_PLUS_APP        }?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,        "_blank",      );    },  };  return (    <div      style={{ height: "100%" }}      className={clsx("excalidraw-app", {        "is-collaborating": isCollaborating,      })}    >      <Excalidraw        excalidrawAPI={excalidrawRefCallback}        onChange={onChange}        initialData={initialStatePromiseRef.current.promise}        isCollaborating={isCollaborating}        onPointerUpdate={collabAPI?.onPointerUpdate}        UIOptions={{          canvasActions: {            toggleTheme: true,            export: {              onExportToBackend,              renderCustomUI: excalidrawAPI                ? (elements, appState, files) => {                    return (                      <ExportToExcalidrawPlus                        elements={elements}                        appState={appState}                        files={files}                        name={excalidrawAPI.getName()}                        onError={(error) => {                          excalidrawAPI?.updateScene({                            appState: {                              errorMessage: error.message,                            },                          });                        }}                        onSuccess={() => {                          excalidrawAPI.updateScene({                            appState: { openDialog: null },                          });                        }}                      />                    );                  }                : undefined,            },          },        }}        langCode={langCode}        renderCustomStats={renderCustomStats}        detectScroll={false}        handleKeyboardGlobally={true}        autoFocus={true}        theme={editorTheme}        renderTopRightUI={(isMobile) => {          if (isMobile || !collabAPI || isCollabDisabled) {            return null;          }          return (            <div className="top-right-ui">              {collabError.message && <CollabError collabError={collabError} />}              <LiveCollaborationTrigger                isCollaborating={isCollaborating}                onSelect={() =>                  setShareDialogState({ isOpen: true, type: "share" })                }              />            </div>          );        }}        onLinkOpen={(element, event) => {          if (element.link && isElementLink(element.link)) {            event.preventDefault();            excalidrawAPI?.scrollToContent(element.link, { animate: true });          }        }}      >        <AppMainMenu          onCollabDialogOpen={onCollabDialogOpen}          isCollaborating={isCollaborating}          isCollabEnabled={!isCollabDisabled}          theme={appTheme}          setTheme={(theme) => setAppTheme(theme)}          refresh={() => forceRefresh((prev) => !prev)}        />        <AppWelcomeScreen          onCollabDialogOpen={onCollabDialogOpen}          isCollabEnabled={!isCollabDisabled}        />        <OverwriteConfirmDialog>          <OverwriteConfirmDialog.Actions.ExportToImage />          <OverwriteConfirmDialog.Actions.SaveToDisk />          {excalidrawAPI && (            <OverwriteConfirmDialog.Action              title={t("overwriteConfirm.action.excalidrawPlus.title")}              actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}              onClick={() => {                exportToExcalidrawPlus(                  excalidrawAPI.getSceneElements(),                  excalidrawAPI.getAppState(),                  excalidrawAPI.getFiles(),                  excalidrawAPI.getName(),                );              }}            >              {t("overwriteConfirm.action.excalidrawPlus.description")}            </OverwriteConfirmDialog.Action>          )}        </OverwriteConfirmDialog>        <AppFooter onChange={() => excalidrawAPI?.refresh()} />        {excalidrawAPI && <AIComponents excalidrawAPI={excalidrawAPI} />}        <TTDDialogTrigger />        {isCollaborating && isOffline && (          <div className="collab-offline-warning">            {t("alerts.collabOfflineWarning")}          </div>        )}        {latestShareableLink && (          <ShareableLinkDialog            link={latestShareableLink}            onCloseRequest={() => setLatestShareableLink(null)}            setErrorMessage={setErrorMessage}          />        )}        {excalidrawAPI && !isCollabDisabled && (          <Collab excalidrawAPI={excalidrawAPI} />        )}        <ShareDialog          collabAPI={collabAPI}          onExportToBackend={async () => {            if (excalidrawAPI) {              try {                await onExportToBackend(                  excalidrawAPI.getSceneElements(),                  excalidrawAPI.getAppState(),                  excalidrawAPI.getFiles(),                );              } catch (error: any) {                setErrorMessage(error.message);              }            }          }}        />        {errorMessage && (          <ErrorDialog onClose={() => setErrorMessage("")}>            {errorMessage}          </ErrorDialog>        )}        <CommandPalette          customCommandPaletteItems={[            {              label: t("labels.liveCollaboration"),              category: DEFAULT_CATEGORIES.app,              keywords: [                "team",                "multiplayer",                "share",                "public",                "session",                "invite",              ],              icon: usersIcon,              perform: () => {                setShareDialogState({                  isOpen: true,                  type: "collaborationOnly",                });              },            },            {              label: t("roomDialog.button_stopSession"),              category: DEFAULT_CATEGORIES.app,              predicate: () => !!collabAPI?.isCollaborating(),              keywords: [                "stop",                "session",                "end",                "leave",                "close",                "exit",                "collaboration",              ],              perform: () => {                if (collabAPI) {                  collabAPI.stopCollaboration();                  if (!collabAPI.isCollaborating()) {                    setShareDialogState({ isOpen: false });                  }                }              },            },            {              label: t("labels.share"),              category: DEFAULT_CATEGORIES.app,              predicate: true,              icon: share,              keywords: [                "link",                "shareable",                "readonly",                "export",                "publish",                "snapshot",                "url",                "collaborate",                "invite",              ],              perform: async () => {                setShareDialogState({ isOpen: true, type: "share" });              },            },            {              label: "GitHub",              icon: GithubIcon,              category: DEFAULT_CATEGORIES.links,              predicate: true,              keywords: [                "issues",                "bugs",                "requests",                "report",                "features",                "social",                "community",              ],              perform: () => {                window.open(                  "https://github.com/excalidraw/excalidraw",                  "_blank",                  "noopener noreferrer",                );              },            },            {              label: t("labels.followUs"),              icon: XBrandIcon,              category: DEFAULT_CATEGORIES.links,              predicate: true,              keywords: ["twitter", "contact", "social", "community"],              perform: () => {                window.open(                  "https://x.com/excalidraw",                  "_blank",                  "noopener noreferrer",                );              },            },            {              label: t("labels.discordChat"),              category: DEFAULT_CATEGORIES.links,              predicate: true,              icon: DiscordIcon,              keywords: [                "chat",                "talk",                "contact",                "bugs",                "requests",                "report",                "feedback",                "suggestions",                "social",                "community",              ],              perform: () => {                window.open(                  "https://discord.gg/UexuTaE",                  "_blank",                  "noopener noreferrer",                );              },            },            {              label: "YouTube",              icon: youtubeIcon,              category: DEFAULT_CATEGORIES.links,              predicate: true,              keywords: ["features", "tutorials", "howto", "help", "community"],              perform: () => {                window.open(                  "https://youtube.com/@excalidraw",                  "_blank",                  "noopener noreferrer",                );              },            },            ...(isExcalidrawPlusSignedUser              ? [                  {                    ...ExcalidrawPlusAppCommand,                    label: "Sign in / Go to Excalidraw+",                  },                ]              : [ExcalidrawPlusCommand, ExcalidrawPlusAppCommand]),            {              label: t("overwriteConfirm.action.excalidrawPlus.button"),              category: DEFAULT_CATEGORIES.export,              icon: exportToPlus,              predicate: true,              keywords: ["plus", "export", "save", "backup"],              perform: () => {                if (excalidrawAPI) {                  exportToExcalidrawPlus(                    excalidrawAPI.getSceneElements(),                    excalidrawAPI.getAppState(),                    excalidrawAPI.getFiles(),                    excalidrawAPI.getName(),                  );                }              },            },            {              ...CommandPalette.defaultItems.toggleTheme,              perform: () => {                setAppTheme(                  editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK,                );              },            },            {              label: t("labels.installPWA"),              category: DEFAULT_CATEGORIES.app,              predicate: () => !!pwaEvent,              perform: () => {                if (pwaEvent) {                  pwaEvent.prompt();                  pwaEvent.userChoice.then(() => {                    // event cannot be reused, but we'll hopefully                    // grab new one as the event should be fired again                    pwaEvent = null;                  });                }              },            },          ]}        />        {isVisualDebuggerEnabled() && excalidrawAPI && (          <DebugCanvas            appState={excalidrawAPI.getAppState()}            scale={window.devicePixelRatio}            ref={debugCanvasRef}          />        )}      </Excalidraw>    </div>  );};const ExcalidrawApp = () => {  const isCloudExportWindow =    window.location.pathname === "/excalidraw-plus-export";  if (isCloudExportWindow) {    return <ExcalidrawPlusIframeExport />;  }  return (    <TopErrorBoundary>      <Provider store={appJotaiStore}>        <ExcalidrawWrapper />      </Provider>    </TopErrorBoundary>  );};export default ExcalidrawApp;
 |