import invariant from "invariant"; import debounce from "lodash/debounce"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; import { CopyIcon, GlobeIcon, InfoIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { toast } from "sonner"; import styled, { useTheme } from "styled-components"; import Squircle from "@shared/components/Squircle"; import { s } from "@shared/styles"; import { UrlHelper } from "@shared/utils/UrlHelper"; import Document from "~/models/Document"; import Share from "~/models/Share"; import Input, { NativeInput } from "~/components/Input"; import Switch from "~/components/Switch"; import env from "~/env"; import usePolicy from "~/hooks/usePolicy"; import useStores from "~/hooks/useStores"; import { AvatarSize } from "../../Avatar/Avatar"; import CopyToClipboard from "../../CopyToClipboard"; import NudeButton from "../../NudeButton"; import { ResizingHeightContainer } from "../../ResizingHeightContainer"; import Text from "../../Text"; import Tooltip from "../../Tooltip"; import { ListItem } from "../components/ListItem"; type Props = { /** The document to share. */ document: Document; /** The existing share model, if any. */ share: Share | null | undefined; /** The existing share parent model, if any. */ sharedParent: Share | null | undefined; /** Ref to the Copy Link button */ copyButtonRef?: React.RefObject; onRequestClose?: () => void; }; function PublicAccess({ document, share, sharedParent }: Props) { const { shares } = useStores(); const { t } = useTranslation(); const theme = useTheme(); const [validationError, setValidationError] = React.useState(""); const [urlId, setUrlId] = React.useState(share?.urlId); const inputRef = React.useRef(null); const can = usePolicy(share); const documentAbilities = usePolicy(document); const canPublish = can.update && documentAbilities.share; React.useEffect(() => { setUrlId(share?.urlId); }, [share?.urlId]); const handlePublishedChange = React.useCallback( async (event) => { const share = shares.getByDocumentId(document.id); invariant(share, "Share must exist"); try { await share.save({ published: event.currentTarget.checked, }); } catch (err) { toast.error(err.message); } }, [document.id, shares] ); const handleUrlChange = React.useMemo( () => debounce(async (ev) => { if (!share) { return; } const val = ev.target.value; setUrlId(val); if (val && !UrlHelper.SHARE_URL_SLUG_REGEX.test(val)) { setValidationError( t("Only lowercase letters, digits and dashes allowed") ); } else { setValidationError(""); if (share.urlId !== val) { try { await share.save({ urlId: isEmpty(val) ? null : val, }); } catch (err) { if (err.message.includes("must be unique")) { setValidationError(t("Sorry, this link has already been used")); } } } } }, 500), [t, share] ); const handleCopied = React.useCallback(() => { toast.success(t("Public link copied to clipboard")); }, [t]); const documentTitle = sharedParent?.documentTitle; const shareUrl = sharedParent?.url ? `${sharedParent.url}${document.url}` : share?.url ?? ""; const copyButton = ( ); return ( {sharedParent && !document.isDraft ? ( Anyone with the link can access because the parent document,{" "} {{ documentTitle }} , is shared ) : ( t("Allow anyone with the link to access") )} } image={ } actions={ sharedParent && !document.isDraft ? null : ( ) } /> {sharedParent?.published ? ( {copyButton} ) : share?.published ? ( inputRef.current?.focus()} value={env.URL.replace(/https?:\/\//, "") + "/s/"} /> } > {copyButton} ) : null} {share?.published && !share.includeChildDocuments ? ( {t( "Nested documents are not shared on the web. Toggle sharing to enable access, this will be the default behavior in the future" )} . ) : null} ); } const StyledInfoIcon = styled(InfoIcon)` vertical-align: bottom; margin-right: 2px; `; const Wrapper = styled.div` margin-bottom: 8px; `; const DomainPrefix = styled(NativeInput)` flex: 0 1 auto; padding-right: 0 !important; cursor: text; color: ${s("placeholder")}; user-select: none; `; const ShareLinkInput = styled(Input)` margin-top: 12px; min-width: 100px; flex: 1; ${NativeInput}:not(:first-child) { padding: 4px 8px 4px 0; flex: 1; } `; const StyledLink = styled(Link)` color: ${s("textSecondary")}; text-decoration: underline; `; export default observer(PublicAccess);