index.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. import LanguageDetector from "i18next-browser-languagedetector";
  2. import { useCallback, useContext, useEffect, useRef, useState } from "react";
  3. import { trackEvent } from "../analytics";
  4. import { getDefaultAppState } from "../appState";
  5. import { ErrorDialog } from "../components/ErrorDialog";
  6. import { TopErrorBoundary } from "../components/TopErrorBoundary";
  7. import {
  8. APP_NAME,
  9. COOKIES,
  10. EVENT,
  11. TITLE_TIMEOUT,
  12. VERSION_TIMEOUT,
  13. } from "../constants";
  14. import { loadFromBlob } from "../data/blob";
  15. import {
  16. ExcalidrawElement,
  17. FileId,
  18. NonDeletedExcalidrawElement,
  19. } from "../element/types";
  20. import { useCallbackRefState } from "../hooks/useCallbackRefState";
  21. import { t } from "../i18n";
  22. import {
  23. Excalidraw,
  24. defaultLang,
  25. languages,
  26. } from "../packages/excalidraw/index";
  27. import {
  28. AppState,
  29. LibraryItems,
  30. ExcalidrawImperativeAPI,
  31. BinaryFiles,
  32. ExcalidrawInitialDataState,
  33. } from "../types";
  34. import {
  35. debounce,
  36. getVersion,
  37. getFrame,
  38. isTestEnv,
  39. preventUnload,
  40. ResolvablePromise,
  41. resolvablePromise,
  42. } from "../utils";
  43. import {
  44. FIREBASE_STORAGE_PREFIXES,
  45. STORAGE_KEYS,
  46. SYNC_BROWSER_TABS_TIMEOUT,
  47. } from "./app_constants";
  48. import CollabWrapper, {
  49. CollabAPI,
  50. CollabContext,
  51. CollabContextConsumer,
  52. } from "./collab/CollabWrapper";
  53. import { LanguageList } from "./components/LanguageList";
  54. import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
  55. import {
  56. getLibraryItemsFromStorage,
  57. importFromLocalStorage,
  58. importUsernameFromLocalStorage,
  59. } from "./data/localStorage";
  60. import CustomStats from "./CustomStats";
  61. import { restoreAppState, RestoredDataState } from "../data/restore";
  62. import { Tooltip } from "../components/Tooltip";
  63. import { shield } from "../components/icons";
  64. import "./index.scss";
  65. import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
  66. import { updateStaleImageStatuses } from "./data/FileManager";
  67. import { newElementWith } from "../element/mutateElement";
  68. import { isInitializedImageElement } from "../element/typeChecks";
  69. import { loadFilesFromFirebase } from "./data/firebase";
  70. import { LocalData } from "./data/LocalData";
  71. import { isBrowserStorageStateNewer } from "./data/tabSync";
  72. import clsx from "clsx";
  73. import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
  74. const isExcalidrawPlusSignedUser = document.cookie.includes(
  75. COOKIES.AUTH_STATE_COOKIE,
  76. );
  77. const languageDetector = new LanguageDetector();
  78. languageDetector.init({
  79. languageUtils: {},
  80. });
  81. const initializeScene = async (opts: {
  82. collabAPI: CollabAPI;
  83. }): Promise<
  84. { scene: ExcalidrawInitialDataState | null } & (
  85. | { isExternalScene: true; id: string; key: string }
  86. | { isExternalScene: false; id?: null; key?: null }
  87. )
  88. > => {
  89. const searchParams = new URLSearchParams(window.location.search);
  90. const id = searchParams.get("id");
  91. const jsonBackendMatch = window.location.hash.match(
  92. /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/,
  93. );
  94. const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
  95. const localDataState = importFromLocalStorage();
  96. let scene: RestoredDataState & {
  97. scrollToContent?: boolean;
  98. } = await loadScene(null, null, localDataState);
  99. let roomLinkData = getCollaborationLinkData(window.location.href);
  100. const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
  101. if (isExternalScene) {
  102. if (
  103. // don't prompt if scene is empty
  104. !scene.elements.length ||
  105. // don't prompt for collab scenes because we don't override local storage
  106. roomLinkData ||
  107. // otherwise, prompt whether user wants to override current scene
  108. window.confirm(t("alerts.loadSceneOverridePrompt"))
  109. ) {
  110. if (jsonBackendMatch) {
  111. scene = await loadScene(
  112. jsonBackendMatch[1],
  113. jsonBackendMatch[2],
  114. localDataState,
  115. );
  116. }
  117. scene.scrollToContent = true;
  118. if (!roomLinkData) {
  119. window.history.replaceState({}, APP_NAME, window.location.origin);
  120. }
  121. } else {
  122. // https://github.com/excalidraw/excalidraw/issues/1919
  123. if (document.hidden) {
  124. return new Promise((resolve, reject) => {
  125. window.addEventListener(
  126. "focus",
  127. () => initializeScene(opts).then(resolve).catch(reject),
  128. {
  129. once: true,
  130. },
  131. );
  132. });
  133. }
  134. roomLinkData = null;
  135. window.history.replaceState({}, APP_NAME, window.location.origin);
  136. }
  137. } else if (externalUrlMatch) {
  138. window.history.replaceState({}, APP_NAME, window.location.origin);
  139. const url = externalUrlMatch[1];
  140. try {
  141. const request = await fetch(window.decodeURIComponent(url));
  142. const data = await loadFromBlob(await request.blob(), null, null);
  143. if (
  144. !scene.elements.length ||
  145. window.confirm(t("alerts.loadSceneOverridePrompt"))
  146. ) {
  147. return { scene: data, isExternalScene };
  148. }
  149. } catch (error: any) {
  150. return {
  151. scene: {
  152. appState: {
  153. errorMessage: t("alerts.invalidSceneUrl"),
  154. },
  155. },
  156. isExternalScene,
  157. };
  158. }
  159. }
  160. if (roomLinkData) {
  161. return {
  162. scene: await opts.collabAPI.initializeSocketClient(roomLinkData),
  163. isExternalScene: true,
  164. id: roomLinkData.roomId,
  165. key: roomLinkData.roomKey,
  166. };
  167. } else if (scene) {
  168. return isExternalScene && jsonBackendMatch
  169. ? {
  170. scene,
  171. isExternalScene,
  172. id: jsonBackendMatch[1],
  173. key: jsonBackendMatch[2],
  174. }
  175. : { scene, isExternalScene: false };
  176. }
  177. return { scene: null, isExternalScene: false };
  178. };
  179. const PlusLPLinkJSX = (
  180. <p style={{ direction: "ltr", unicodeBidi: "embed" }}>
  181. Introducing Excalidraw+
  182. <br />
  183. <a
  184. href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
  185. target="_blank"
  186. rel="noreferrer"
  187. >
  188. Try out now!
  189. </a>
  190. </p>
  191. );
  192. const PlusAppLinkJSX = (
  193. <a
  194. href={`${process.env.REACT_APP_PLUS_APP}/#excalidraw-redirect`}
  195. target="_blank"
  196. rel="noreferrer"
  197. className="plus-button"
  198. >
  199. Go to Excalidraw+
  200. </a>
  201. );
  202. const ExcalidrawWrapper = () => {
  203. const [errorMessage, setErrorMessage] = useState("");
  204. let currentLangCode = languageDetector.detect() || defaultLang.code;
  205. if (Array.isArray(currentLangCode)) {
  206. currentLangCode = currentLangCode[0];
  207. }
  208. const [langCode, setLangCode] = useState(currentLangCode);
  209. // initial state
  210. // ---------------------------------------------------------------------------
  211. const initialStatePromiseRef = useRef<{
  212. promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
  213. }>({ promise: null! });
  214. if (!initialStatePromiseRef.current.promise) {
  215. initialStatePromiseRef.current.promise =
  216. resolvablePromise<ExcalidrawInitialDataState | null>();
  217. }
  218. useEffect(() => {
  219. trackEvent("load", "frame", getFrame());
  220. // Delayed so that the app has a time to load the latest SW
  221. setTimeout(() => {
  222. trackEvent("load", "version", getVersion());
  223. }, VERSION_TIMEOUT);
  224. }, []);
  225. const [excalidrawAPI, excalidrawRefCallback] =
  226. useCallbackRefState<ExcalidrawImperativeAPI>();
  227. const collabAPI = useContext(CollabContext)?.api;
  228. useHandleLibrary({
  229. excalidrawAPI,
  230. getInitialLibraryItems: getLibraryItemsFromStorage,
  231. });
  232. useEffect(() => {
  233. if (!collabAPI || !excalidrawAPI) {
  234. return;
  235. }
  236. const loadImages = (
  237. data: ResolutionType<typeof initializeScene>,
  238. isInitialLoad = false,
  239. ) => {
  240. if (!data.scene) {
  241. return;
  242. }
  243. if (collabAPI.isCollaborating()) {
  244. if (data.scene.elements) {
  245. collabAPI
  246. .fetchImageFilesFromFirebase({
  247. elements: data.scene.elements,
  248. })
  249. .then(({ loadedFiles, erroredFiles }) => {
  250. excalidrawAPI.addFiles(loadedFiles);
  251. updateStaleImageStatuses({
  252. excalidrawAPI,
  253. erroredFiles,
  254. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  255. });
  256. });
  257. }
  258. } else {
  259. const fileIds =
  260. data.scene.elements?.reduce((acc, element) => {
  261. if (isInitializedImageElement(element)) {
  262. return acc.concat(element.fileId);
  263. }
  264. return acc;
  265. }, [] as FileId[]) || [];
  266. if (data.isExternalScene) {
  267. loadFilesFromFirebase(
  268. `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
  269. data.key,
  270. fileIds,
  271. ).then(({ loadedFiles, erroredFiles }) => {
  272. excalidrawAPI.addFiles(loadedFiles);
  273. updateStaleImageStatuses({
  274. excalidrawAPI,
  275. erroredFiles,
  276. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  277. });
  278. });
  279. } else if (isInitialLoad) {
  280. if (fileIds.length) {
  281. LocalData.fileStorage
  282. .getFiles(fileIds)
  283. .then(({ loadedFiles, erroredFiles }) => {
  284. if (loadedFiles.length) {
  285. excalidrawAPI.addFiles(loadedFiles);
  286. }
  287. updateStaleImageStatuses({
  288. excalidrawAPI,
  289. erroredFiles,
  290. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  291. });
  292. });
  293. }
  294. // on fresh load, clear unused files from IDB (from previous
  295. // session)
  296. LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
  297. }
  298. }
  299. };
  300. initializeScene({ collabAPI }).then((data) => {
  301. loadImages(data, /* isInitialLoad */ true);
  302. initialStatePromiseRef.current.promise.resolve(data.scene);
  303. });
  304. const onHashChange = async (event: HashChangeEvent) => {
  305. event.preventDefault();
  306. const libraryUrlTokens = parseLibraryTokensFromUrl();
  307. if (!libraryUrlTokens) {
  308. initializeScene({ collabAPI }).then((data) => {
  309. loadImages(data);
  310. if (data.scene) {
  311. excalidrawAPI.updateScene({
  312. ...data.scene,
  313. appState: restoreAppState(data.scene.appState, null),
  314. });
  315. }
  316. });
  317. }
  318. };
  319. const titleTimeout = setTimeout(
  320. () => (document.title = APP_NAME),
  321. TITLE_TIMEOUT,
  322. );
  323. const syncData = debounce(() => {
  324. if (isTestEnv()) {
  325. return;
  326. }
  327. if (!document.hidden && !collabAPI.isCollaborating()) {
  328. // don't sync if local state is newer or identical to browser state
  329. if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
  330. const localDataState = importFromLocalStorage();
  331. const username = importUsernameFromLocalStorage();
  332. let langCode = languageDetector.detect() || defaultLang.code;
  333. if (Array.isArray(langCode)) {
  334. langCode = langCode[0];
  335. }
  336. setLangCode(langCode);
  337. excalidrawAPI.updateScene({
  338. ...localDataState,
  339. });
  340. excalidrawAPI.updateLibrary({
  341. libraryItems: getLibraryItemsFromStorage(),
  342. });
  343. collabAPI.setUsername(username || "");
  344. }
  345. if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
  346. const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
  347. const currFiles = excalidrawAPI.getFiles();
  348. const fileIds =
  349. elements?.reduce((acc, element) => {
  350. if (
  351. isInitializedImageElement(element) &&
  352. // only load and update images that aren't already loaded
  353. !currFiles[element.fileId]
  354. ) {
  355. return acc.concat(element.fileId);
  356. }
  357. return acc;
  358. }, [] as FileId[]) || [];
  359. if (fileIds.length) {
  360. LocalData.fileStorage
  361. .getFiles(fileIds)
  362. .then(({ loadedFiles, erroredFiles }) => {
  363. if (loadedFiles.length) {
  364. excalidrawAPI.addFiles(loadedFiles);
  365. }
  366. updateStaleImageStatuses({
  367. excalidrawAPI,
  368. erroredFiles,
  369. elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
  370. });
  371. });
  372. }
  373. }
  374. }
  375. }, SYNC_BROWSER_TABS_TIMEOUT);
  376. const onUnload = () => {
  377. LocalData.flushSave();
  378. };
  379. const visibilityChange = (event: FocusEvent | Event) => {
  380. if (event.type === EVENT.BLUR || document.hidden) {
  381. LocalData.flushSave();
  382. }
  383. if (
  384. event.type === EVENT.VISIBILITY_CHANGE ||
  385. event.type === EVENT.FOCUS
  386. ) {
  387. syncData();
  388. }
  389. };
  390. window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
  391. window.addEventListener(EVENT.UNLOAD, onUnload, false);
  392. window.addEventListener(EVENT.BLUR, visibilityChange, false);
  393. document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
  394. window.addEventListener(EVENT.FOCUS, visibilityChange, false);
  395. return () => {
  396. window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
  397. window.removeEventListener(EVENT.UNLOAD, onUnload, false);
  398. window.removeEventListener(EVENT.BLUR, visibilityChange, false);
  399. window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
  400. document.removeEventListener(
  401. EVENT.VISIBILITY_CHANGE,
  402. visibilityChange,
  403. false,
  404. );
  405. clearTimeout(titleTimeout);
  406. };
  407. }, [collabAPI, excalidrawAPI]);
  408. useEffect(() => {
  409. const unloadHandler = (event: BeforeUnloadEvent) => {
  410. LocalData.flushSave();
  411. if (
  412. excalidrawAPI &&
  413. LocalData.fileStorage.shouldPreventUnload(
  414. excalidrawAPI.getSceneElements(),
  415. )
  416. ) {
  417. preventUnload(event);
  418. }
  419. };
  420. window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
  421. return () => {
  422. window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
  423. };
  424. }, [excalidrawAPI]);
  425. useEffect(() => {
  426. languageDetector.cacheUserLanguage(langCode);
  427. }, [langCode]);
  428. const onChange = (
  429. elements: readonly ExcalidrawElement[],
  430. appState: AppState,
  431. files: BinaryFiles,
  432. ) => {
  433. if (collabAPI?.isCollaborating()) {
  434. collabAPI.syncElements(elements);
  435. }
  436. // this check is redundant, but since this is a hot path, it's best
  437. // not to evaludate the nested expression every time
  438. if (!LocalData.isSavePaused()) {
  439. LocalData.save(elements, appState, files, () => {
  440. if (excalidrawAPI) {
  441. let didChange = false;
  442. const elements = excalidrawAPI
  443. .getSceneElementsIncludingDeleted()
  444. .map((element) => {
  445. if (
  446. LocalData.fileStorage.shouldUpdateImageElementStatus(element)
  447. ) {
  448. const newElement = newElementWith(element, { status: "saved" });
  449. if (newElement !== element) {
  450. didChange = true;
  451. }
  452. return newElement;
  453. }
  454. return element;
  455. });
  456. if (didChange) {
  457. excalidrawAPI.updateScene({
  458. elements,
  459. });
  460. }
  461. }
  462. });
  463. }
  464. };
  465. const onExportToBackend = async (
  466. exportedElements: readonly NonDeletedExcalidrawElement[],
  467. appState: AppState,
  468. files: BinaryFiles,
  469. canvas: HTMLCanvasElement | null,
  470. ) => {
  471. if (exportedElements.length === 0) {
  472. return window.alert(t("alerts.cannotExportEmptyCanvas"));
  473. }
  474. if (canvas) {
  475. try {
  476. await exportToBackend(
  477. exportedElements,
  478. {
  479. ...appState,
  480. viewBackgroundColor: appState.exportBackground
  481. ? appState.viewBackgroundColor
  482. : getDefaultAppState().viewBackgroundColor,
  483. },
  484. files,
  485. );
  486. } catch (error: any) {
  487. if (error.name !== "AbortError") {
  488. const { width, height } = canvas;
  489. console.error(error, { width, height });
  490. setErrorMessage(error.message);
  491. }
  492. }
  493. }
  494. };
  495. const renderTopRightUI = useCallback(
  496. (isMobile: boolean, appState: AppState) => {
  497. if (isMobile) {
  498. return null;
  499. }
  500. return (
  501. <div
  502. style={{
  503. width: isExcalidrawPlusSignedUser ? "21ch" : "23ch",
  504. fontSize: "0.7em",
  505. textAlign: "center",
  506. }}
  507. >
  508. {isExcalidrawPlusSignedUser ? PlusAppLinkJSX : PlusLPLinkJSX}
  509. </div>
  510. );
  511. },
  512. [],
  513. );
  514. const renderFooter = useCallback(
  515. (isMobile: boolean) => {
  516. const renderEncryptedIcon = () => (
  517. <a
  518. className="encrypted-icon tooltip"
  519. href="https://blog.excalidraw.com/end-to-end-encryption/"
  520. target="_blank"
  521. rel="noopener noreferrer"
  522. aria-label={t("encrypted.link")}
  523. >
  524. <Tooltip label={t("encrypted.tooltip")} long={true}>
  525. {shield}
  526. </Tooltip>
  527. </a>
  528. );
  529. const renderLanguageList = () => (
  530. <LanguageList
  531. onChange={(langCode) => setLangCode(langCode)}
  532. languages={languages}
  533. currentLangCode={langCode}
  534. />
  535. );
  536. if (isMobile) {
  537. const isTinyDevice = window.innerWidth < 362;
  538. return (
  539. <div
  540. style={{
  541. display: "flex",
  542. flexDirection: isTinyDevice ? "column" : "row",
  543. }}
  544. >
  545. <fieldset>
  546. <legend>{t("labels.language")}</legend>
  547. {renderLanguageList()}
  548. </fieldset>
  549. {/* FIXME remove after 2021-05-20 */}
  550. <div
  551. style={{
  552. width: "24ch",
  553. fontSize: "0.7em",
  554. textAlign: "center",
  555. marginTop: isTinyDevice ? 16 : undefined,
  556. marginLeft: "auto",
  557. marginRight: isTinyDevice ? "auto" : undefined,
  558. padding: isExcalidrawPlusSignedUser ? undefined : "4px 2px",
  559. border: isExcalidrawPlusSignedUser
  560. ? undefined
  561. : "1px dashed #aaa",
  562. borderRadius: 12,
  563. }}
  564. >
  565. {isExcalidrawPlusSignedUser ? PlusAppLinkJSX : PlusLPLinkJSX}
  566. </div>
  567. </div>
  568. );
  569. }
  570. return (
  571. <>
  572. {renderEncryptedIcon()}
  573. {renderLanguageList()}
  574. </>
  575. );
  576. },
  577. [langCode],
  578. );
  579. const renderCustomStats = () => {
  580. return (
  581. <CustomStats
  582. setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
  583. />
  584. );
  585. };
  586. const onLibraryChange = async (items: LibraryItems) => {
  587. if (!items.length) {
  588. localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
  589. return;
  590. }
  591. const serializedItems = JSON.stringify(items);
  592. localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
  593. };
  594. const onRoomClose = useCallback(() => {
  595. LocalData.fileStorage.reset();
  596. }, []);
  597. return (
  598. <div
  599. style={{ height: "100%" }}
  600. className={clsx("excalidraw-app", {
  601. "is-collaborating": collabAPI?.isCollaborating(),
  602. })}
  603. >
  604. <Excalidraw
  605. ref={excalidrawRefCallback}
  606. onChange={onChange}
  607. initialData={initialStatePromiseRef.current.promise}
  608. onCollabButtonClick={collabAPI?.onCollabButtonClick}
  609. isCollaborating={collabAPI?.isCollaborating()}
  610. onPointerUpdate={collabAPI?.onPointerUpdate}
  611. UIOptions={{
  612. canvasActions: {
  613. export: {
  614. onExportToBackend,
  615. renderCustomUI: (elements, appState, files) => {
  616. return (
  617. <ExportToExcalidrawPlus
  618. elements={elements}
  619. appState={appState}
  620. files={files}
  621. onError={(error) => {
  622. excalidrawAPI?.updateScene({
  623. appState: {
  624. errorMessage: error.message,
  625. },
  626. });
  627. }}
  628. />
  629. );
  630. },
  631. },
  632. },
  633. }}
  634. renderTopRightUI={renderTopRightUI}
  635. renderFooter={renderFooter}
  636. langCode={langCode}
  637. renderCustomStats={renderCustomStats}
  638. detectScroll={false}
  639. handleKeyboardGlobally={true}
  640. onLibraryChange={onLibraryChange}
  641. autoFocus={true}
  642. />
  643. {excalidrawAPI && (
  644. <CollabWrapper
  645. excalidrawAPI={excalidrawAPI}
  646. onRoomClose={onRoomClose}
  647. />
  648. )}
  649. {errorMessage && (
  650. <ErrorDialog
  651. message={errorMessage}
  652. onClose={() => setErrorMessage("")}
  653. />
  654. )}
  655. </div>
  656. );
  657. };
  658. const ExcalidrawApp = () => {
  659. return (
  660. <TopErrorBoundary>
  661. <CollabContextConsumer>
  662. <ExcalidrawWrapper />
  663. </CollabContextConsumer>
  664. </TopErrorBoundary>
  665. );
  666. };
  667. export default ExcalidrawApp;