You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
7.6 KiB
291 lines
7.6 KiB
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} |
|
/> |
|
); |
|
};
|
|
|