diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 15ba02b6b8..74328af39b 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -596,7 +596,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -616,14 +616,16 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child { + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not( + .mx_ShareDialog button + ):last-child { margin-right: 0px; } .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -635,7 +637,7 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -648,7 +650,7 @@ legend { .mx_Dialog_buttons button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( .mx_ThemeChoicePanel_CustomTheme button - ):not(.mx_UnpinAllDialog button), + ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -664,7 +666,7 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/views/dialogs/_ShareDialog.pcss b/res/css/views/dialogs/_ShareDialog.pcss index 086222af31..561e0dc20f 100644 --- a/res/css/views/dialogs/_ShareDialog.pcss +++ b/res/css/views/dialogs/_ShareDialog.pcss @@ -5,50 +5,73 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -.mx_ShareDialog hr { - margin-top: 25px; - margin-bottom: 25px; - border-color: $light-fg-color; -} +.mx_ShareDialog { + /* Value from figma design */ + width: 416px; -.mx_ShareDialog .mx_ShareDialog_content { - margin: 10px 0; + .mx_Dialog_header { + text-align: center; + margin-bottom: var(--cpd-space-6x); + /* Override dialog header padding to able to center it */ + padding-inline-end: 0; + } - .mx_CopyableText { - width: unset; /* full width */ + .mx_ShareDialog_content { + display: flex; + flex-direction: column; + gap: var(--cpd-space-6x); + align-items: center; - > a { - text-decoration: none; - flex-shrink: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + .mx_ShareDialog_top { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + align-items: center; + width: 100%; + + span { + text-align: center; + font: var(--cpd-font-body-sm-semibold); + color: var(--cpd-color-text-secondary); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + width: 100%; + } + } + + label { + display: inline-flex; + gap: var(--cpd-space-3x); + justify-content: center; + align-items: center; + font: var(--cpd-font-body-md-medium); + } + + button { + width: 100%; + } + + .mx_ShareDialog_social { + display: flex; + gap: var(--cpd-space-3x); + justify-content: center; + + a { + /* 48px on figma but we need to add the border size */ + width: 46px; + height: 46px; + border-radius: 99px; + border: 1px solid var(--cpd-color-border-interactive-secondary); + display: flex; + justify-content: center; + align-items: center; + + img { + width: 24px; + height: 24px; + } + } } } } - -.mx_ShareDialog_split { - display: flex; - flex-wrap: wrap; -} - -.mx_ShareDialog_qrcode_container { - float: left; - height: 256px; - width: 256px; - margin-right: 64px; -} - -.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { - width: 299px; -} - -.mx_ShareDialog_social_container { - display: inline-block; -} - -.mx_ShareDialog_social_icon { - display: inline-grid; - margin-right: 10px; - margin-bottom: 10px; -} diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index f9382227e4..1796b79239 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -7,22 +7,23 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import * as React from "react"; +import React, { JSX, useMemo, useRef, useState } from "react"; import { Room, RoomMember, MatrixEvent, User } from "matrix-js-sdk/src/matrix"; +import { Checkbox, Button } from "@vector-im/compound-web"; +import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link"; +import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; import { _t } from "../../../languageHandler"; import QRCode from "../elements/QRCode"; import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks"; -import { selectText } from "../../../utils/strings"; -import StyledCheckbox from "../elements/StyledCheckbox"; -import SettingsStore from "../../../settings/SettingsStore"; +import { copyPlaintext } from "../../../utils/strings"; import { UIFeature } from "../../../settings/UIFeature"; import BaseDialog from "./BaseDialog"; -import CopyableText from "../elements/CopyableText"; import { XOR } from "../../../@types/common"; +import { useSettingValue } from "../../../hooks/useSettings.ts"; /* eslint-disable @typescript-eslint/no-require-imports */ -const socials = [ +const SOCIALS = [ { name: "Facebook", img: require("../../../../res/img/social/facebook.png"), @@ -33,11 +34,7 @@ const socials = [ img: require("../../../../res/img/social/twitter-2.png"), url: (url: string) => `https://twitter.com/home?status=${url}`, }, - /* // icon missing - name: 'Google Plus', - img: 'img/social/', - url: (url) => `https://plus.google.com/share?url=${url}`, - },*/ { + { name: "LinkedIn", img: require("../../../../res/img/social/linkedin.png"), url: (url: string) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`, @@ -78,160 +75,153 @@ interface Props extends BaseProps { * A matrix.to link will be generated out of it if it's not already a url. */ target: Room | User | RoomMember | URL; + /** + * Optional when the target is a Room, User, RoomMember or a URL. + * Mandatory when the target is a MatrixEvent. + */ permalinkCreator?: RoomPermalinkCreator; } interface EventProps extends BaseProps { + /** + * The target to link to. + */ target: MatrixEvent; + /** + * Optional when the target is a Room, User, RoomMember or a URL. + * Mandatory when the target is a MatrixEvent. + */ permalinkCreator: RoomPermalinkCreator; } -interface IState { - linkSpecificEvent: boolean; - permalinkCreator: RoomPermalinkCreator | null; +type ShareDialogProps = XOR; + +/** + * A dialog to share a link to a room, user, room member or a matrix event. + */ +export function ShareDialog({ target, customTitle, onFinished, permalinkCreator }: ShareDialogProps): JSX.Element { + const showQrCode = useSettingValue(UIFeature.ShareQRCode); + const showSocials = useSettingValue(UIFeature.ShareSocial); + + const timeoutIdRef = useRef(); + const [isCopied, setIsCopied] = useState(false); + + const [linkToSpecificEvent, setLinkToSpecificEvent] = useState(target instanceof MatrixEvent); + const { title, url, checkboxLabel } = useTargetValues(target, linkToSpecificEvent, permalinkCreator); + const newTitle = customTitle ?? title; + + return ( + + + + {showQrCode && } + {url} + + {checkboxLabel && ( + + setLinkToSpecificEvent(evt.target.checked)} + /> + {checkboxLabel} + + )} + { + clearTimeout(timeoutIdRef.current); + await copyPlaintext(url); + setIsCopied(true); + timeoutIdRef.current = setTimeout(() => setIsCopied(false), 2000); + }} + > + {isCopied ? _t("share|link_copied") : _t("action|copy_link")} + + {showSocials && } + + + ); } -export default class ShareDialog extends React.PureComponent, IState> { - public constructor(props: XOR) { - super(props); +/** + * Social links to share the link on different platforms. + */ +interface SocialLinksProps { + /** + * The URL to share. + */ + url: string; +} - let permalinkCreator: RoomPermalinkCreator | null = null; - if (props.target instanceof Room) { - permalinkCreator = new RoomPermalinkCreator(props.target); - permalinkCreator.load(); +/** + * The socials to share the link on. + */ +function SocialLinks({ url }: SocialLinksProps): JSX.Element { + return ( + + {SOCIALS.map((social) => ( + + + + ))} + + ); +} + +/** + * Get the title, url and checkbox label for the dialog based on the target. + * @param target + * @param linkToSpecificEvent + * @param permalinkCreator + */ +function useTargetValues( + target: ShareDialogProps["target"], + linkToSpecificEvent: boolean, + permalinkCreator?: RoomPermalinkCreator, +): { title: string; url: string; checkboxLabel?: string } { + return useMemo(() => { + if (target instanceof URL) return { title: _t("share|title_link"), url: target.toString() }; + if (target instanceof User || target instanceof RoomMember) + return { + title: _t("share|title_user"), + url: makeUserPermalink(target.userId), + }; + + if (target instanceof Room) { + const title = _t("share|title_room"); + const newPermalinkCreator = new RoomPermalinkCreator(target); + newPermalinkCreator.load(); + + const events = target.getLiveTimeline().getEvents(); + return { + title, + url: linkToSpecificEvent + ? newPermalinkCreator.forEvent(events[events.length - 1].getId()!) + : newPermalinkCreator.forShareableRoom(), + ...(events.length > 0 && { checkboxLabel: _t("share|permalink_most_recent") }), + }; } - this.state = { - // MatrixEvent defaults to share linkSpecificEvent - linkSpecificEvent: this.props.target instanceof MatrixEvent, - permalinkCreator, + // MatrixEvent is remaining and should have a permalinkCreator + const url = linkToSpecificEvent + ? permalinkCreator!.forEvent(target.getId()!) + : permalinkCreator!.forShareableRoom(); + return { + title: _t("share|title_message"), + url, + checkboxLabel: _t("share|permalink_message"), }; - } - - public static onLinkClick(e: React.MouseEvent): void { - e.preventDefault(); - selectText(e.currentTarget); - } - - private onLinkSpecificEventCheckboxClick = (): void => { - this.setState({ - linkSpecificEvent: !this.state.linkSpecificEvent, - }); - }; - - private getUrl(): string { - if (this.props.target instanceof URL) { - return this.props.target.toString(); - } else if (this.props.target instanceof Room) { - if (this.state.linkSpecificEvent) { - const events = this.props.target.getLiveTimeline().getEvents(); - return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!); - } else { - return this.state.permalinkCreator!.forShareableRoom(); - } - } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { - return makeUserPermalink(this.props.target.userId); - } else if (this.state.linkSpecificEvent) { - return this.props.permalinkCreator!.forEvent(this.props.target.getId()!); - } else { - return this.props.permalinkCreator!.forShareableRoom(); - } - } - - public render(): React.ReactNode { - let title: string | undefined; - let checkbox: JSX.Element | undefined; - - if (this.props.target instanceof URL) { - title = this.props.customTitle ?? _t("share|title_link"); - } else if (this.props.target instanceof Room) { - title = this.props.customTitle ?? _t("share|title_room"); - - const events = this.props.target.getLiveTimeline().getEvents(); - if (events.length > 0) { - checkbox = ( - - - {_t("share|permalink_most_recent")} - - - ); - } - } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { - title = this.props.customTitle ?? _t("share|title_user"); - } else if (this.props.target instanceof MatrixEvent) { - title = this.props.customTitle ?? _t("share|title_message"); - checkbox = ( - - - {_t("share|permalink_message")} - - - ); - } - - const matrixToUrl = this.getUrl(); - const encodedUrl = encodeURIComponent(matrixToUrl); - - const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode); - const showSocials = SettingsStore.getValue(UIFeature.ShareSocial); - - let qrSocialSection; - if (showQrCode || showSocials) { - qrSocialSection = ( - <> - - - {showQrCode && ( - - - - )} - {showSocials && ( - - {socials.map((social) => ( - - - - ))} - - )} - - > - ); - } - - return ( - - {this.props.subtitle && {this.props.subtitle}} - - matrixToUrl}> - - {matrixToUrl} - - - {checkbox} - {qrSocialSection} - - - ); - } + }, [target, linkToSpecificEvent, permalinkCreator]); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 50ca4ae1e4..f467bbc714 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2952,7 +2952,7 @@ "warning": "WARNING: " }, "share": { - "link_title": "Link to room", + "link_copied": "Link copied", "permalink_message": "Link to selected message", "permalink_most_recent": "Link to most recent message", "share_call": "Conference invite link", diff --git a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx index cb7d556235..89fec6ec24 100644 --- a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx @@ -14,7 +14,7 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../src/settings/SettingsStore"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import { _t } from "../../../../../src/languageHandler"; -import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; import { UIFeature } from "../../../../../src/settings/UIFeature"; import { stubClient } from "../../../../test-utils"; jest.mock("../../../../../src/utils/ShieldUtils");
{this.props.subtitle}