summaryrefslogtreecommitdiffstats
path: root/excalidraw-app/share/ShareDialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'excalidraw-app/share/ShareDialog.tsx')
-rw-r--r--excalidraw-app/share/ShareDialog.tsx290
1 files changed, 290 insertions, 0 deletions
diff --git a/excalidraw-app/share/ShareDialog.tsx b/excalidraw-app/share/ShareDialog.tsx
new file mode 100644
index 0000000..bba7e36
--- /dev/null
+++ b/excalidraw-app/share/ShareDialog.tsx
@@ -0,0 +1,290 @@
+import { useEffect, useRef, useState } from "react";
+import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
+import { trackEvent } from "@excalidraw/excalidraw/analytics";
+import { getFrame } from "@excalidraw/excalidraw/utils";
+import { useI18n } from "@excalidraw/excalidraw/i18n";
+import { KEYS } from "@excalidraw/excalidraw/keys";
+import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
+import {
+ copyIcon,
+ LinkIcon,
+ playerPlayIcon,
+ playerStopFilledIcon,
+ share,
+ shareIOS,
+ shareWindows,
+} from "@excalidraw/excalidraw/components/icons";
+import { TextField } from "@excalidraw/excalidraw/components/TextField";
+import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
+import type { CollabAPI } from "../collab/Collab";
+import { activeRoomLinkAtom } from "../collab/Collab";
+import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
+import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
+import { atom, useAtom, useAtomValue } from "../app-jotai";
+
+import "./ShareDialog.scss";
+
+type OnExportToBackend = () => void;
+type ShareDialogType = "share" | "collaborationOnly";
+
+export const shareDialogStateAtom = atom<
+ { isOpen: false } | { isOpen: true; type: ShareDialogType }
+>({ isOpen: false });
+
+const getShareIcon = () => {
+ const navigator = window.navigator as any;
+ const isAppleBrowser = /Apple/.test(navigator.vendor);
+ const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1;
+
+ if (isAppleBrowser) {
+ return shareIOS;
+ } else if (isWindowsBrowser) {
+ return shareWindows;
+ }
+
+ return share;
+};
+
+export type ShareDialogProps = {
+ collabAPI: CollabAPI | null;
+ handleClose: () => void;
+ onExportToBackend: OnExportToBackend;
+ type: ShareDialogType;
+};
+
+const ActiveRoomDialog = ({
+ collabAPI,
+ activeRoomLink,
+ handleClose,
+}: {
+ collabAPI: CollabAPI;
+ activeRoomLink: string;
+ handleClose: () => void;
+}) => {
+ const { t } = useI18n();
+ const [, setJustCopied] = useState(false);
+ const timerRef = useRef<number>(0);
+ const ref = useRef<HTMLInputElement>(null);
+ const isShareSupported = "share" in navigator;
+ const { onCopy, copyStatus } = useCopyStatus();
+
+ const copyRoomLink = async () => {
+ try {
+ await copyTextToSystemClipboard(activeRoomLink);
+ } catch (e) {
+ collabAPI.setCollabError(t("errors.copyToSystemClipboardFailed"));
+ }
+
+ setJustCopied(true);
+
+ if (timerRef.current) {
+ window.clearTimeout(timerRef.current);
+ }
+
+ timerRef.current = window.setTimeout(() => {
+ setJustCopied(false);
+ }, 3000);
+
+ ref.current?.select();
+ };
+
+ const shareRoomLink = async () => {
+ try {
+ await navigator.share({
+ title: t("roomDialog.shareTitle"),
+ text: t("roomDialog.shareTitle"),
+ url: activeRoomLink,
+ });
+ } catch (error: any) {
+ // Just ignore.
+ }
+ };
+
+ return (
+ <>
+ <h3 className="ShareDialog__active__header">
+ {t("labels.liveCollaboration").replace(/\./g, "")}
+ </h3>
+ <TextField
+ defaultValue={collabAPI.getUsername()}
+ placeholder="Your name"
+ label="Your name"
+ onChange={collabAPI.setUsername}
+ onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()}
+ />
+ <div className="ShareDialog__active__linkRow">
+ <TextField
+ ref={ref}
+ label="Link"
+ readonly
+ fullWidth
+ value={activeRoomLink}
+ />
+ {isShareSupported && (
+ <FilledButton
+ size="large"
+ variant="icon"
+ label="Share"
+ icon={getShareIcon()}
+ className="ShareDialog__active__share"
+ onClick={shareRoomLink}
+ />
+ )}
+ <FilledButton
+ size="large"
+ label={t("buttons.copyLink")}
+ icon={copyIcon}
+ status={copyStatus}
+ onClick={() => {
+ copyRoomLink();
+ onCopy();
+ }}
+ />
+ </div>
+ <div className="ShareDialog__active__description">
+ <p>
+ <span
+ role="img"
+ aria-hidden="true"
+ className="ShareDialog__active__description__emoji"
+ >
+ 🔒{" "}
+ </span>
+ {t("roomDialog.desc_privacy")}
+ </p>
+ <p>{t("roomDialog.desc_exitSession")}</p>
+ </div>
+
+ <div className="ShareDialog__active__actions">
+ <FilledButton
+ size="large"
+ variant="outlined"
+ color="danger"
+ label={t("roomDialog.button_stopSession")}
+ icon={playerStopFilledIcon}
+ onClick={() => {
+ trackEvent("share", "room closed");
+ collabAPI.stopCollaboration();
+ if (!collabAPI.isCollaborating()) {
+ handleClose();
+ }
+ }}
+ />
+ </div>
+ </>
+ );
+};
+
+const ShareDialogPicker = (props: ShareDialogProps) => {
+ const { t } = useI18n();
+
+ const { collabAPI } = props;
+
+ const startCollabJSX = collabAPI ? (
+ <>
+ <div className="ShareDialog__picker__header">
+ {t("labels.liveCollaboration").replace(/\./g, "")}
+ </div>
+
+ <div className="ShareDialog__picker__description">
+ <div style={{ marginBottom: "1em" }}>{t("roomDialog.desc_intro")}</div>
+ {t("roomDialog.desc_privacy")}
+ </div>
+
+ <div className="ShareDialog__picker__button">
+ <FilledButton
+ size="large"
+ label={t("roomDialog.button_startSession")}
+ icon={playerPlayIcon}
+ onClick={() => {
+ trackEvent("share", "room creation", `ui (${getFrame()})`);
+ collabAPI.startCollaboration(null);
+ }}
+ />
+ </div>
+
+ {props.type === "share" && (
+ <div className="ShareDialog__separator">
+ <span>{t("shareDialog.or")}</span>
+ </div>
+ )}
+ </>
+ ) : null;
+
+ return (
+ <>
+ {startCollabJSX}
+
+ {props.type === "share" && (
+ <>
+ <div className="ShareDialog__picker__header">
+ {t("exportDialog.link_title")}
+ </div>
+ <div className="ShareDialog__picker__description">
+ {t("exportDialog.link_details")}
+ </div>
+
+ <div className="ShareDialog__picker__button">
+ <FilledButton
+ size="large"
+ label={t("exportDialog.link_button")}
+ icon={LinkIcon}
+ onClick={async () => {
+ await props.onExportToBackend();
+ props.handleClose();
+ }}
+ />
+ </div>
+ </>
+ )}
+ </>
+ );
+};
+
+const ShareDialogInner = (props: ShareDialogProps) => {
+ const activeRoomLink = useAtomValue(activeRoomLinkAtom);
+
+ return (
+ <Dialog size="small" onCloseRequest={props.handleClose} title={false}>
+ <div className="ShareDialog">
+ {props.collabAPI && activeRoomLink ? (
+ <ActiveRoomDialog
+ collabAPI={props.collabAPI}
+ activeRoomLink={activeRoomLink}
+ handleClose={props.handleClose}
+ />
+ ) : (
+ <ShareDialogPicker {...props} />
+ )}
+ </div>
+ </Dialog>
+ );
+};
+
+export const ShareDialog = (props: {
+ collabAPI: CollabAPI | null;
+ onExportToBackend: OnExportToBackend;
+}) => {
+ const [shareDialogState, setShareDialogState] = useAtom(shareDialogStateAtom);
+
+ const { openDialog } = useUIAppState();
+
+ useEffect(() => {
+ if (openDialog) {
+ setShareDialogState({ isOpen: false });
+ }
+ }, [openDialog, setShareDialogState]);
+
+ if (!shareDialogState.isOpen) {
+ return null;
+ }
+
+ return (
+ <ShareDialogInner
+ handleClose={() => setShareDialogState({ isOpen: false })}
+ collabAPI={props.collabAPI}
+ onExportToBackend={props.onExportToBackend}
+ type={shareDialogState.type}
+ />
+ );
+};