index.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import React, { useState, useLayoutEffect, useEffect } from "react";
  2. import ReactDOM from "react-dom";
  3. import * as Sentry from "@sentry/browser";
  4. import * as SentryIntegrations from "@sentry/integrations";
  5. import { EVENT } from "./constants";
  6. import { TopErrorBoundary } from "./components/TopErrorBoundary";
  7. import Excalidraw from "./excalidraw-embed/index";
  8. import { register as registerServiceWorker } from "./serviceWorker";
  9. import { loadFromBlob } from "./data";
  10. import { debounce } from "./utils";
  11. import {
  12. importFromLocalStorage,
  13. importUsernameFromLocalStorage,
  14. saveUsernameToLocalStorage,
  15. saveToLocalStorage,
  16. } from "./data/localStorage";
  17. import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./time_constants";
  18. import { DataState } from "./data/types";
  19. import { LoadingMessage } from "./components/LoadingMessage";
  20. import { ExcalidrawElement } from "./element/types";
  21. import { AppState } from "./types";
  22. // On Apple mobile devices add the proprietary app icon and splashscreen markup.
  23. // No one should have to do this manually, and eventually this annoyance will
  24. // go away once https://bugs.webkit.org/show_bug.cgi?id=183937 is fixed.
  25. if (
  26. /\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent) &&
  27. !matchMedia("(display-mode: standalone)").matches
  28. ) {
  29. import(/* webpackChunkName: "pwacompat" */ "pwacompat");
  30. }
  31. const SentryEnvHostnameMap: { [key: string]: string } = {
  32. "excalidraw.com": "production",
  33. "vercel.app": "staging",
  34. };
  35. const REACT_APP_DISABLE_SENTRY =
  36. process.env.REACT_APP_DISABLE_SENTRY === "true";
  37. const REACT_APP_GIT_SHA = process.env.REACT_APP_GIT_SHA as string;
  38. // Disable Sentry locally or inside the Docker to avoid noise/respect privacy
  39. const onlineEnv =
  40. !REACT_APP_DISABLE_SENTRY &&
  41. Object.keys(SentryEnvHostnameMap).find(
  42. (item) => window.location.hostname.indexOf(item) >= 0,
  43. );
  44. Sentry.init({
  45. dsn: onlineEnv
  46. ? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
  47. : undefined,
  48. environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
  49. release: REACT_APP_GIT_SHA,
  50. ignoreErrors: [
  51. "undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
  52. ],
  53. integrations: [
  54. new SentryIntegrations.CaptureConsole({
  55. levels: ["error"],
  56. }),
  57. ],
  58. beforeSend(event) {
  59. if (event.request?.url) {
  60. event.request.url = event.request.url.replace(/#.*$/, "");
  61. }
  62. return event;
  63. },
  64. });
  65. window.__EXCALIDRAW_SHA__ = REACT_APP_GIT_SHA;
  66. const saveDebounced = debounce(
  67. (elements: readonly ExcalidrawElement[], state: AppState) => {
  68. saveToLocalStorage(elements, state);
  69. },
  70. SAVE_TO_LOCAL_STORAGE_TIMEOUT,
  71. );
  72. const onUsernameChange = (username: string) => {
  73. saveUsernameToLocalStorage(username);
  74. };
  75. const onBlur = () => {
  76. saveDebounced.flush();
  77. };
  78. function ExcalidrawApp() {
  79. // dimensions
  80. // ---------------------------------------------------------------------------
  81. const [dimensions, setDimensions] = useState({
  82. width: window.innerWidth,
  83. height: window.innerHeight,
  84. });
  85. useLayoutEffect(() => {
  86. const onResize = () => {
  87. setDimensions({
  88. width: window.innerWidth,
  89. height: window.innerHeight,
  90. });
  91. };
  92. window.addEventListener("resize", onResize);
  93. return () => window.removeEventListener("resize", onResize);
  94. }, []);
  95. // initial state
  96. // ---------------------------------------------------------------------------
  97. const [initialState, setInitialState] = useState<{
  98. data: DataState;
  99. user: {
  100. name: string | null;
  101. };
  102. } | null>(null);
  103. useEffect(() => {
  104. setInitialState({
  105. data: importFromLocalStorage(),
  106. user: {
  107. name: importUsernameFromLocalStorage(),
  108. },
  109. });
  110. }, []);
  111. // blur/unload
  112. // ---------------------------------------------------------------------------
  113. useEffect(() => {
  114. window.addEventListener(EVENT.UNLOAD, onBlur, false);
  115. window.addEventListener(EVENT.BLUR, onBlur, false);
  116. return () => {
  117. window.removeEventListener(EVENT.UNLOAD, onBlur, false);
  118. window.removeEventListener(EVENT.BLUR, onBlur, false);
  119. };
  120. }, []);
  121. // ---------------------------------------------------------------------------
  122. if (!initialState) {
  123. return <LoadingMessage />;
  124. }
  125. return (
  126. <TopErrorBoundary>
  127. <Excalidraw
  128. width={dimensions.width}
  129. height={dimensions.height}
  130. onChange={saveDebounced}
  131. initialData={initialState.data}
  132. user={initialState.user}
  133. onUsernameChange={onUsernameChange}
  134. />
  135. </TopErrorBoundary>
  136. );
  137. }
  138. const rootElement = document.getElementById("root");
  139. ReactDOM.render(<ExcalidrawApp />, rootElement);
  140. registerServiceWorker({
  141. onUpdate: (registration) => {
  142. const waitingServiceWorker = registration.waiting;
  143. if (waitingServiceWorker) {
  144. waitingServiceWorker.addEventListener(
  145. EVENT.STATE_CHANGE,
  146. (event: Event) => {
  147. const target = event.target as ServiceWorker;
  148. const state = target.state as ServiceWorkerState;
  149. if (state === "activated") {
  150. window.location.reload();
  151. }
  152. },
  153. );
  154. waitingServiceWorker.postMessage({ type: "SKIP_WAITING" });
  155. }
  156. },
  157. });
  158. if ("launchQueue" in window && "LaunchParams" in window) {
  159. (window as any).launchQueue.setConsumer(
  160. async (launchParams: { files: any[] }) => {
  161. if (!launchParams.files.length) {
  162. return;
  163. }
  164. const fileHandle = launchParams.files[0];
  165. const blob = await fileHandle.getFile();
  166. loadFromBlob(blob);
  167. },
  168. );
  169. }