From 9a3c410e81eb28e9b7b93503eb44e1e4826fa0bf Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 14 Nov 2024 17:08:27 +0100 Subject: [PATCH 1/4] Rework `UrlPreviewSettings` to use `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` --- .../room_settings/UrlPreviewSettings.tsx | 205 ++++++++++-------- .../tabs/room/GeneralRoomSettingsTab.tsx | 2 +- .../room_settings/UrlPreviewSettings-test.tsx | 82 +++++++ .../UrlPreviewSettings-test.tsx.snap | 203 +++++++++++++++++ 4 files changed, 405 insertions(+), 87 deletions(-) create mode 100644 test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx create mode 100644 test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap diff --git a/src/components/views/room_settings/UrlPreviewSettings.tsx b/src/components/views/room_settings/UrlPreviewSettings.tsx index babe7cf1405..55f01d572ba 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.tsx +++ b/src/components/views/room_settings/UrlPreviewSettings.tsx @@ -9,107 +9,140 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode } from "react"; +import React, { ReactNode, JSX } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; -import { _t, _td } from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; import dis from "../../../dispatcher/dispatcher"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { Action } from "../../../dispatcher/actions"; import { SettingLevel } from "../../../settings/SettingLevel"; import SettingsFlag from "../elements/SettingsFlag"; import SettingsFieldset from "../settings/SettingsFieldset"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts"; +import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx"; +import { useSettingValueAt } from "../../../hooks/useSettings.ts"; -interface IProps { +/** + * The URL preview settings for a room + */ +interface UrlPreviewSettingsProps { + /** + * The room. + */ room: Room; } -export default class UrlPreviewSettings extends React.Component { - private onClickUserSettings = (e: ButtonEvent): void => { - e.preventDefault(); - e.stopPropagation(); - dis.fire(Action.ViewUserSettings); - }; - - public render(): ReactNode { - const roomId = this.props.room.roomId; - const isEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId); - - let previewsForAccount: ReactNode | undefined; - let previewsForRoom: ReactNode | undefined; - - if (!isEncrypted) { - // Only show account setting state and room state setting state in non-e2ee rooms where they apply - const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled"); - if (accountEnabled) { - previewsForAccount = _t( - "room_settings|general|user_url_previews_default_on", - {}, - { - a: (sub) => ( - - {sub} - - ), - }, - ); - } else { - previewsForAccount = _t( - "room_settings|general|user_url_previews_default_off", - {}, - { - a: (sub) => ( - - {sub} - - ), - }, - ); - } - - if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) { - previewsForRoom = ( - - ); - } else { - let str = _td("room_settings|general|default_url_previews_on"); - if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/ true)) { - str = _td("room_settings|general|default_url_previews_off"); - } - previewsForRoom =
{_t(str)}
; - } - } else { - previewsForAccount = _t("room_settings|general|url_preview_encryption_warning"); - } - - const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in - ( - - ); - - const description = ( - <> -

{_t("room_settings|general|url_preview_explainer")}

-

{previewsForAccount}

- +export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element { + const { roomId } = room; + const matrixClient = useMatrixClientContext(); + const isEncrypted = Boolean(useIsEncrypted(matrixClient, room)); + const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in + ( + ); - return ( - - {previewsForRoom} - {previewsForRoomAccount} - + return ( + } + > + + {previewsForRoomAccount} + + ); +} + +/** + * Click handler for the user settings link + * @param e + */ +function onClickUserSettings(e: ButtonEvent): void { + e.preventDefault(); + e.stopPropagation(); + dis.fire(Action.ViewUserSettings); +} + +/** + * The description for the URL preview settings + */ +interface DescriptionProps { + /** + * Whether the room is encrypted + */ + isEncrypted: boolean; +} + +function Description({ isEncrypted }: DescriptionProps): JSX.Element { + const urlPreviewsEnabled = useSettingValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled"); + + let previewsForAccount: ReactNode | undefined; + if (isEncrypted) { + previewsForAccount = _t("room_settings|general|url_preview_encryption_warning"); + } else { + const button = { + a: (sub: string) => ( + + {sub} + + ), + }; + + previewsForAccount = urlPreviewsEnabled + ? _t("room_settings|general|user_url_previews_default_on", {}, button) + : _t("room_settings|general|user_url_previews_default_off", {}, button); + } + + return ( + <> +

{_t("room_settings|general|url_preview_explainer")}

+

{previewsForAccount}

+ + ); +} + +/** + * The description for the URL preview settings + */ +interface PreviewsForRoomProps { + /** + * Whether the room is encrypted + */ + isEncrypted: boolean; + /** + * The room ID + */ + roomId: string; +} + +function PreviewsForRoom({ isEncrypted, roomId }: PreviewsForRoomProps): JSX.Element | null { + const urlPreviewsEnabled = useSettingValueAt( + SettingLevel.ACCOUNT, + "urlPreviewsEnabled", + roomId, + /*explicit=*/ true, + ); + if (isEncrypted) return null; + + let previewsForRoom: ReactNode; + if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) { + previewsForRoom = ( + + ); + } else { + previewsForRoom = ( +
+ {urlPreviewsEnabled + ? _t("room_settings|general|default_url_previews_on") + : _t("room_settings|general|default_url_previews_off")} +
); } + + return previewsForRoom; } diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 066dc45366d..048fe5df9d9 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -16,12 +16,12 @@ import dis from "../../../../../dispatcher/dispatcher"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; -import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings"; import AliasSettings from "../../../room_settings/AliasSettings"; import PosthogTrackers from "../../../../../PosthogTrackers"; import { SettingsSubsection } from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; +import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings"; interface IProps { room: Room; diff --git a/test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx b/test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx new file mode 100644 index 00000000000..371fef9dfcc --- /dev/null +++ b/test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { render, screen } from "jest-matrix-react"; +import { waitFor } from "@testing-library/dom"; + +import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils"; +import { UrlPreviewSettings } from "../../../../../src/components/views/room_settings/UrlPreviewSettings.tsx"; +import SettingsStore from "../../../../../src/settings/SettingsStore.ts"; +import dis from "../../../../../src/dispatcher/dispatcher.ts"; +import { Action } from "../../../../../src/dispatcher/actions.ts"; + +describe("UrlPreviewSettings", () => { + let client: MatrixClient; + let room: Room; + + beforeEach(() => { + client = createTestClient(); + room = mkStubRoom("roomId", "room", client); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function renderComponent() { + return render(, withClientContextRenderOptions(client)); + } + + it("should display the correct preview when the room is encrypted and the url preview is enabled", async () => { + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + + const { asFragment } = renderComponent(); + await waitFor(() => { + expect( + screen.getByText( + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", + ), + ).toBeInTheDocument(); + }); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should display the correct preview when the room is unencrypted and the url preview is enabled", async () => { + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); + jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true); + jest.spyOn(dis, "fire").mockReturnValue(undefined); + + const { asFragment } = renderComponent(); + await waitFor(() => { + expect(screen.getByRole("button", { name: "enabled" })).toBeInTheDocument(); + expect( + screen.getByText("URL previews are enabled by default for participants in this room."), + ).toBeInTheDocument(); + }); + expect(asFragment()).toMatchSnapshot(); + + screen.getByRole("button", { name: "enabled" }).click(); + expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings); + }); + + it("should display the correct preview when the room is unencrypted and the url preview is disabled", async () => { + jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false); + jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false); + + const { asFragment } = renderComponent(); + await waitFor(() => { + expect(screen.getByRole("button", { name: "disabled" })).toBeInTheDocument(); + expect( + screen.getByText("URL previews are disabled by default for participants in this room."), + ).toBeInTheDocument(); + }); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap b/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap new file mode 100644 index 00000000000..2bbdcb6f21f --- /dev/null +++ b/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap @@ -0,0 +1,203 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UrlPreviewSettings should display the correct preview when the room is encrypted and the url preview is enabled 1`] = ` + +
+ + URL Previews + +
+
+

+ When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website. +

+

+ In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room. +

+
+
+
+
+ +
+
+
+
+
+
+
+`; + +exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is disabled 1`] = ` + +
+ + URL Previews + +
+
+

+ When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website. +

+

+ + You have + +

+ + URL previews by default. +

+

+
+
+
+ URL previews are disabled by default for participants in this room. +
+
+ +
+
+
+
+
+
+
+`; + +exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is enabled 1`] = ` + +
+ + URL Previews + +
+
+

+ When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website. +

+

+ + You have + +

+ + URL previews by default. +

+

+
+
+
+ URL previews are enabled by default for participants in this room. +
+
+ +
+
+
+
+
+
+
+`; From 625ddb820e275e58b34d0c405e19f5ef80ec242d Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 18 Nov 2024 14:32:55 +0100 Subject: [PATCH 2/4] Handle loading state --- .../room_settings/UrlPreviewSettings.tsx | 27 ++++++++++--------- .../room_settings/UrlPreviewSettings-test.tsx | 10 ++++++- .../UrlPreviewSettings-test.tsx.snap | 19 +++++++++++++ 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/components/views/room_settings/UrlPreviewSettings.tsx b/src/components/views/room_settings/UrlPreviewSettings.tsx index 55f01d572ba..3690f602abd 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.tsx +++ b/src/components/views/room_settings/UrlPreviewSettings.tsx @@ -37,23 +37,26 @@ interface UrlPreviewSettingsProps { export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element { const { roomId } = room; const matrixClient = useMatrixClientContext(); - const isEncrypted = Boolean(useIsEncrypted(matrixClient, room)); - const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in - ( - - ); + const isEncrypted = useIsEncrypted(matrixClient, room); + const isLoading = isEncrypted === null; return ( } + description={!isLoading && } > - - {previewsForRoomAccount} + {isLoading ? ( + _t("common|loading") + ) : ( + <> + + + + )} ); } diff --git a/test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx b/test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx index 371fef9dfcc..cbd5410599a 100644 --- a/test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx +++ b/test/unit-tests/components/views/room_settings/UrlPreviewSettings-test.tsx @@ -33,9 +33,17 @@ describe("UrlPreviewSettings", () => { return render(, withClientContextRenderOptions(client)); } + it("should display the correct preview when the setting is in a loading state", () => { + jest.spyOn(client, "getCrypto").mockReturnValue(undefined); + const { asFragment } = renderComponent(); + expect(screen.getByText("URL Previews")).toBeInTheDocument(); + + expect(asFragment()).toMatchSnapshot(); + }); + it("should display the correct preview when the room is encrypted and the url preview is enabled", async () => { jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true); - jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true); const { asFragment } = renderComponent(); await waitFor(() => { diff --git a/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap b/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap index 2bbdcb6f21f..f9b10357ee2 100644 --- a/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap +++ b/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap @@ -201,3 +201,22 @@ exports[`UrlPreviewSettings should display the correct preview when the room is `; + +exports[`UrlPreviewSettings should display the correct preview when the setting is in a loading state 1`] = ` + +
+ + URL Previews + +
+ Loading… +
+
+
+`; From 001b1d1e69520c0784cc9c09595d7e2127f0d5b0 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 20 Nov 2024 17:44:27 +0100 Subject: [PATCH 3/4] Update `@vector-im/compound-web` to have `InlineSpinner` --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 622269f3c0b..a48284bb976 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@vector-im/compound-design-tokens": "^2.0.1", - "@vector-im/compound-web": "^7.3.0", + "@vector-im/compound-web": "^7.4.0", "@vector-im/matrix-wysiwyg": "2.37.13", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/yarn.lock b/yarn.lock index 20f545347e2..a2bf286f1b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3419,10 +3419,10 @@ resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.0.1.tgz#add14494caab16cdbe98f2bdabe726908739def4" integrity sha512-4nkPcrPII+sejispn+UkWZYFN7LecN39e4WGBupdceiMq0NJrfXrnVtJ9/6BDLgSqHInb6R/IWQkIbPbzfqRMg== -"@vector-im/compound-web@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.3.0.tgz#9594113ac50bff4794715104a30a60c52d15517d" - integrity sha512-gDppQUtpk5LvNHUg+Zlv9qzo1iBAag0s3g8Ec0qS5q4zGBKG6ruXXrNUKg1aK8rpbo2hYQsGaHM6dD8NqLoq3Q== +"@vector-im/compound-web@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.4.0.tgz#a5af8af6346f8ff6c14c70f5d4eb2eab7357a7cc" + integrity sha512-ZRBUeEGNmj/fTkIRa8zGnyVN7ytowpfOtHChqNm+m/+OTJN3o/lOMuQHDV8jeSEW2YwPJqGvPuG/dRr89IcQkA== dependencies: "@floating-ui/react" "^0.26.24" "@radix-ui/react-context-menu" "^2.2.1" From 7ba528f7a530e82308f4afc1967d0073880da33b Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 20 Nov 2024 17:44:49 +0100 Subject: [PATCH 4/4] Use `InlineSpinner` instead of a loading text. --- .../views/room_settings/UrlPreviewSettings.tsx | 3 ++- .../UrlPreviewSettings-test.tsx.snap | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/views/room_settings/UrlPreviewSettings.tsx b/src/components/views/room_settings/UrlPreviewSettings.tsx index 3690f602abd..4ca63fd4a0d 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.tsx +++ b/src/components/views/room_settings/UrlPreviewSettings.tsx @@ -11,6 +11,7 @@ Please see LICENSE files in the repository root for full details. import React, { ReactNode, JSX } from "react"; import { Room } from "matrix-js-sdk/src/matrix"; +import { InlineSpinner } from "@vector-im/compound-web"; import { _t } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; @@ -46,7 +47,7 @@ export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Eleme description={!isLoading && } > {isLoading ? ( - _t("common|loading") + ) : ( <> diff --git a/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap b/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap index f9b10357ee2..d3a7eb4ad4c 100644 --- a/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap +++ b/test/unit-tests/components/views/room_settings/__snapshots__/UrlPreviewSettings-test.tsx.snap @@ -215,7 +215,21 @@ exports[`UrlPreviewSettings should display the correct preview when the setting
- Loading… + + +