Update URL Preview settings (#32992)

* Remove ability for url previews to be set per-room

* Add ability to enable E2EE URL Previews globally

* Remove old migration

* Cleanup

* Remove room account handler

* update snap

* screenshot updated

* Add a test
This commit is contained in:
Will Hunt
2026-04-09 13:32:50 +01:00
committed by GitHub
parent 253dcb44dd
commit b4d0c21abf
16 changed files with 231 additions and 719 deletions
@@ -1,96 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";
import { Form } from "@vector-im/compound-web";
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(
<Form.Root>
<UrlPreviewSettings room={room} />
</Form.Root>,
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();
});
});
@@ -1,270 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`UrlPreviewSettings should display the correct preview when the room is encrypted and the url preview is enabled 1`] = `
<DocumentFragment>
<form
class="_root_19upo_16"
>
<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="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
Enable URL previews for this room (only affects you)
</label>
</div>
</div>
</div>
</fieldset>
</form>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is disabled 1`] = `
<DocumentFragment>
<form
class="_root_19upo_16"
>
<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="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
Enable inline URL previews by default
</label>
</div>
</div>
</div>
</fieldset>
</form>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the room is unencrypted and the url preview is enabled 1`] = `
<DocumentFragment>
<form
class="_root_19upo_16"
>
<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="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
id="mx_SettingsFlag_vY7Q4uEh9K38"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_vY7Q4uEh9K38"
>
Enable inline URL previews by default
</label>
</div>
</div>
</div>
</fieldset>
</form>
</DocumentFragment>
`;
exports[`UrlPreviewSettings should display the correct preview when the setting is in a loading state 1`] = `
<DocumentFragment>
<form
class="_root_19upo_16"
>
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
URL Previews
</legend>
<div
class="mx_SettingsFieldset_content"
>
<svg
class="_icon_11k6c_18"
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 2"
fill-rule="evenodd"
/>
</svg>
</div>
</fieldset>
</form>
</DocumentFragment>
`;
@@ -905,9 +905,18 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Images, GIFs and videos
Link previews
</h2>
</div>
<div
class="mx_SettingsSubsection_description"
>
<div
class="mx_SettingsSubsection_text"
>
Shows information about links underneath messages
</div>
</div>
<div
class="mx_SettingsSubsection_content"
>
@@ -923,7 +932,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_vfGFMldL2r2v"
role="switch"
type="checkbox"
@@ -940,7 +948,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_vfGFMldL2r2v"
>
Enable inline URL previews by default
Enable previews
</label>
</div>
</div>
@@ -955,7 +963,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_bsSwicmKUiOB"
role="switch"
type="checkbox"
@@ -972,10 +979,31 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_bsSwicmKUiOB"
>
Autoplay GIFs
Enable previews in encrypted rooms
</label>
</div>
</div>
</div>
</div>
</form>
<form
class="_root_19upo_16"
>
<div
class="mx_SettingsSubsection"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h2
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Images, GIFs and videos
</h2>
</div>
<div
class="mx_SettingsSubsection_content"
>
<div
class="_inline-field_19upo_32"
>
@@ -1003,6 +1031,38 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="_label_19upo_59"
for="mx_SettingsFlag_dvqsxEaZtl3A"
>
Autoplay GIFs
</label>
</div>
</div>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_NIiWzqsApP1c"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_NIiWzqsApP1c"
>
Autoplay videos
</label>
@@ -1029,39 +1089,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<div
class="mx_SettingsSubsection_content"
>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_NIiWzqsApP1c"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_NIiWzqsApP1c"
>
Show typing notifications
</label>
</div>
</div>
<div
class="_inline-field_19upo_32"
>
@@ -1091,7 +1118,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_q1SIAPqLMVXh"
>
Show a placeholder for removed messages
Show typing notifications
</label>
</div>
</div>
@@ -1124,7 +1151,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_dXFDGgBsKXay"
>
Show read receipts sent by other users
Show a placeholder for removed messages
</label>
</div>
</div>
@@ -1157,7 +1184,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_7Az0xw4Bs4Tt"
>
Show join/leave messages (invites/removes/bans unaffected)
Show read receipts sent by other users
</label>
</div>
</div>
@@ -1190,7 +1217,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_8jmzPIlPoBCv"
>
Show display name changes
Show join/leave messages (invites/removes/bans unaffected)
</label>
</div>
</div>
@@ -1223,7 +1250,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_enFRaTjdsFou"
>
Show chat effects (animations when receiving e.g. confetti)
Show display name changes
</label>
</div>
</div>
@@ -1256,7 +1283,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_bfwnd5rz4XNX"
>
Show profile picture changes
Show chat effects (animations when receiving e.g. confetti)
</label>
</div>
</div>
@@ -1289,7 +1316,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_gs5uWEzYzZrS"
>
Show avatars in user, room and event mentions
Show profile picture changes
</label>
</div>
</div>
@@ -1322,7 +1349,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_qWg7OgID1yRR"
>
Enable big emoji in chat
Show avatars in user, room and event mentions
</label>
</div>
</div>
@@ -1355,7 +1382,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
class="_label_19upo_59"
for="mx_SettingsFlag_pOPewl7rtMbV"
>
Jump to the bottom of the timeline when you send a message
Enable big emoji in chat
</label>
</div>
</div>
@@ -1387,6 +1414,39 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="_label_19upo_59"
for="mx_SettingsFlag_cmt3PZSyNp3v"
>
Jump to the bottom of the timeline when you send a message
</label>
</div>
</div>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_dJJz3lHUv9XX"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_dJJz3lHUv9XX"
>
Show current profile picture and name for users in message history
</label>
@@ -1424,7 +1484,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<input
class="_input_udcm8_24"
id="_r_24_"
id="_r_26_"
role="switch"
type="checkbox"
/>
@@ -1438,7 +1498,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_19upo_59"
for="_r_24_"
for="_r_26_"
>
Hide avatars of room and inviter
</label>
@@ -1452,13 +1512,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_19upo_59"
for="radix-_r_26_"
for="radix-_r_28_"
>
Show media in timeline
</label>
<span
class="_message_19upo_85 _help-message_19upo_91 mx_MediaPreviewAccountSetting_RadioHelp"
id="radix-_r_27_"
id="radix-_r_29_"
>
A hidden media can always be shown by tapping on it
</span>
@@ -1571,7 +1631,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
checked=""
class="_input_udcm8_24"
disabled=""
id="_r_2b_"
id="_r_2d_"
role="switch"
type="checkbox"
/>
@@ -1585,13 +1645,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_19upo_59"
for="_r_2b_"
for="_r_2d_"
>
Allow users to invite you to rooms
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="radix-_r_2d_"
id="radix-_r_2f_"
>
Your server does not implement this feature.
</span>
@@ -1636,7 +1696,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<input
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_dJJz3lHUv9XX"
id="mx_SettingsFlag_SBSSOZDRlzlA"
role="switch"
type="checkbox"
/>
@@ -1650,7 +1710,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_dJJz3lHUv9XX"
for="mx_SettingsFlag_SBSSOZDRlzlA"
>
Show NSFW content
</label>
@@ -1690,7 +1750,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
checked=""
class="_input_udcm8_24"
disabled=""
id="mx_SettingsFlag_SBSSOZDRlzlA"
id="mx_SettingsFlag_FLEpLCb0jpp6"
role="switch"
type="checkbox"
/>
@@ -1704,7 +1764,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="_label_19upo_59"
for="mx_SettingsFlag_SBSSOZDRlzlA"
for="mx_SettingsFlag_FLEpLCb0jpp6"
>
Prompt before sending invites to potentially invalid matrix IDs
</label>
@@ -96,63 +96,18 @@ describe("SettingsStore", () => {
describe("runMigrations", () => {
let client: MatrixClient;
let room: Room;
let localStorageSetItemSpy: jest.SpyInstance;
let localStorageSetPromise: Promise<void>;
beforeEach(() => {
client = stubClient();
room = mkStubRoom("!room:example.org", "Room", client);
room.getAccountData = jest.fn().mockReturnValue({
getContent: jest.fn().mockReturnValue({
urlPreviewsEnabled_e2ee: true,
}),
});
client.getRooms = jest.fn().mockReturnValue([room]);
client.getRoom = jest.fn().mockReturnValue(room);
localStorageSetPromise = new Promise((resolve) => {
localStorageSetItemSpy = jest
.spyOn(localStorage.__proto__, "setItem")
.mockImplementation(() => resolve());
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("migrates URL previews setting for e2ee rooms", async () => {
SettingsStore.runMigrations(false);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(room.getAccountData).toHaveBeenCalled();
await localStorageSetPromise;
expect(localStorageSetItemSpy!).toHaveBeenCalledWith(
`mx_setting_urlPreviewsEnabled_e2ee_${room.roomId}`,
JSON.stringify({ value: true }),
);
});
it("does not migrate e2ee URL previews on a fresh login", async () => {
SettingsStore.runMigrations(true);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(room.getAccountData).not.toHaveBeenCalled();
});
it("does not migrate if the device is flagged as migrated", async () => {
jest.spyOn(localStorage.__proto__, "getItem").mockImplementation((key: unknown): string | undefined => {
if (key === "url_previews_e2ee_migration_done") return JSON.stringify({ value: true });
return undefined;
});
SettingsStore.runMigrations(false);
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
expect(room.getAccountData).not.toHaveBeenCalled();
});
describe("Migrate media preview configuration", () => {
beforeEach(() => {
MatrixClientBackedController.matrixClient = client;
@@ -0,0 +1,33 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import RequiresSettingsController from "../../../../src/settings/controllers/RequiresSettingsController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore";
describe("RequiresSettingsController", () => {
afterEach(() => {
SettingsStore.reset();
});
it("forces a value if a setting is false", async () => {
const forcedValue = true;
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
await SettingsStore.setValue("useCustomFontSize", null, SettingLevel.DEVICE, false);
const controller = new RequiresSettingsController(["useCompactLayout", "useCustomFontSize"], forcedValue);
expect(controller.settingDisabled).toEqual(true);
expect(controller.getValueOverride()).toEqual(forcedValue);
});
it("does not force a value if all settings are true", async () => {
const controller = new RequiresSettingsController(["useCompactLayout", "useCustomFontSize"]);
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
await SettingsStore.setValue("useCustomFontSize", null, SettingLevel.DEVICE, true);
expect(controller.settingDisabled).toEqual(false);
expect(controller.getValueOverride()).toEqual(null);
});
});