From bfc2cec7d43eb8eaa46dd3f91084932381257059 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: excalidraw-app/ --- excalidraw-app/share/ShareDialog.tsx | 290 +++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 excalidraw-app/share/ShareDialog.tsx (limited to 'excalidraw-app/share/ShareDialog.tsx') 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(0); + const ref = useRef(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 ( + <> +

+ {t("labels.liveCollaboration").replace(/\./g, "")} +

+ event.key === KEYS.ENTER && handleClose()} + /> +
+ + {isShareSupported && ( + + )} + { + copyRoomLink(); + onCopy(); + }} + /> +
+
+

+ + {t("roomDialog.desc_privacy")} +

+

{t("roomDialog.desc_exitSession")}

+
+ +
+ { + trackEvent("share", "room closed"); + collabAPI.stopCollaboration(); + if (!collabAPI.isCollaborating()) { + handleClose(); + } + }} + /> +
+ + ); +}; + +const ShareDialogPicker = (props: ShareDialogProps) => { + const { t } = useI18n(); + + const { collabAPI } = props; + + const startCollabJSX = collabAPI ? ( + <> +
+ {t("labels.liveCollaboration").replace(/\./g, "")} +
+ +
+
{t("roomDialog.desc_intro")}
+ {t("roomDialog.desc_privacy")} +
+ +
+ { + trackEvent("share", "room creation", `ui (${getFrame()})`); + collabAPI.startCollaboration(null); + }} + /> +
+ + {props.type === "share" && ( +
+ {t("shareDialog.or")} +
+ )} + + ) : null; + + return ( + <> + {startCollabJSX} + + {props.type === "share" && ( + <> +
+ {t("exportDialog.link_title")} +
+
+ {t("exportDialog.link_details")} +
+ +
+ { + await props.onExportToBackend(); + props.handleClose(); + }} + /> +
+ + )} + + ); +}; + +const ShareDialogInner = (props: ShareDialogProps) => { + const activeRoomLink = useAtomValue(activeRoomLinkAtom); + + return ( + +
+ {props.collabAPI && activeRoomLink ? ( + + ) : ( + + )} +
+
+ ); +}; + +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 ( + setShareDialogState({ isOpen: false })} + collabAPI={props.collabAPI} + onExportToBackend={props.onExportToBackend} + type={shareDialogState.type} + /> + ); +}; -- cgit v1.2.3