feat: add shareable viewonly links
This commit is contained in:
parent
74e82d0d7c
commit
dcedba88a2
@ -66,4 +66,10 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.shareable-link--viewonly {
|
||||
svg {
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)}
|
||||
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);
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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") {
|
||||
|
@ -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, {
|
||||
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 && (
|
||||
|
@ -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",
|
||||
|
@ -180,6 +180,7 @@ export interface ExcalidrawProps {
|
||||
}) => void;
|
||||
onExportToBackend?: (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
viewonly: boolean,
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
) => void;
|
||||
|
Loading…
x
Reference in New Issue
Block a user