Compare commits

...

2 Commits

Author SHA1 Message Date
Aakansha Doshi
bba0117377 fix 2021-02-20 02:22:40 +05:30
Aakansha Doshi
dcedba88a2 feat: add shareable viewonly links 2021-02-20 01:56:47 +05:30
8 changed files with 71 additions and 23 deletions

View File

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

View File

@ -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);

View File

@ -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
}

View File

@ -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(

View File

@ -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") {

View File

@ -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,18 @@ function ExcalidrawWrapper() {
}
initializeScene({ collabAPI }).then((scene) => {
if (scene?.appState?.viewModeEnabled) {
setViewModeEnabled(true);
}
initialStatePromiseRef.current.promise.resolve(scene);
});
const onHashChange = (_: HashChangeEvent) => {
initializeScene({ collabAPI }).then((scene) => {
if (scene) {
if (scene?.appState?.viewModeEnabled) {
setViewModeEnabled(true);
}
excalidrawAPI.updateScene(scene);
}
});
@ -224,6 +233,7 @@ function ExcalidrawWrapper() {
const onExportToBackend = async (
exportedElements: readonly NonDeletedExcalidrawElement[],
viewonly: boolean,
appState: AppState,
canvas: HTMLCanvasElement | null,
) => {
@ -232,12 +242,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;
@ -272,7 +286,6 @@ function ExcalidrawWrapper() {
},
[langCode],
);
return (
<>
<Excalidraw
@ -287,6 +300,7 @@ function ExcalidrawWrapper() {
onExportToBackend={onExportToBackend}
renderFooter={renderFooter}
langCode={langCode}
viewModeEnabled={viewModeEnabled ? viewModeEnabled : undefined}
/>
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
{errorMessage && (

View File

@ -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",

View File

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