Rework `UrlPreviewSettings` to use `MatrixClient.CryptoApi.isEncryptionEnabledInRoom` (#28463)

* Rework `UrlPreviewSettings` to use `MatrixClient.CryptoApi.isEncryptionEnabledInRoom`

* Handle loading state

* Update `@vector-im/compound-web` to have `InlineSpinner`

* Use `InlineSpinner` instead of a loading text.
pull/28509/head
Florian Duros 2024-11-20 18:08:34 +01:00 committed by GitHub
parent 7329a5f1fc
commit d5c111f656
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 450 additions and 87 deletions

View File

@ -86,7 +86,7 @@
"@matrix-org/spec": "^1.7.0", "@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0", "@sentry/browser": "^8.0.0",
"@vector-im/compound-design-tokens": "^2.0.1", "@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", "@vector-im/matrix-wysiwyg": "2.37.13",
"@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4",

View File

@ -9,107 +9,144 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. 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 { Room } from "matrix-js-sdk/src/matrix";
import { InlineSpinner } from "@vector-im/compound-web";
import { _t, _td } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsFlag from "../elements/SettingsFlag"; import SettingsFlag from "../elements/SettingsFlag";
import SettingsFieldset from "../settings/SettingsFieldset"; import SettingsFieldset from "../settings/SettingsFieldset";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; 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; room: Room;
} }
export default class UrlPreviewSettings extends React.Component<IProps> { export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element {
private onClickUserSettings = (e: ButtonEvent): void => { const { roomId } = room;
e.preventDefault(); const matrixClient = useMatrixClientContext();
e.stopPropagation(); const isEncrypted = useIsEncrypted(matrixClient, room);
dis.fire(Action.ViewUserSettings); const isLoading = isEncrypted === null;
};
public render(): ReactNode { return (
const roomId = this.props.room.roomId; <SettingsFieldset
const isEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId); legend={_t("room_settings|general|url_previews_section")}
description={!isLoading && <Description isEncrypted={isEncrypted} />}
let previewsForAccount: ReactNode | undefined; >
let previewsForRoom: ReactNode | undefined; {isLoading ? (
<InlineSpinner />
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"); <PreviewsForRoom isEncrypted={isEncrypted} roomId={roomId} />
if (accountEnabled) {
previewsForAccount = _t(
"room_settings|general|user_url_previews_default_on",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
{sub}
</AccessibleButton>
),
},
);
} else {
previewsForAccount = _t(
"room_settings|general|user_url_previews_default_off",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
{sub}
</AccessibleButton>
),
},
);
}
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
previewsForRoom = (
<SettingsFlag
name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
roomId={roomId}
isExplicit={true}
/>
);
} 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 = <div>{_t(str)}</div>;
}
} 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
(
<SettingsFlag <SettingsFlag
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"} name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
level={SettingLevel.ROOM_DEVICE} level={SettingLevel.ROOM_DEVICE}
roomId={roomId} roomId={roomId}
/> />
</>
)}
</SettingsFieldset>
); );
}
const description = ( /**
* 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) => (
<AccessibleButton kind="link_inline" onClick={onClickUserSettings}>
{sub}
</AccessibleButton>
),
};
previewsForAccount = urlPreviewsEnabled
? _t("room_settings|general|user_url_previews_default_on", {}, button)
: _t("room_settings|general|user_url_previews_default_off", {}, button);
}
return (
<> <>
<p>{_t("room_settings|general|url_preview_explainer")}</p> <p>{_t("room_settings|general|url_preview_explainer")}</p>
<p>{previewsForAccount}</p> <p>{previewsForAccount}</p>
</> </>
); );
}
return ( /**
<SettingsFieldset legend={_t("room_settings|general|url_previews_section")} description={description}> * The description for the URL preview settings
{previewsForRoom} */
{previewsForRoomAccount} interface PreviewsForRoomProps {
</SettingsFieldset> /**
* 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 = (
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.ROOM} roomId={roomId} isExplicit={true} />
);
} else {
previewsForRoom = (
<div>
{urlPreviewsEnabled
? _t("room_settings|general|default_url_previews_on")
: _t("room_settings|general|default_url_previews_off")}
</div>
); );
} }
return previewsForRoom;
} }

View File

@ -16,12 +16,12 @@ import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature"; import { UIFeature } from "../../../../../settings/UIFeature";
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
import AliasSettings from "../../../room_settings/AliasSettings"; import AliasSettings from "../../../room_settings/AliasSettings";
import PosthogTrackers from "../../../../../PosthogTrackers"; import PosthogTrackers from "../../../../../PosthogTrackers";
import { SettingsSubsection } from "../../shared/SettingsSubsection"; import { SettingsSubsection } from "../../shared/SettingsSubsection";
import SettingsTab from "../SettingsTab"; import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection"; import { SettingsSection } from "../../shared/SettingsSection";
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
interface IProps { interface IProps {
room: Room; room: Room;

View File

@ -0,0 +1,90 @@
/*
* 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(<UrlPreviewSettings room={room} />, 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, "getValueAt").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();
});
});

View File

@ -0,0 +1,236 @@
// 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`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
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.
</p>
<p>
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.
</p>
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
/>
</label>
<div
aria-checked="true"
aria-disabled="false"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is disabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
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.
</p>
<p>
<span>
You have
</span>
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
disabled
</div>
URL previews by default.
<p />
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div>
URL previews are disabled by default for participants in this room.
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable inline URL previews by default
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is enabled 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
<p>
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.
</p>
<p>
<span>
You have
</span>
</p>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
enabled
</div>
URL previews by default.
<p />
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div>
URL previews are enabled by default for participants in this room.
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable inline URL previews by default
</span>
</label>
<div
aria-checked="true"
aria-disabled="false"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</fieldset>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the setting is in a loading state 1`] = `
<DocumentFragment>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_content"
>
<svg
class="_icon_1ye7b_27"
fill="currentColor"
height="1em"
style="width: 20px; height: 20px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2Z"
fill-rule="evenodd"
/>
</svg>
</div>
</fieldset>
</DocumentFragment>
`;

View File

@ -3419,10 +3419,10 @@
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.0.1.tgz#add14494caab16cdbe98f2bdabe726908739def4" 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== integrity sha512-4nkPcrPII+sejispn+UkWZYFN7LecN39e4WGBupdceiMq0NJrfXrnVtJ9/6BDLgSqHInb6R/IWQkIbPbzfqRMg==
"@vector-im/compound-web@^7.3.0": "@vector-im/compound-web@^7.4.0":
version "7.3.0" version "7.4.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.3.0.tgz#9594113ac50bff4794715104a30a60c52d15517d" resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.4.0.tgz#a5af8af6346f8ff6c14c70f5d4eb2eab7357a7cc"
integrity sha512-gDppQUtpk5LvNHUg+Zlv9qzo1iBAag0s3g8Ec0qS5q4zGBKG6ruXXrNUKg1aK8rpbo2hYQsGaHM6dD8NqLoq3Q== integrity sha512-ZRBUeEGNmj/fTkIRa8zGnyVN7ytowpfOtHChqNm+m/+OTJN3o/lOMuQHDV8jeSEW2YwPJqGvPuG/dRr89IcQkA==
dependencies: dependencies:
"@floating-ui/react" "^0.26.24" "@floating-ui/react" "^0.26.24"
"@radix-ui/react-context-menu" "^2.2.1" "@radix-ui/react-context-menu" "^2.2.1"