Browse Source

feat: add shareable viewonly links

Aakansha Doshi 4 years ago
parent
commit
dcedba88a2

+ 6 - 0
src/components/ExportDialog.scss

@@ -66,4 +66,10 @@
       overflow-y: auto;
     }
   }
+
+  .shareable-link--viewonly {
+    svg {
+      width: 1.5em;
+    }
+  }
 }

+ 26 - 10
src/components/ExportDialog.tsx

@@ -12,7 +12,7 @@ import { exportToCanvas, getExportSize } from "../scene/export";
 import { AppState } from "../types";
 import { Dialog } from "./Dialog";
 import "./ExportDialog.scss";
-import { clipboard, exportFile, link } from "./icons";
+import { clipboard, exportFile, eyeIcon, link } from "./icons";
 import Stack from "./Stack";
 import { ToolButton } from "./ToolButton";
 
@@ -66,7 +66,10 @@ const ExportModal = ({
   onExportToPng: ExportCB;
   onExportToSvg: ExportCB;
   onExportToClipboard: ExportCB;
-  onExportToBackend?: ExportCB;
+  onExportToBackend?: (
+    elements: readonly NonDeletedExcalidrawElement[],
+    viewonly: boolean,
+  ) => void;
   onCloseRequest: () => void;
 }) => {
   const someElementIsSelected = isSomeElementSelected(elements, appState);
@@ -155,13 +158,23 @@ const ExportModal = ({
               />
             )}
             {onExportToBackend && (
-              <ToolButton
-                type="button"
-                icon={link}
-                title={t("buttons.getShareableLink")}
-                aria-label={t("buttons.getShareableLink")}
-                onClick={() => onExportToBackend(exportedElements)}
-              />
+              <>
+                <ToolButton
+                  type="button"
+                  icon={link}
+                  title={t("buttons.getShareableLink")}
+                  aria-label={t("buttons.getShareableLink")}
+                  onClick={() => onExportToBackend(exportedElements, false)}
+                />
+                <ToolButton
+                  type="button"
+                  icon={eyeIcon}
+                  className="shareable-link--viewonly"
+                  title={t("buttons.getViewonlyShareableLink")}
+                  aria-label={t("buttons.getShareableLink")}
+                  onClick={() => onExportToBackend(exportedElements, true)}
+                />
+              </>
             )}
           </Stack.Row>
           <div className="ExportDialog__name">
@@ -236,7 +249,10 @@ export const ExportDialog = ({
   onExportToPng: ExportCB;
   onExportToSvg: ExportCB;
   onExportToClipboard: ExportCB;
-  onExportToBackend?: ExportCB;
+  onExportToBackend?: (
+    elements: readonly NonDeletedExcalidrawElement[],
+    viewonly: boolean,
+  ) => void;
 }) => {
   const [modalIsShown, setModalIsShown] = useState(false);
   const triggerButton = useRef<HTMLButtonElement>(null);

+ 3 - 2
src/components/LayerUI.tsx

@@ -58,6 +58,7 @@ interface LayerUIProps {
   isCollaborating: boolean;
   onExportToBackend?: (
     exportedElements: readonly NonDeletedExcalidrawElement[],
+    viewonly: boolean,
     appState: AppState,
     canvas: HTMLCanvasElement | null,
   ) => void;
@@ -368,9 +369,9 @@ const LayerUI = ({
         onExportToClipboard={createExporter("clipboard")}
         onExportToBackend={
           onExportToBackend
-            ? (elements) => {
+            ? (elements, viewonly) => {
                 onExportToBackend &&
-                  onExportToBackend(elements, appState, canvas);
+                  onExportToBackend(elements, viewonly, appState, canvas);
               }
             : undefined
         }

+ 4 - 0
src/components/icons.tsx

@@ -113,6 +113,10 @@ export const questionCircle = createIcon(
   { mirror: true },
 );
 
+export const eyeIcon = createIcon(
+  "M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z",
+);
+
 // Icon imported form Storybook
 // Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
 export const resetZoom = createIcon(

+ 6 - 1
src/excalidraw-app/data/index.ts

@@ -251,6 +251,7 @@ export const loadScene = async (
 export const exportToBackend = async (
   elements: readonly ExcalidrawElement[],
   appState: AppState,
+  viewonly: boolean,
 ) => {
   const json = serializeAsJSON(elements, appState);
   const encoded = new TextEncoder().encode(json);
@@ -288,9 +289,13 @@ export const exportToBackend = async (
     const json = await response.json();
     if (json.id) {
       const url = new URL(window.location.href);
+      url.hash = "#";
+      if (viewonly) {
+        url.hash = `${url.hash}viewonly=true&`;
+      }
       // We need to store the key (and less importantly the id) as hash instead
       // of queryParam in order to never send it to the server
-      url.hash = `json=${json.id},${exportedKey.k!}`;
+      url.hash = `${url.hash}json=${json.id},${exportedKey.k!}`;
       const urlString = url.toString();
       window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
     } else if (json.error_class === "RequestTooLargeError") {

+ 20 - 9
src/excalidraw-app/index.tsx

@@ -70,9 +70,8 @@ const initializeScene = async (opts: {
   const searchParams = new URLSearchParams(window.location.search);
   const id = searchParams.get("id");
   const jsonMatch = window.location.hash.match(
-    /^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
+    /json=([0-9]+),([a-zA-Z0-9_-]+)/,
   );
-
   const initialData = importFromLocalStorage();
 
   let scene = await loadScene(null, null, initialData);
@@ -94,6 +93,10 @@ const initializeScene = async (opts: {
       } else if (jsonMatch) {
         scene = await loadScene(jsonMatch[1], jsonMatch[2], initialData);
       }
+      const isViewModeEnabled = !!window.location.hash.match(/#viewonly=true/);
+      if (isViewModeEnabled) {
+        scene.appState.viewModeEnabled = true;
+      }
       if (!roomLinkData) {
         window.history.replaceState({}, APP_NAME, window.location.origin);
       }
@@ -134,6 +137,7 @@ function ExcalidrawWrapper() {
   const [errorMessage, setErrorMessage] = useState("");
   const currentLangCode = languageDetector.detect() || defaultLang.code;
   const [langCode, setLangCode] = useState(currentLangCode);
+  const [viewModeEnabled, setViewModeEnabled] = useState(false);
 
   useLayoutEffect(() => {
     const onResize = () => {
@@ -157,7 +161,6 @@ function ExcalidrawWrapper() {
   if (!initialStatePromiseRef.current.promise) {
     initialStatePromiseRef.current.promise = resolvablePromise<ImportedDataState | null>();
   }
-
   useEffect(() => {
     // Delayed so that the app has a time to load the latest SW
     setTimeout(() => {
@@ -178,12 +181,14 @@ function ExcalidrawWrapper() {
     }
 
     initializeScene({ collabAPI }).then((scene) => {
+      setViewModeEnabled(!!scene?.appState?.viewModeEnabled);
       initialStatePromiseRef.current.promise.resolve(scene);
     });
 
     const onHashChange = (_: HashChangeEvent) => {
       initializeScene({ collabAPI }).then((scene) => {
         if (scene) {
+          setViewModeEnabled(!!scene.appState?.viewModeEnabled);
           excalidrawAPI.updateScene(scene);
         }
       });
@@ -224,6 +229,7 @@ function ExcalidrawWrapper() {
 
   const onExportToBackend = async (
     exportedElements: readonly NonDeletedExcalidrawElement[],
+    viewonly: boolean,
     appState: AppState,
     canvas: HTMLCanvasElement | null,
   ) => {
@@ -232,12 +238,16 @@ function ExcalidrawWrapper() {
     }
     if (canvas) {
       try {
-        await exportToBackend(exportedElements, {
-          ...appState,
-          viewBackgroundColor: appState.exportBackground
-            ? appState.viewBackgroundColor
-            : getDefaultAppState().viewBackgroundColor,
-        });
+        await exportToBackend(
+          exportedElements,
+          {
+            ...appState,
+            viewBackgroundColor: appState.exportBackground
+              ? appState.viewBackgroundColor
+              : getDefaultAppState().viewBackgroundColor,
+          },
+          viewonly,
+        );
       } catch (error) {
         if (error.name !== "AbortError") {
           const { width, height } = canvas;
@@ -287,6 +297,7 @@ function ExcalidrawWrapper() {
         onExportToBackend={onExportToBackend}
         renderFooter={renderFooter}
         langCode={langCode}
+        viewModeEnabled={viewModeEnabled}
       />
       {excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
       {errorMessage && (

+ 1 - 0
src/locales/en.json

@@ -106,6 +106,7 @@
     "saveAs": "Save as",
     "load": "Load",
     "getShareableLink": "Get shareable link",
+    "getViewonlyShareableLink": "Get View only shareable link",
     "close": "Close",
     "selectLanguage": "Select language",
     "scrollBackToContent": "Scroll back to content",

+ 1 - 0
src/types.ts

@@ -180,6 +180,7 @@ export interface ExcalidrawProps {
   }) => void;
   onExportToBackend?: (
     exportedElements: readonly NonDeletedExcalidrawElement[],
+    viewonly: boolean,
     appState: AppState,
     canvas: HTMLCanvasElement | null,
   ) => void;