Skip to content

Commit d5c111f

Browse files
authored
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.
1 parent 7329a5f commit d5c111f

File tree

6 files changed

+453
-90
lines changed

6 files changed

+453
-90
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"@matrix-org/spec": "^1.7.0",
8787
"@sentry/browser": "^8.0.0",
8888
"@vector-im/compound-design-tokens": "^2.0.1",
89-
"@vector-im/compound-web": "^7.3.0",
89+
"@vector-im/compound-web": "^7.4.0",
9090
"@vector-im/matrix-wysiwyg": "2.37.13",
9191
"@zxcvbn-ts/core": "^3.0.4",
9292
"@zxcvbn-ts/language-common": "^3.0.4",

src/components/views/room_settings/UrlPreviewSettings.tsx

Lines changed: 121 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -9,107 +9,144 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
99
Please see LICENSE files in the repository root for full details.
1010
*/
1111

12-
import React, { ReactNode } from "react";
12+
import React, { ReactNode, JSX } from "react";
1313
import { Room } from "matrix-js-sdk/src/matrix";
14+
import { InlineSpinner } from "@vector-im/compound-web";
1415

15-
import { _t, _td } from "../../../languageHandler";
16+
import { _t } from "../../../languageHandler";
1617
import SettingsStore from "../../../settings/SettingsStore";
1718
import dis from "../../../dispatcher/dispatcher";
18-
import { MatrixClientPeg } from "../../../MatrixClientPeg";
1919
import { Action } from "../../../dispatcher/actions";
2020
import { SettingLevel } from "../../../settings/SettingLevel";
2121
import SettingsFlag from "../elements/SettingsFlag";
2222
import SettingsFieldset from "../settings/SettingsFieldset";
2323
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
24+
import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
25+
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
26+
import { useSettingValueAt } from "../../../hooks/useSettings.ts";
2427

25-
interface IProps {
28+
/**
29+
* The URL preview settings for a room
30+
*/
31+
interface UrlPreviewSettingsProps {
32+
/**
33+
* The room.
34+
*/
2635
room: Room;
2736
}
2837

29-
export default class UrlPreviewSettings extends React.Component<IProps> {
30-
private onClickUserSettings = (e: ButtonEvent): void => {
31-
e.preventDefault();
32-
e.stopPropagation();
33-
dis.fire(Action.ViewUserSettings);
34-
};
35-
36-
public render(): ReactNode {
37-
const roomId = this.props.room.roomId;
38-
const isEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);
39-
40-
let previewsForAccount: ReactNode | undefined;
41-
let previewsForRoom: ReactNode | undefined;
42-
43-
if (!isEncrypted) {
44-
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
45-
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
46-
if (accountEnabled) {
47-
previewsForAccount = _t(
48-
"room_settings|general|user_url_previews_default_on",
49-
{},
50-
{
51-
a: (sub) => (
52-
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
53-
{sub}
54-
</AccessibleButton>
55-
),
56-
},
57-
);
58-
} else {
59-
previewsForAccount = _t(
60-
"room_settings|general|user_url_previews_default_off",
61-
{},
62-
{
63-
a: (sub) => (
64-
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
65-
{sub}
66-
</AccessibleButton>
67-
),
68-
},
69-
);
70-
}
71-
72-
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
73-
previewsForRoom = (
38+
export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element {
39+
const { roomId } = room;
40+
const matrixClient = useMatrixClientContext();
41+
const isEncrypted = useIsEncrypted(matrixClient, room);
42+
const isLoading = isEncrypted === null;
43+
44+
return (
45+
<SettingsFieldset
46+
legend={_t("room_settings|general|url_previews_section")}
47+
description={!isLoading && <Description isEncrypted={isEncrypted} />}
48+
>
49+
{isLoading ? (
50+
<InlineSpinner />
51+
) : (
52+
<>
53+
<PreviewsForRoom isEncrypted={isEncrypted} roomId={roomId} />
7454
<SettingsFlag
75-
name="urlPreviewsEnabled"
76-
level={SettingLevel.ROOM}
55+
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
56+
level={SettingLevel.ROOM_DEVICE}
7757
roomId={roomId}
78-
isExplicit={true}
7958
/>
80-
);
81-
} else {
82-
let str = _td("room_settings|general|default_url_previews_on");
83-
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/ true)) {
84-
str = _td("room_settings|general|default_url_previews_off");
85-
}
86-
previewsForRoom = <div>{_t(str)}</div>;
87-
}
88-
} else {
89-
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
90-
}
91-
92-
const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in
93-
(
94-
<SettingsFlag
95-
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
96-
level={SettingLevel.ROOM_DEVICE}
97-
roomId={roomId}
98-
/>
99-
);
100-
101-
const description = (
102-
<>
103-
<p>{_t("room_settings|general|url_preview_explainer")}</p>
104-
<p>{previewsForAccount}</p>
105-
</>
106-
);
59+
</>
60+
)}
61+
</SettingsFieldset>
62+
);
63+
}
64+
65+
/**
66+
* Click handler for the user settings link
67+
* @param e
68+
*/
69+
function onClickUserSettings(e: ButtonEvent): void {
70+
e.preventDefault();
71+
e.stopPropagation();
72+
dis.fire(Action.ViewUserSettings);
73+
}
74+
75+
/**
76+
* The description for the URL preview settings
77+
*/
78+
interface DescriptionProps {
79+
/**
80+
* Whether the room is encrypted
81+
*/
82+
isEncrypted: boolean;
83+
}
84+
85+
function Description({ isEncrypted }: DescriptionProps): JSX.Element {
86+
const urlPreviewsEnabled = useSettingValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
87+
88+
let previewsForAccount: ReactNode | undefined;
89+
if (isEncrypted) {
90+
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
91+
} else {
92+
const button = {
93+
a: (sub: string) => (
94+
<AccessibleButton kind="link_inline" onClick={onClickUserSettings}>
95+
{sub}
96+
</AccessibleButton>
97+
),
98+
};
10799

108-
return (
109-
<SettingsFieldset legend={_t("room_settings|general|url_previews_section")} description={description}>
110-
{previewsForRoom}
111-
{previewsForRoomAccount}
112-
</SettingsFieldset>
100+
previewsForAccount = urlPreviewsEnabled
101+
? _t("room_settings|general|user_url_previews_default_on", {}, button)
102+
: _t("room_settings|general|user_url_previews_default_off", {}, button);
103+
}
104+
105+
return (
106+
<>
107+
<p>{_t("room_settings|general|url_preview_explainer")}</p>
108+
<p>{previewsForAccount}</p>
109+
</>
110+
);
111+
}
112+
113+
/**
114+
* The description for the URL preview settings
115+
*/
116+
interface PreviewsForRoomProps {
117+
/**
118+
* Whether the room is encrypted
119+
*/
120+
isEncrypted: boolean;
121+
/**
122+
* The room ID
123+
*/
124+
roomId: string;
125+
}
126+
127+
function PreviewsForRoom({ isEncrypted, roomId }: PreviewsForRoomProps): JSX.Element | null {
128+
const urlPreviewsEnabled = useSettingValueAt(
129+
SettingLevel.ACCOUNT,
130+
"urlPreviewsEnabled",
131+
roomId,
132+
/*explicit=*/ true,
133+
);
134+
if (isEncrypted) return null;
135+
136+
let previewsForRoom: ReactNode;
137+
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
138+
previewsForRoom = (
139+
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.ROOM} roomId={roomId} isExplicit={true} />
140+
);
141+
} else {
142+
previewsForRoom = (
143+
<div>
144+
{urlPreviewsEnabled
145+
? _t("room_settings|general|default_url_previews_on")
146+
: _t("room_settings|general|default_url_previews_off")}
147+
</div>
113148
);
114149
}
150+
151+
return previewsForRoom;
115152
}

src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import dis from "../../../../../dispatcher/dispatcher";
1616
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
1717
import SettingsStore from "../../../../../settings/SettingsStore";
1818
import { UIFeature } from "../../../../../settings/UIFeature";
19-
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
2019
import AliasSettings from "../../../room_settings/AliasSettings";
2120
import PosthogTrackers from "../../../../../PosthogTrackers";
2221
import { SettingsSubsection } from "../../shared/SettingsSubsection";
2322
import SettingsTab from "../SettingsTab";
2423
import { SettingsSection } from "../../shared/SettingsSection";
24+
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
2525

2626
interface IProps {
2727
room: Room;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
10+
import { render, screen } from "jest-matrix-react";
11+
import { waitFor } from "@testing-library/dom";
12+
13+
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils";
14+
import { UrlPreviewSettings } from "../../../../../src/components/views/room_settings/UrlPreviewSettings.tsx";
15+
import SettingsStore from "../../../../../src/settings/SettingsStore.ts";
16+
import dis from "../../../../../src/dispatcher/dispatcher.ts";
17+
import { Action } from "../../../../../src/dispatcher/actions.ts";
18+
19+
describe("UrlPreviewSettings", () => {
20+
let client: MatrixClient;
21+
let room: Room;
22+
23+
beforeEach(() => {
24+
client = createTestClient();
25+
room = mkStubRoom("roomId", "room", client);
26+
});
27+
28+
afterEach(() => {
29+
jest.restoreAllMocks();
30+
});
31+
32+
function renderComponent() {
33+
return render(<UrlPreviewSettings room={room} />, withClientContextRenderOptions(client));
34+
}
35+
36+
it("should display the correct preview when the setting is in a loading state", () => {
37+
jest.spyOn(client, "getCrypto").mockReturnValue(undefined);
38+
const { asFragment } = renderComponent();
39+
expect(screen.getByText("URL Previews")).toBeInTheDocument();
40+
41+
expect(asFragment()).toMatchSnapshot();
42+
});
43+
44+
it("should display the correct preview when the room is encrypted and the url preview is enabled", async () => {
45+
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
46+
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
47+
48+
const { asFragment } = renderComponent();
49+
await waitFor(() => {
50+
expect(
51+
screen.getByText(
52+
"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.",
53+
),
54+
).toBeInTheDocument();
55+
});
56+
expect(asFragment()).toMatchSnapshot();
57+
});
58+
59+
it("should display the correct preview when the room is unencrypted and the url preview is enabled", async () => {
60+
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
61+
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
62+
jest.spyOn(dis, "fire").mockReturnValue(undefined);
63+
64+
const { asFragment } = renderComponent();
65+
await waitFor(() => {
66+
expect(screen.getByRole("button", { name: "enabled" })).toBeInTheDocument();
67+
expect(
68+
screen.getByText("URL previews are enabled by default for participants in this room."),
69+
).toBeInTheDocument();
70+
});
71+
expect(asFragment()).toMatchSnapshot();
72+
73+
screen.getByRole("button", { name: "enabled" }).click();
74+
expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings);
75+
});
76+
77+
it("should display the correct preview when the room is unencrypted and the url preview is disabled", async () => {
78+
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
79+
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
80+
81+
const { asFragment } = renderComponent();
82+
await waitFor(() => {
83+
expect(screen.getByRole("button", { name: "disabled" })).toBeInTheDocument();
84+
expect(
85+
screen.getByText("URL previews are disabled by default for participants in this room."),
86+
).toBeInTheDocument();
87+
});
88+
expect(asFragment()).toMatchSnapshot();
89+
});
90+
});

0 commit comments

Comments
 (0)