1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114 |
- import polyfill from "../packages/excalidraw/polyfill";
- import LanguageDetector from "i18next-browser-languagedetector";
- import { useCallback, useEffect, useRef, useState } from "react";
- import { trackEvent } from "../packages/excalidraw/analytics";
- import { getDefaultAppState } from "../packages/excalidraw/appState";
- import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog";
- import { TopErrorBoundary } from "./components/TopErrorBoundary";
- import {
- APP_NAME,
- EVENT,
- THEME,
- TITLE_TIMEOUT,
- VERSION_TIMEOUT,
- } from "../packages/excalidraw/constants";
- import { loadFromBlob } from "../packages/excalidraw/data/blob";
- import {
- FileId,
- NonDeletedExcalidrawElement,
- OrderedExcalidrawElement,
- Theme,
- } from "../packages/excalidraw/element/types";
- import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState";
- import { t } from "../packages/excalidraw/i18n";
- import {
- Excalidraw,
- defaultLang,
- LiveCollaborationTrigger,
- TTDDialog,
- TTDDialogTrigger,
- } from "../packages/excalidraw/index";
- import {
- AppState,
- ExcalidrawImperativeAPI,
- BinaryFiles,
- ExcalidrawInitialDataState,
- UIAppState,
- } from "../packages/excalidraw/types";
- import {
- debounce,
- getVersion,
- getFrame,
- isTestEnv,
- preventUnload,
- ResolvablePromise,
- resolvablePromise,
- isRunningInIframe,
- } from "../packages/excalidraw/utils";
- import {
- FIREBASE_STORAGE_PREFIXES,
- isExcalidrawPlusSignedUser,
- STORAGE_KEYS,
- SYNC_BROWSER_TABS_TIMEOUT,
- } from "./app_constants";
- import Collab, {
- CollabAPI,
- collabAPIAtom,
- isCollaboratingAtom,
- isOfflineAtom,
- } from "./collab/Collab";
- import {
- exportToBackend,
- getCollaborationLinkData,
- isCollaborationLink,
- loadScene,
- } from "./data";
- import {
- importFromLocalStorage,
- importUsernameFromLocalStorage,
- } from "./data/localStorage";
- import CustomStats from "./CustomStats";
- import {
- restore,
- restoreAppState,
- RestoredDataState,
- } from "../packages/excalidraw/data/restore";
- import {
- ExportToExcalidrawPlus,
- exportToExcalidrawPlus,
- } from "./components/ExportToExcalidrawPlus";
- import { updateStaleImageStatuses } from "./data/FileManager";
- import { newElementWith } from "../packages/excalidraw/element/mutateElement";
- import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks";
- import { loadFilesFromFirebase } from "./data/firebase";
- import {
- LibraryIndexedDBAdapter,
- LibraryLocalStorageMigrationAdapter,
- LocalData,
- } from "./data/LocalData";
- import { isBrowserStorageStateNewer } from "./data/tabSync";
- import clsx from "clsx";
- import {
- parseLibraryTokensFromUrl,
- useHandleLibrary,
- } from "../packages/excalidraw/data/library";
- import { AppMainMenu } from "./components/AppMainMenu";
- import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
- import { AppFooter } from "./components/AppFooter";
- import { atom, Provider, useAtom, useAtomValue } from "jotai";
- import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
- import { appJotaiStore } from "./app-jotai";
- import "./index.scss";
- import { ResolutionType } from "../packages/excalidraw/utility-types";
- import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
- import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
- import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
- import Trans from "../packages/excalidraw/components/Trans";
- import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
- import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
- import {
- RemoteExcalidrawElement,
- reconcileElements,
- } from "../packages/excalidraw/data/reconcile";
- import {
- CommandPalette,
- DEFAULT_CATEGORIES,
- } from "../packages/excalidraw/components/CommandPalette/CommandPalette";
- import {
- GithubIcon,
- XBrandIcon,
- DiscordIcon,
- ExcalLogo,
- usersIcon,
- exportToPlus,
- share,
- } from "../packages/excalidraw/components/icons";
- polyfill();
- window.EXCALIDRAW_THROTTLE_RENDER = true;
- 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 languageDetector = new LanguageDetector();
- languageDetector.init({
- languageUtils: {},
- });
- 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 detectedLangCode = languageDetector.detect() || defaultLang.code;
- export const appLangCodeAtom = atom(
- Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
- );
- const ExcalidrawWrapper = () => {
- const [errorMessage, setErrorMessage] = useState("");
- const [langCode, setLangCode] = useAtom(appLangCodeAtom);
- const isCollabDisabled = isRunningInIframe();
- // initial state
- // ---------------------------------------------------------------------------
- const initialStatePromiseRef = useRef<{
- promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
- }>({ promise: null! });
- if (!initialStatePromiseRef.current.promise) {
- initialStatePromiseRef.current.promise =
- resolvablePromise<ExcalidrawInitialDataState | 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,
- });
- 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 }),
- commitToHistory: true,
- });
- }
- });
- }
- };
- 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();
- let langCode = languageDetector.detect() || defaultLang.code;
- if (Array.isArray(langCode)) {
- langCode = langCode[0];
- }
- setLangCode(langCode);
- excalidrawAPI.updateScene({
- ...localDataState,
- });
- 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(),
- )
- ) {
- preventUnload(event);
- }
- };
- window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
- return () => {
- window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
- };
- }, [excalidrawAPI]);
- useEffect(() => {
- languageDetector.cacheUserLanguage(langCode);
- }, [langCode]);
- const [theme, setTheme] = useState<Theme>(
- () =>
- (localStorage.getItem(
- STORAGE_KEYS.LOCAL_STORAGE_THEME,
- ) as Theme | null) ||
- // FIXME migration from old LS scheme. Can be removed later. #5660
- importFromLocalStorage().appState?.theme ||
- THEME.LIGHT,
- );
- useEffect(() => {
- localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme);
- // currently only used for body styling during init (see public/index.html),
- // but may change in the future
- document.documentElement.classList.toggle("dark", theme === THEME.DARK);
- }, [theme]);
- const onChange = (
- elements: readonly OrderedExcalidrawElement[],
- appState: AppState,
- files: BinaryFiles,
- ) => {
- if (collabAPI?.isCollaborating()) {
- collabAPI.syncElements(elements);
- }
- setTheme(appState.theme);
- // 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,
- });
- }
- }
- });
- }
- };
- 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={theme}
- 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>
- );
- }}
- >
- <AppMainMenu
- onCollabDialogOpen={onCollabDialogOpen}
- isCollaborating={isCollaborating}
- isCollabEnabled={!isCollabDisabled}
- />
- <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 />
- <TTDDialog
- onTextSubmit={async (input) => {
- try {
- const response = await fetch(
- `${
- import.meta.env.VITE_APP_AI_BACKEND
- }/v1/ai/text-to-diagram/generate`,
- {
- method: "POST",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ prompt: input }),
- },
- );
- const rateLimit = response.headers.has("X-Ratelimit-Limit")
- ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
- : undefined;
- const rateLimitRemaining = response.headers.has(
- "X-Ratelimit-Remaining",
- )
- ? parseInt(
- response.headers.get("X-Ratelimit-Remaining") || "0",
- 10,
- )
- : undefined;
- const json = await response.json();
- if (!response.ok) {
- if (response.status === 429) {
- return {
- rateLimit,
- rateLimitRemaining,
- error: new Error(
- "Too many requests today, please try again tomorrow!",
- ),
- };
- }
- throw new Error(json.message || "Generation failed...");
- }
- const generatedResponse = json.generatedResponse;
- if (!generatedResponse) {
- throw new Error("Generation failed...");
- }
- return { generatedResponse, rateLimit, rateLimitRemaining };
- } catch (err: any) {
- throw new Error("Request failed");
- }
- }}
- />
- <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",
- );
- },
- },
- ...(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,
- ]}
- />
- </Excalidraw>
- </div>
- );
- };
- const ExcalidrawApp = () => {
- return (
- <TopErrorBoundary>
- <Provider unstable_createStore={() => appJotaiStore}>
- <ExcalidrawWrapper />
- </Provider>
- </TopErrorBoundary>
- );
- };
- export default ExcalidrawApp;
|