Skip to content

Commit 5354450

Browse files
Half-Shotsnowping
authored andcommitted
Global configuration flag for media previews (element-hq#29582)
* Modify useMediaVisible to take a room. * Add initial support for a account data level key. * Update controls. * Update settings * Lint and fixes * make some tests go happy * lint * i18n * update preferences * prettier * Update settings tab. * update screenshot * Update docs * Rewrite controller * Rewrite tons of tests * Rewrite RoomAvatar to be a functional component This is so we can use hooks to determine the setting state. * lint * lint * Tidy up comments * Apply media visible hook to inline images. * Move conditionals. * copyright all the things * Review changes * Update html utils to properly discard media. * Types fix * Fixing tests that break settings getValue expectations * Fix logic around media preview calculation * Fix room header tests * Fixup tests for timelinePanel * Clear settings in matrixchat * Update tests to use SettingsStore where possible. * fix bug * revert changes to client.ts * copyright years * Add header * Add a test for MediaPreviewAccountSettingsTab * Mark initMatrixClient as optional * Improve on types * Ensure we do not set the account data twice. * lint * Review changes * Ensure we include the client on rendered messages. * Fix test * update labels * clean designs * update settings tab * update snapshot * copyright * prevent mutation
1 parent 2aecb69 commit 5354450

File tree

44 files changed

+1280
-275
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1280
-275
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import * as fs from "node:fs";
9+
import { type EventType, type MsgType, type RoomJoinRulesEventContent } from "matrix-js-sdk/src/types";
10+
11+
import { test, expect } from "../../element-web-test";
12+
13+
const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png");
14+
15+
test.describe("Media preview settings", () => {
16+
test.use({
17+
displayName: "Alan",
18+
room: async ({ app, page, homeserver, bot, user }, use) => {
19+
const mxc = (await bot.uploadContent(MEDIA_FILE, { name: "image.png", type: "image/png" })).content_uri;
20+
const roomId = await bot.createRoom({
21+
name: "Test room",
22+
invite: [user.userId],
23+
initial_state: [{ type: "m.room.avatar", content: { url: mxc }, state_key: "" }],
24+
});
25+
await bot.sendEvent(roomId, null, "m.room.message" as EventType, {
26+
msgtype: "m.image" as MsgType,
27+
body: "image.png",
28+
url: mxc,
29+
});
30+
31+
await use({ roomId });
32+
},
33+
});
34+
35+
test("should be able to hide avatars of inviters", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
36+
let settings = await app.settings.openUserSettings("Preferences");
37+
await settings.getByLabel("Hide avatars of room and inviter").click();
38+
await app.closeDialog();
39+
await app.viewRoomById(room.roomId);
40+
await expect(
41+
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
42+
).toMatchScreenshot("invite-no-avatar.png");
43+
await expect(
44+
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
45+
).toMatchScreenshot("invite-room-tree-no-avatar.png");
46+
47+
// And then go back to being visible
48+
settings = await app.settings.openUserSettings("Preferences");
49+
await settings.getByLabel("Hide avatars of room and inviter").click();
50+
await app.closeDialog();
51+
await page.goto("#/home");
52+
await app.viewRoomById(room.roomId);
53+
await expect(
54+
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
55+
).toMatchScreenshot("invite-with-avatar.png");
56+
await expect(
57+
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
58+
).toMatchScreenshot("invite-room-tree-with-avatar.png");
59+
});
60+
61+
test("should be able to hide media in rooms globally", async ({ page, app, room, user }) => {
62+
const settings = await app.settings.openUserSettings("Preferences");
63+
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
64+
await app.closeDialog();
65+
await app.viewRoomById(room.roomId);
66+
await page.getByRole("button", { name: "Accept" }).click();
67+
await expect(page.getByText("Show image")).toBeVisible();
68+
});
69+
test("should be able to hide media in non-private rooms globally", async ({ page, app, room, user, bot }) => {
70+
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
71+
join_rule: "public",
72+
});
73+
const settings = await app.settings.openUserSettings("Preferences");
74+
await settings.getByLabel("Show media in timeline").getByLabel("In private rooms").click();
75+
await app.closeDialog();
76+
await app.viewRoomById(room.roomId);
77+
await page.getByRole("button", { name: "Accept" }).click();
78+
await expect(page.getByText("Show image")).toBeVisible();
79+
for (const joinRule of ["invite", "knock", "restricted"] as RoomJoinRulesEventContent["join_rule"][]) {
80+
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
81+
join_rule: joinRule,
82+
} satisfies RoomJoinRulesEventContent);
83+
await expect(page.getByText("Show image")).not.toBeVisible();
84+
}
85+
});
86+
test("should be able to show media in rooms globally", async ({ page, app, room, user }) => {
87+
const settings = await app.settings.openUserSettings("Preferences");
88+
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
89+
await app.closeDialog();
90+
await app.viewRoomById(room.roomId);
91+
await page.getByRole("button", { name: "Accept" }).click();
92+
await expect(page.getByText("Show image")).not.toBeVisible();
93+
});
94+
test("should be able to hide media in an individual room", async ({ page, app, room, user }) => {
95+
const settings = await app.settings.openUserSettings("Preferences");
96+
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
97+
await app.closeDialog();
98+
99+
await app.viewRoomById(room.roomId);
100+
await page.getByRole("button", { name: "Accept" }).click();
101+
102+
const roomSettings = await app.settings.openRoomSettings("General");
103+
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
104+
await app.closeDialog();
105+
106+
await expect(page.getByText("Show image")).toBeVisible();
107+
});
108+
test("should be able to show media in an individual room", async ({ page, app, room, user }) => {
109+
const settings = await app.settings.openUserSettings("Preferences");
110+
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
111+
await app.closeDialog();
112+
113+
await app.viewRoomById(room.roomId);
114+
await page.getByRole("button", { name: "Accept" }).click();
115+
116+
const roomSettings = await app.settings.openRoomSettings("General");
117+
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
118+
await app.closeDialog();
119+
120+
await expect(page.getByText("Show image")).not.toBeVisible();
121+
});
122+
});
Loading
Loading

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@
379379
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss";
380380
@import "./views/settings/tabs/user/_HelpUserSettingsTab.pcss";
381381
@import "./views/settings/tabs/user/_KeyboardUserSettingsTab.pcss";
382+
@import "./views/settings/tabs/user/_MediaPreviewAccountSettings.pcss";
382383
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss";
383384
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.pcss";
384385
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss";
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
.mx_MediaPreviewAccountSetting_Radio {
9+
margin: var(--cpd-space-1x) 0;
10+
}
11+
12+
.mx_MediaPreviewAccountSetting {
13+
margin-top: var(--cpd-space-1x);
14+
}
15+
16+
.mx_MediaPreviewAccountSetting_RadioHelp {
17+
margin-top: 0;
18+
margin-bottom: var(--cpd-space-1x);
19+
}
20+
21+
.mx_MediaPreviewAccountSetting_Form {
22+
width: 100%;
23+
}
24+
25+
.mx_MediaPreviewAccountSetting_ToggleSwitch {
26+
font: var(--cpd-font-body-md-medium);
27+
letter-spacing: var(--cpd-font-letter-spacing-body-md);
28+
}

src/@types/matrix-js-sdk.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2024 New Vector Ltd.
2+
Copyright 2024, 2025 New Vector Ltd.
33
Copyright 2024 The Matrix.org Foundation C.I.C.
44
55
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -14,6 +14,7 @@ import type { EncryptedFile } from "matrix-js-sdk/src/types";
1414
import type { EmptyObject } from "matrix-js-sdk/src/matrix";
1515
import type { DeviceClientInformation } from "../utils/device/types.ts";
1616
import type { UserWidget } from "../utils/WidgetUtils-types.ts";
17+
import { type MediaPreviewConfig } from "./media_preview.ts";
1718

1819
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
1920
declare module "matrix-js-sdk/src/types" {
@@ -87,6 +88,8 @@ declare module "matrix-js-sdk/src/types" {
8788
"m.accepted_terms": {
8889
accepted: string[];
8990
};
91+
92+
"io.element.msc4278.media_preview_config": MediaPreviewConfig;
9093
}
9194

9295
export interface AudioContent {

src/@types/media_preview.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
export enum MediaPreviewValue {
9+
/**
10+
* Media previews should be enabled.
11+
*/
12+
On = "on",
13+
/**
14+
* Media previews should only be enabled for rooms with non-public join rules.
15+
*/
16+
Private = "private",
17+
/**
18+
* Media previews should be disabled.
19+
*/
20+
Off = "off",
21+
}
22+
23+
export const MEDIA_PREVIEW_ACCOUNT_DATA_TYPE = "io.element.msc4278.media_preview_config";
24+
export interface MediaPreviewConfig extends Record<string, unknown> {
25+
/**
26+
* Media preview setting for thumbnails of media in rooms.
27+
*/
28+
media_previews: MediaPreviewValue;
29+
/**
30+
* Media preview settings for avatars of rooms we have been invited to.
31+
*/
32+
invite_avatars: MediaPreviewValue.On | MediaPreviewValue.Off;
33+
}

0 commit comments

Comments
 (0)