123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- import { trackEvent } from "@excalidraw/excalidraw/analytics";
- import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
- import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
- import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
- import { TextField } from "@excalidraw/excalidraw/components/TextField";
- import {
- copyIcon,
- LinkIcon,
- playerPlayIcon,
- playerStopFilledIcon,
- share,
- shareIOS,
- shareWindows,
- } from "@excalidraw/excalidraw/components/icons";
- import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
- import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
- import { useI18n } from "@excalidraw/excalidraw/i18n";
- import { KEYS, getFrame } from "@excalidraw/common";
- import { useEffect, useRef, useState } from "react";
- import { atom, useAtom, useAtomValue } from "../app-jotai";
- import { activeRoomLinkAtom } from "../collab/Collab";
- import "./ShareDialog.scss";
- import type { CollabAPI } from "../collab/Collab";
- 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}
- />
- );
- };
|