From 3717e8a71692b5ca5229497a30b8f565e194bf04 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 29 Nov 2024 17:38:59 +0100 Subject: [PATCH 1/6] New UX for `ShareDialog` --- res/css/_common.pcss | 14 +- res/css/views/dialogs/_ShareDialog.pcss | 97 +++--- src/components/views/dialogs/ShareDialog.tsx | 296 +++++++++--------- src/i18n/strings/en_EN.json | 2 +- .../views/dialogs/ShareDialog-test.tsx | 2 +- 5 files changed, 213 insertions(+), 198 deletions(-) diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 15ba02b6b88..74328af39b2 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 086222af31b..561e0dc20f1 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_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_ShareDialog .mx_ShareDialog_content { - margin: 10px 0; + .mx_ShareDialog_content { + display: flex; + flex-direction: column; + gap: var(--cpd-space-6x); + align-items: center; - .mx_CopyableText { - width: unset; /* full width */ + .mx_ShareDialog_top { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + align-items: center; + width: 100%; - > a { - text-decoration: none; - flex-shrink: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + 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%; + } } - } -} -.mx_ShareDialog_split { - display: flex; - flex-wrap: wrap; -} + label { + display: inline-flex; + gap: var(--cpd-space-3x); + justify-content: center; + align-items: center; + font: var(--cpd-font-body-md-medium); + } -.mx_ShareDialog_qrcode_container { - float: left; - height: 256px; - width: 256px; - margin-right: 64px; -} + button { + width: 100%; + } -.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { - width: 299px; -} + .mx_ShareDialog_social { + display: flex; + gap: var(--cpd-space-3x); + justify-content: center; -.mx_ShareDialog_social_container { - display: inline-block; -} + 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; -.mx_ShareDialog_social_icon { - display: inline-grid; - margin-right: 10px; - margin-bottom: 10px; + img { + width: 24px; + height: 24px; + } + } + } + } } diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index f9382227e4e..1796b79239e 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 && ( + + )} + + {showSocials && } +
+
+ ); } -export default class ShareDialog extends React.PureComponent, IState> { - public constructor(props: XOR) { - super(props); - - let permalinkCreator: RoomPermalinkCreator | null = null; - if (props.target instanceof Room) { - permalinkCreator = new RoomPermalinkCreator(props.target); - permalinkCreator.load(); - } +/** + * Social links to share the link on different platforms. + */ +interface SocialLinksProps { + /** + * The URL to share. + */ + url: string; +} - this.state = { - // MatrixEvent defaults to share linkSpecificEvent - linkSpecificEvent: this.props.target instanceof MatrixEvent, - permalinkCreator, - }; - } - - 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")} - -
- ); - } +/** + * The socials to share the link on. + */ +function SocialLinks({ url }: SocialLinksProps): JSX.Element { + return ( +
+ {SOCIALS.map((social) => ( + + {social.name} + + ))} +
+ ); +} - 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) => ( - - {social.name} - - ))} -
- )} -
- - ); +/** + * 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") }), + }; } - return ( - - {this.props.subtitle &&

{this.props.subtitle}

} -
- matrixToUrl}> - - {matrixToUrl} - - - {checkbox} - {qrSocialSection} -
-
- ); - } + // 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"), + }; + }, [target, linkToSpecificEvent, permalinkCreator]); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 50ca4ae1e42..f467bbc7140 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 cb7d556235c..89fec6ec248 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"); From f372de1ea3b620f4ba32a632b79d14c32b09267a Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 29 Nov 2024 17:39:09 +0100 Subject: [PATCH 2/6] Use new named import --- src/components/views/context_menus/MessageContextMenu.tsx | 2 +- src/components/views/right_panel/RoomSummaryCard.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 2 +- src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx | 2 +- .../components/views/right_panel/RoomSummaryCard-test.tsx | 2 +- test/unit-tests/components/views/right_panel/UserInfo-test.tsx | 2 +- .../views/rooms/RoomHeader/CallGuestLinkButton-test.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index d5749658c98..b6b77f52561 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -38,7 +38,7 @@ import ContextMenu, { toRightOf, MenuProps } from "../../structures/ContextMenu" import ReactionPicker from "../emojipicker/ReactionPicker"; import ViewSource from "../../structures/ViewSource"; import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import EndPollDialog from "../dialogs/EndPollDialog"; import { isPollEnded } from "../messages/MPollBody"; diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 664977bbe27..bb5c00cde66 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -47,7 +47,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import Modal from "../../../Modal"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d07b3566e2e..50c10b6cb6c 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -63,7 +63,7 @@ import PowerSelector from "../elements/PowerSelector"; import MemberAvatar from "../avatars/MemberAvatar"; import PresenceLabel from "../rooms/PresenceLabel"; import BulkRedactDialog from "../dialogs/BulkRedactDialog"; -import ShareDialog from "../dialogs/ShareDialog"; +import { ShareDialog } from "../dialogs/ShareDialog"; import ErrorDialog from "../dialogs/ErrorDialog"; import QuestionDialog from "../dialogs/QuestionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index ae8e7be16bf..8c000bdf3bc 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -12,7 +12,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { EventType, JoinRule, Room } from "matrix-js-sdk/src/matrix"; import Modal from "../../../../Modal"; -import ShareDialog from "../../dialogs/ShareDialog"; +import { ShareDialog } from "../../dialogs/ShareDialog"; import { _t } from "../../../../languageHandler"; import SettingsStore from "../../../../settings/SettingsStore"; import { calculateRoomVia } from "../../../../utils/permalinks/Permalinks"; diff --git a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx index 4026149f984..41bad774197 100644 --- a/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/unit-tests/components/views/right_panel/RoomSummaryCard-test.tsx @@ -15,7 +15,7 @@ import userEvent from "@testing-library/user-event"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import RoomSummaryCard from "../../../../../src/components/views/right_panel/RoomSummaryCard"; -import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; import ExportDialog from "../../../../../src/components/views/dialogs/ExportDialog"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; diff --git a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx index 7e23679fc2b..1e19d7a20dc 100644 --- a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx +++ b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx @@ -49,7 +49,7 @@ import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; import { UIComponent } from "../../../../../src/settings/UIFeature"; import { Action } from "../../../../../src/dispatcher/actions"; -import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog"; import BulkRedactDialog from "../../../../../src/components/views/dialogs/BulkRedactDialog"; jest.mock("../../../../../src/utils/direct-messages", () => ({ diff --git a/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx index cc28a4ec083..c77114fa96e 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx @@ -19,7 +19,7 @@ import { } from "../../../../../../src/components/views/rooms/RoomHeader/CallGuestLinkButton"; import Modal from "../../../../../../src/Modal"; import SdkConfig from "../../../../../../src/SdkConfig"; -import ShareDialog from "../../../../../../src/components/views/dialogs/ShareDialog"; +import { ShareDialog } from "../../../../../../src/components/views/dialogs/ShareDialog"; import { _t } from "../../../../../../src/languageHandler"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; From fdad4bd7fd0ab0ae5e629d930dd4b04a4723ae80 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 2 Dec 2024 12:25:26 +0100 Subject: [PATCH 3/6] Rewrite tests --- .../views/dialogs/ShareDialog-test.tsx | 174 ++-- .../__snapshots__/ShareDialog-test.tsx.snap | 852 ++++++++++++++++++ 2 files changed, 953 insertions(+), 73 deletions(-) create mode 100644 test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap diff --git a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx index 89fec6ec248..c1d9883b7f6 100644 --- a/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/ShareDialog-test.tsx @@ -7,111 +7,139 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; -import { render, RenderOptions } from "jest-matrix-react"; +import { MatrixClient, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; +import { render, screen, act } from "jest-matrix-react"; +import userEvent from "@testing-library/user-event"; +import { waitFor } from "@testing-library/dom"; -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 { UIFeature } from "../../../../../src/settings/UIFeature"; -import { stubClient } from "../../../../test-utils"; -jest.mock("../../../../../src/utils/ShieldUtils"); - -function getWrapper(): RenderOptions { - return { - wrapper: ({ children }) => ( - {children} - ), - }; -} +import { stubClient, withClientContextRenderOptions } from "../../../../test-utils"; +import * as StringsModule from "../../../../../src/utils/strings"; +import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks.ts"; describe("ShareDialog", () => { + let client: MatrixClient; let room: Room; - - const ROOM_ID = "!1:example.org"; + const copyTextFunc = jest.fn(); beforeEach(async () => { - stubClient(); - room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org"); + client = stubClient(); + room = new Room("!1:example.org", client, "@alice:example.org"); + jest.spyOn(StringsModule, "copyPlaintext").mockImplementation(copyTextFunc); }); afterEach(() => { jest.restoreAllMocks(); + copyTextFunc.mockClear(); }); - it("renders room share dialog", () => { - const { container: withoutEvents } = render(, getWrapper()); - expect(withoutEvents).toHaveTextContent(_t("share|title_room")); + function renderComponent(target: Room | RoomMember | URL) { + return render(, withClientContextRenderOptions(client)); + } + + const getUrl = () => new URL("https://matrix.org/"); + const getRoomMember = () => new RoomMember(room.roomId, "@alice:example.org"); + + test.each([ + { name: "an URL", title: "Share Link", url: "https://matrix.org/", getTarget: getUrl }, + { + name: "a room member", + title: "Share User", + url: "https://matrix.to/#/@alice:example.org", + getTarget: getRoomMember, + }, + ])("should render a share dialog for $name", async ({ title, url, getTarget }) => { + const { asFragment } = renderComponent(getTarget()); + + expect(screen.getByRole("heading", { name: title })).toBeInTheDocument(); + expect(screen.getByText(url)).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); - jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline); - const { container: withEvents } = render(, getWrapper()); - expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent")); + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(url); }); - it("renders user share dialog", () => { - mockRoomMembers(room, 1); - const { container } = render( - , - getWrapper(), - ); - expect(container).toHaveTextContent(_t("share|title_user")); + it("should render a share dialog for a room", async () => { + const expectedURL = "https://matrix.to/#/!1:example.org"; + jest.spyOn(room.getLiveTimeline(), "getEvents").mockReturnValue([new MatrixEvent({ event_id: "!eventId" })]); + + const { asFragment } = renderComponent(room); + expect(screen.getByRole("heading", { name: "Share Room" })).toBeInTheDocument(); + expect(screen.getByText(expectedURL)).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "Link to most recent message" })).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(expectedURL); + + // Click on the checkbox to link to the most recent message + await userEvent.click(screen.getByRole("checkbox", { name: "Link to most recent message" })); + const newExpectedURL = "https://matrix.to/#/!1:example.org/!eventId"; + expect(screen.getByText(newExpectedURL)).toBeInTheDocument(); }); - it("renders link share dialog", () => { - mockRoomMembers(room, 1); - const { container } = render( - , - getWrapper(), + it("should render a share dialog for a matrix event", async () => { + const matrixEvent = new MatrixEvent({ event_id: "!eventId" }); + const permalinkCreator = new RoomPermalinkCreator(room); + const expectedURL = "https://matrix.to/#/!1:example.org/!eventId"; + + const { asFragment } = render( + , + withClientContextRenderOptions(client), ); - expect(container).toHaveTextContent(_t("share|title_link")); + expect(screen.getByRole("heading", { name: "Share Room Message" })).toBeInTheDocument(); + expect(screen.getByText(expectedURL)).toBeInTheDocument(); + expect(screen.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked(); + expect(asFragment()).toMatchSnapshot(); + + await userEvent.click(screen.getByRole("button", { name: "Copy link" })); + expect(copyTextFunc).toHaveBeenCalledWith(expectedURL); + + // Click on the checkbox to link to the room + await userEvent.click(screen.getByRole("checkbox", { name: "Link to selected message" })); + expect(screen.getByText("https://matrix.to/#/!1:example.org")).toBeInTheDocument(); + }); + + it("should change the copy button text when clicked", async () => { + jest.useFakeTimers(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + // To not be bother with rtl warnings about QR code state update + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + + renderComponent(room); + await user.click(screen.getByRole("button", { name: "Copy link" })); + // Move after `copyPlaintext` + await jest.advanceTimersToNextTimerAsync(); + expect(screen.getByRole("button", { name: "Link copied" })).toBeInTheDocument(); + + // 2 sec after the button should be back to normal + act(() => jest.advanceTimersByTime(2000)); + await waitFor(() => expect(screen.getByRole("button", { name: "Copy link" })).toBeInTheDocument()); }); - it("renders the QR code if configured", () => { + it("should not render the QR code if disabled", () => { const originalGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => { - if (feature === UIFeature.ShareQRCode) return true; + if (feature === UIFeature.ShareQRCode) return false; return originalGetValue(feature); }); - const { container } = render(, getWrapper()); - const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0; - expect(qrCodesVisible).toBe(true); + + const { asFragment } = renderComponent(room); + expect(screen.queryByRole("img", { name: "QR code" })).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); - it("renders the social button if configured", () => { + it("should not render the socials if disabled", () => { const originalGetValue = SettingsStore.getValue; jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => { - if (feature === UIFeature.ShareSocial) return true; + if (feature === UIFeature.ShareSocial) return false; return originalGetValue(feature); }); - const { container } = render(, getWrapper()); - const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0; - expect(qrCodesVisible).toBe(true); - }); - it("renders custom title and subtitle", () => { - const { container } = render( - , - getWrapper(), - ); - expect(container).toHaveTextContent("test_title_123"); - expect(container).toHaveTextContent("custom_subtitle_1234"); + + const { asFragment } = renderComponent(room); + expect(screen.queryByRole("link", { name: "Reddit" })).toBeNull(); + expect(asFragment()).toMatchSnapshot(); }); }); -/** - * - * @param count the number of users to create - */ -function mockRoomMembers(room: Room, count: number) { - const members = Array(count) - .fill(0) - .map((_, index) => new RoomMember(room.roomId, "@alice:example.org")); - - room.currentState.setJoinedMemberCount(members.length); - room.getJoinedMembers = jest.fn().mockReturnValue(members); -} diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap new file mode 100644 index 00000000000..ab8b8ffb582 --- /dev/null +++ b/test/unit-tests/components/views/dialogs/__snapshots__/ShareDialog-test.tsx.snap @@ -0,0 +1,852 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ShareDialog should not render the QR code if disabled 1`] = ` + +
+