diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js index e58f568a..5dacea32 100644 --- a/app/menus/DocumentMenu.js +++ b/app/menus/DocumentMenu.js @@ -9,7 +9,6 @@ import styled from "styled-components"; import Document from "models/Document"; import DocumentDelete from "scenes/DocumentDelete"; import DocumentMove from "scenes/DocumentMove"; -import DocumentShare from "scenes/DocumentShare"; import DocumentTemplatize from "scenes/DocumentTemplatize"; import CollectionIcon from "components/CollectionIcon"; import ContextMenu from "components/ContextMenu"; @@ -17,7 +16,6 @@ import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import Template from "components/ContextMenu/Template"; import Flex from "components/Flex"; import Modal from "components/Modal"; -import useCurrentTeam from "hooks/useCurrentTeam"; import useStores from "hooks/useStores"; import getDataTransferFiles from "utils/getDataTransferFiles"; import { @@ -52,7 +50,6 @@ function DocumentMenu({ onOpen, onClose, }: Props) { - const team = useCurrentTeam(); const { policies, collections, ui, documents } = useStores(); const menu = useMenuState({ modal, @@ -66,7 +63,6 @@ function DocumentMenu({ const [showDeleteModal, setShowDeleteModal] = React.useState(false); const [showMoveModal, setShowMoveModal] = React.useState(false); const [showTemplateModal, setShowTemplateModal] = React.useState(false); - const [showShareModal, setShowShareModal] = React.useState(false); const file = React.useRef(); const handleOpen = React.useCallback(() => { @@ -133,17 +129,8 @@ function DocumentMenu({ [document] ); - const handleShareLink = React.useCallback( - async (ev: SyntheticEvent<>) => { - await document.share(); - setShowShareModal(true); - }, - [document] - ); - const collection = collections.get(document.collectionId); const can = policies.abilities(document.id); - const canShareDocuments = !!(can.share && team.sharing); const canViewHistory = can.read && !can.restore; const stopPropagation = React.useCallback((ev: SyntheticEvent<>) => { @@ -290,11 +277,6 @@ function DocumentMenu({ onClick: handleStar, visible: !document.isStarred && !!can.star, }, - { - title: `${t("Share link")}…`, - onClick: handleShareLink, - visible: canShareDocuments, - }, { title: t("Enable embeds"), onClick: document.enableEmbeds, @@ -414,16 +396,6 @@ function DocumentMenu({ onSubmit={() => setShowTemplateModal(false)} /> - setShowShareModal(false)} - isOpen={showShareModal} - > - setShowShareModal(false)} - /> - )} diff --git a/app/scenes/Document/components/Header.js b/app/scenes/Document/components/Header.js index 9a621d71..e7292fea 100644 --- a/app/scenes/Document/components/Header.js +++ b/app/scenes/Document/components/Header.js @@ -3,16 +3,14 @@ import { observer } from "mobx-react"; import { TableOfContentsIcon, EditIcon, - GlobeIcon, PlusIcon, MoreIcon, } from "outline-icons"; import * as React from "react"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import styled from "styled-components"; import Document from "models/Document"; -import DocumentShare from "scenes/DocumentShare"; import { Action, Separator } from "components/Actions"; import Badge from "components/Badge"; import Breadcrumb, { Slash } from "components/Breadcrumb"; @@ -20,8 +18,8 @@ import Button from "components/Button"; import Collaborators from "components/Collaborators"; import Fade from "components/Fade"; import Header from "components/Header"; -import Modal from "components/Modal"; import Tooltip from "components/Tooltip"; +import ShareButton from "./ShareButton"; import useMobile from "hooks/useMobile"; import useStores from "hooks/useStores"; import DocumentMenu from "menus/DocumentMenu"; @@ -61,9 +59,8 @@ function DocumentHeader({ onSave, }: Props) { const { t } = useTranslation(); - const { auth, ui, shares, policies } = useStores(); + const { auth, ui, policies } = useStores(); const isMobile = useMobile(); - const [showShareModal, setShowShareModal] = React.useState(false); const handleSave = React.useCallback(() => { onSave({ done: true }); @@ -73,21 +70,6 @@ function DocumentHeader({ onSave({ done: true, publish: true }); }, [onSave]); - const handleShareLink = React.useCallback( - async (ev: SyntheticEvent<>) => { - await document.share(); - - setShowShareModal(true); - }, - [document] - ); - - const handleCloseShareModal = React.useCallback(() => { - setShowShareModal(false); - }, []); - - const share = shares.getByDocumentId(document.id); - const isPubliclyShared = share && share.published; const isNew = document.isNew; const isTemplate = document.isTemplate; const can = policies.abilities(document.id); @@ -146,13 +128,6 @@ function DocumentHeader({ return ( <> - - -
@@ -186,28 +161,7 @@ function DocumentHeader({ )} {!isEditing && canShareDocument && (!isMobile || !isTemplate) && ( - - Anyone with the link
- can view this document - - ) : ( - "" - ) - } - delay={500} - placement="bottom" - > - -
+
)} {isEditing && ( diff --git a/app/scenes/Document/components/ShareButton.js b/app/scenes/Document/components/ShareButton.js new file mode 100644 index 00000000..411c6222 --- /dev/null +++ b/app/scenes/Document/components/ShareButton.js @@ -0,0 +1,82 @@ +// @flow +import { observer } from "mobx-react"; +import { GlobeIcon } from "outline-icons"; +import * as React from "react"; +import { useTranslation, Trans } from "react-i18next"; +import { usePopoverState, Popover, PopoverDisclosure } from "reakit/Popover"; +import styled from "styled-components"; +import { fadeAndScaleIn } from "shared/styles/animations"; +import Document from "models/Document"; +import Button from "components/Button"; +import Tooltip from "components/Tooltip"; +import SharePopover from "./SharePopover"; +import useStores from "hooks/useStores"; + +type Props = {| + document: Document, +|}; + +function ShareButton({ document }: Props) { + const { t } = useTranslation(); + const { shares } = useStores(); + const share = shares.getByDocumentId(document.id); + const isPubliclyShared = share && share.published; + const popover = usePopoverState({ + gutter: 0, + placement: "bottom-end", + }); + + return ( + <> + + {(props) => ( + + Anyone with the link
+ can view this document + + ) : ( + "" + ) + } + delay={500} + placement="bottom" + > + +
+ )} +
+ + + + + + + ); +} + +const Contents = styled.div` + animation: ${fadeAndScaleIn} 200ms ease; + transform-origin: 75% 0; + background: ${(props) => props.theme.menuBackground}; + border-radius: 6px; + padding: 24px 24px 12px; + width: 380px; + box-shadow: ${(props) => props.theme.menuShadow}; + border: ${(props) => + props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"}; +`; + +export default observer(ShareButton); diff --git a/app/scenes/Document/components/SharePopover.js b/app/scenes/Document/components/SharePopover.js new file mode 100644 index 00000000..9897d245 --- /dev/null +++ b/app/scenes/Document/components/SharePopover.js @@ -0,0 +1,155 @@ +// @flow +import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; +import invariant from "invariant"; +import { observer } from "mobx-react"; +import { GlobeIcon, PadlockIcon } from "outline-icons"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; +import Document from "models/Document"; +import Share from "models/Share"; +import Button from "components/Button"; +import CopyToClipboard from "components/CopyToClipboard"; +import Flex from "components/Flex"; +import HelpText from "components/HelpText"; +import Input from "components/Input"; +import Switch from "components/Switch"; +import useStores from "hooks/useStores"; + +type Props = {| + document: Document, + share: Share, + onSubmit: () => void, +|}; + +function DocumentShare({ document, share, onSubmit }: Props) { + const { t } = useTranslation(); + const { policies, shares, ui } = useStores(); + const [isCopied, setIsCopied] = React.useState(false); + const [isSaving, setIsSaving] = React.useState(false); + const timeout = React.useRef(); + const can = policies.abilities(share ? share.id : ""); + const canPublish = can.update && !document.isTemplate; + + React.useEffect(() => { + document.share(); + return () => clearTimeout(timeout.current); + }, [document]); + + const handlePublishedChange = React.useCallback( + async (event) => { + const share = shares.getByDocumentId(document.id); + invariant(share, "Share must exist"); + + setIsSaving(true); + + try { + await share.save({ published: event.currentTarget.checked }); + } catch (err) { + ui.showToast(err.message, { type: "error" }); + } finally { + setIsSaving(false); + } + }, + [document.id, shares, ui] + ); + + const handleCopied = React.useCallback(() => { + setIsCopied(true); + + timeout.current = setTimeout(() => { + setIsCopied(false); + onSubmit(); + + ui.showToast(t("Share link copied"), { type: "info" }); + }, 250); + }, [t, onSubmit, ui]); + + return ( + <> + + {share && share.published ? ( + + ) : ( + + )}{" "} + {t("Share this document")} + + + {canPublish && ( + + + + + {share.published + ? t("Anyone with the link can view this document") + : t("Only team members with access can view")} + {share.lastAccessedAt && ( + <> + .{" "} + {t("The shared link was last accessed {{ timeAgo }}.", { + timeAgo: distanceInWordsToNow(share.lastAccessedAt, { + addSuffix: true, + }), + })} + + )} + + + + )} + + + + + + + + ); +} + +const Heading = styled.h2` + display: flex; + align-items: center; + margin-top: 0; + margin-left: -4px; +`; + +const PrivacySwitch = styled.div` + margin: 20px 0; +`; + +const InputLink = styled(Input)` + flex-grow: 1; + margin-right: 8px; +`; + +const Privacy = styled(Flex)` + flex-align: center; + + svg { + flex-shrink: 0; + } +`; + +const PrivacyText = styled(HelpText)` + margin: 0; + font-size: 15px; +`; + +export default observer(DocumentShare); diff --git a/app/scenes/DocumentShare.js b/app/scenes/DocumentShare.js deleted file mode 100644 index afd8992a..00000000 --- a/app/scenes/DocumentShare.js +++ /dev/null @@ -1,137 +0,0 @@ -// @flow -import invariant from "invariant"; -import { observable } from "mobx"; -import { observer, inject } from "mobx-react"; -import { GlobeIcon, PadlockIcon } from "outline-icons"; -import * as React from "react"; -import { Link } from "react-router-dom"; -import styled from "styled-components"; -import PoliciesStore from "stores/PoliciesStore"; -import SharesStore from "stores/SharesStore"; -import UiStore from "stores/UiStore"; -import Document from "models/Document"; -import Button from "components/Button"; -import CopyToClipboard from "components/CopyToClipboard"; -import Flex from "components/Flex"; -import HelpText from "components/HelpText"; -import Input from "components/Input"; -import Switch from "components/Switch"; - -type Props = { - document: Document, - shares: SharesStore, - ui: UiStore, - policies: PoliciesStore, - onSubmit: () => void, -}; - -@observer -class DocumentShare extends React.Component { - @observable isCopied: boolean; - @observable isSaving: boolean = false; - timeout: TimeoutID; - - componentWillUnmount() { - clearTimeout(this.timeout); - } - - handlePublishedChange = async (event) => { - const { document, shares } = this.props; - const share = shares.getByDocumentId(document.id); - invariant(share, "Share must exist"); - - this.isSaving = true; - - try { - await share.save({ published: event.currentTarget.checked }); - } catch (err) { - this.props.ui.showToast(err.message, { type: "error" }); - } finally { - this.isSaving = false; - } - }; - - handleCopied = () => { - this.isCopied = true; - - this.timeout = setTimeout(() => { - this.isCopied = false; - this.props.onSubmit(); - }, 1500); - }; - - render() { - const { document, policies, shares, onSubmit } = this.props; - const share = shares.getByDocumentId(document.id); - const can = policies.abilities(share ? share.id : ""); - const canPublish = can.update && !document.isTemplate; - - return ( -
- - The link below provides a read-only version of the document{" "} - {document.titleWithDefault}.{" "} - {canPublish - ? "You can optionally make it accessible to anyone with the link." - : "It is only viewable by those that already have access to the collection."}{" "} - - Manage all share links - - . - - {canPublish && ( - <> - - - {share.published ? : } - - {share.published - ? "Anyone with the link can view this document" - : "Only team members with access can view this document"} - - - - )} -
- - - - -     - - Preview - -
- ); - } -} - -const Privacy = styled(Flex)` - flex-align: center; - margin-left: -4px; -`; - -const PrivacyText = styled(HelpText)` - margin: 0; - margin-left: 2px; - font-size: 15px; -`; - -export default inject("shares", "ui", "policies")(DocumentShare); diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index b02dbb31..cd43b0e0 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -159,7 +159,6 @@ "Choose a collection": "Choose a collection", "Unpin": "Unpin", "Pin to collection": "Pin to collection", - "Share link": "Share link", "Enable embeds": "Enable embeds", "Disable embeds": "Disable embeds", "New nested document": "New nested document", @@ -172,7 +171,6 @@ "Print": "Print", "Move {{ documentName }}": "Move {{ documentName }}", "Delete {{ documentName }}": "Delete {{ documentName }}", - "Share document": "Share document", "Edit group": "Edit group", "Delete group": "Delete group", "Group options": "Group options", @@ -272,13 +270,18 @@ "Show contents": "Show contents", "Edit {{noun}}": "Edit {{noun}}", "Archived": "Archived", - "Anyone with the link <1>can view this document": "Anyone with the link <1>can view this document", - "Share": "Share", "Save Draft": "Save Draft", "Done Editing": "Done Editing", "New from template": "New from template", "Publish": "Publish", "Publishing": "Publishing", + "Anyone with the link <1>can view this document": "Anyone with the link <1>can view this document", + "Share": "Share", + "Share this document": "Share this document", + "Publish to internet": "Publish to internet", + "Anyone with the link can view this document": "Anyone with the link can view this document", + "Only team members with access can view": "Only team members with access can view", + "The shared link was last accessed {{ timeAgo }}.": "The shared link was last accessed {{ timeAgo }}.", "Are you sure you want to delete the {{ documentTitle }} template?": "Are you sure you want to delete the {{ documentTitle }} template?", "Are you sure about that? Deleting the {{ documentTitle }} document will delete all of its history and any nested documents.": "Are you sure about that? Deleting the {{ documentTitle }} document will delete all of its history and any nested documents.", "If you’d like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.": "If you’d like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.", diff --git a/shared/styles/theme.js b/shared/styles/theme.js index d02b379f..d1b4bc30 100644 --- a/shared/styles/theme.js +++ b/shared/styles/theme.js @@ -141,7 +141,7 @@ export const light = { menuBackground: colors.white, menuShadow: - "0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.08)", + "0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.08), 0 30px 40px rgb(0 0 0 / 8%)", divider: colors.slateLight, titleBarDivider: colors.slateLight, inputBorder: colors.slateLight,