| 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}    />  );};
 |