diff --git a/playwright/e2e/timeline/media-preview-settings.spec.ts b/playwright/e2e/timeline/media-preview-settings.spec.ts new file mode 100644 index 0000000000..23777eea65 --- /dev/null +++ b/playwright/e2e/timeline/media-preview-settings.spec.ts @@ -0,0 +1,129 @@ +/* +Copyright 2024, 2025 New Vector Ltd. +Copyright 2022, 2023 The Matrix.org Foundation C.I.C. + +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 * as fs from "node:fs"; +import { type EventType, type MsgType } from "matrix-js-sdk/src/types"; +import { JoinRule } from "matrix-js-sdk/src/matrix"; + +import type { Locator, Page } from "@playwright/test"; +import { test, expect } from "../../element-web-test"; +import { Bot } from "../../pages/bot"; + +const ROOM_NAME = "Test room"; +const OLD_NAME = "Alan"; + +const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png"); + +test.describe("Media preview settings", () => { + test.use({ + displayName: OLD_NAME, + room: async ({ app, page, homeserver, bot, user }, use) => { + const mxc = (await bot.uploadContent(MEDIA_FILE, { name: "image.png", type: "image/png" })).content_uri; + const roomId = await bot.createRoom({ + name: ROOM_NAME, + invite: [user.userId], + initial_state: [{ type: "m.room.avatar", content: { url: mxc }, state_key: "" }], + }); + await bot.sendEvent(roomId, null, "m.room.message" as EventType, { + msgtype: "m.image" as MsgType, + body: "image.png", + url: mxc, + }); + + await use({ roomId }); + }, + }); + + test("should be able to hide avatars of inviters", { tag: "@screenshot" }, async ({ page, app, room, user }) => { + let settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Hide avatars of room and inviter").click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + expect(page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" })).toMatchScreenshot( + "invite-no-avatar.png", + ); + expect( + page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }), + ).toMatchScreenshot("invite-room-tree-no-avatar.png"); + + // And then go back to being visible + settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Hide avatars of room and inviter").click(); + await app.closeDialog(); + await page.goto("#/home"); + await app.viewRoomById(room.roomId); + expect(page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" })).toMatchScreenshot( + "invite-with-avatar.png", + ); + expect( + page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }), + ).toMatchScreenshot("invite-room-tree-with-avatar.png"); + }); + + test("should be able to hide media in rooms globally", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + await expect(page.getByText("Show image")).toBeVisible(); + }); + test("should be able to hide media in non-private rooms globally", async ({ page, app, room, user, bot }) => { + await bot.sendStateEvent(room.roomId, "m.room.join_rules", { + join_rule: "public", + }); + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByLabel("In private rooms").click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + await expect(page.getByText("Show image")).toBeVisible(); + for (const joinRule of [JoinRule.Invite, JoinRule.Knock, JoinRule.Restricted]) { + await bot.sendStateEvent(room.roomId, "m.room.join_rules", { + join_rule: joinRule, + }); + await expect(page.getByText("Show image")).not.toBeVisible(); + } + }); + test("should be able to show media in rooms globally", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click(); + await app.closeDialog(); + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + await expect(page.getByText("Show image")).not.toBeVisible(); + }); + test("should be able to hide media in an individual room", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click(); + await app.closeDialog(); + + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + + const roomSettings = await app.settings.openRoomSettings("General"); + await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click(); + await app.closeDialog(); + + await expect(page.getByText("Show image")).toBeVisible(); + }); + test("should be able to show media in an individual room", async ({ page, app, room, user }) => { + const settings = await app.settings.openUserSettings("Preferences"); + await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click(); + await app.closeDialog(); + + await app.viewRoomById(room.roomId); + await page.getByRole("button", { name: "Accept" }).click(); + + const roomSettings = await app.settings.openRoomSettings("General"); + await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click(); + await app.closeDialog(); + + await expect(page.getByText("Show image")).not.toBeVisible(); + }); +}); diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png new file mode 100644 index 0000000000..00fdb29c81 Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-no-avatar-linux.png differ diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png new file mode 100644 index 0000000000..452f08d3e2 Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-no-avatar-linux.png differ diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png new file mode 100644 index 0000000000..452f08d3e2 Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-room-tree-with-avatar-linux.png differ diff --git a/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png new file mode 100644 index 0000000000..00fdb29c81 Binary files /dev/null and b/playwright/snapshots/timeline/media-preview-settings.spec.ts/invite-with-avatar-linux.png differ diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx index 2615c083c9..e50dafd5d3 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.tsx @@ -22,6 +22,7 @@ import { SettingsSubsection } from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings"; +import { MediaPreviewAccountSettings } from "../user/MediaPreviewSetting"; interface IProps { room: Room; @@ -92,6 +93,7 @@ export default class GeneralRoomSettingsTab extends React.Component {urlPreviewSettings} + {leaveSection} diff --git a/src/components/views/settings/tabs/user/MediaPreviewSetting.tsx b/src/components/views/settings/tabs/user/MediaPreviewSetting.tsx index 859e750b21..fb22626b30 100644 --- a/src/components/views/settings/tabs/user/MediaPreviewSetting.tsx +++ b/src/components/views/settings/tabs/user/MediaPreviewSetting.tsx @@ -15,19 +15,30 @@ import { useSettingValue } from "../../../../../hooks/useSettings"; import SettingsStore from "../../../../../settings/SettingsStore"; import { SettingLevel } from "../../../../../settings/SettingLevel"; -export const MediaPreviewAccountSettings: React.FC = () => { - const currentMediaPreview = useSettingValue("mediaPreviewConfig"); +export const MediaPreviewAccountSettings: React.FC<{ roomId?: string }> = ({ roomId }) => { + const currentMediaPreview = useSettingValue("mediaPreviewConfig", roomId); + + const changeSetting = useCallback( + (newValue: MediaPreviewConfig) => { + SettingsStore.setValue( + "mediaPreviewConfig", + roomId ?? null, + roomId ? SettingLevel.ROOM_ACCOUNT : SettingLevel.ACCOUNT, + newValue, + ); + }, + [roomId], + ); const avatarOnChange = useCallback( (c: boolean) => { - const newValue = { + changeSetting({ ...currentMediaPreview, // Switch is inverted. "Hide avatars..." invite_avatars: c ? MediaPreviewValue.Off : MediaPreviewValue.On, - } satisfies MediaPreviewConfig; - SettingsStore.setValue("mediaPreviewConfig", null, SettingLevel.ACCOUNT, newValue); + }); }, - [currentMediaPreview], + [changeSetting, currentMediaPreview], ); const mediaPreviewOnChangeOff = useCallback>( @@ -35,12 +46,12 @@ export const MediaPreviewAccountSettings: React.FC = () => { if (!event.target.checked) { return; } - SettingsStore.setValue("mediaPreviewConfig", null, SettingLevel.ACCOUNT, { + changeSetting({ ...currentMediaPreview, media_previews: MediaPreviewValue.Off, - } satisfies MediaPreviewConfig); + }); }, - [currentMediaPreview], + [changeSetting, currentMediaPreview], ); const mediaPreviewOnChangePrivate = useCallback>( @@ -48,12 +59,12 @@ export const MediaPreviewAccountSettings: React.FC = () => { if (!event.target.checked) { return; } - SettingsStore.setValue("mediaPreviewConfig", null, SettingLevel.ACCOUNT, { + changeSetting({ ...currentMediaPreview, media_previews: MediaPreviewValue.Private, - } satisfies MediaPreviewConfig); + }); }, - [currentMediaPreview], + [changeSetting, currentMediaPreview], ); const mediaPreviewOnChangeOn = useCallback>( @@ -61,56 +72,71 @@ export const MediaPreviewAccountSettings: React.FC = () => { if (!event.target.checked) { return; } - SettingsStore.setValue("mediaPreviewConfig", null, SettingLevel.ACCOUNT, { + changeSetting({ ...currentMediaPreview, media_previews: MediaPreviewValue.On, - } satisfies MediaPreviewConfig); + }); }, - [currentMediaPreview], + [changeSetting, currentMediaPreview], ); return ( - - + {!roomId && ( + + )} + {/* Explict label here because htmlFor is not supported for linking to radiogroups */} + {_t("settings|media_preview|media_preview_description")} } > - - - - } - > - + + {!roomId && ( + + } + > + + + )} } > - + diff --git a/src/hooks/room/useJoinRule.ts b/src/hooks/room/useJoinRule.ts new file mode 100644 index 0000000000..f027ffe4cd --- /dev/null +++ b/src/hooks/room/useJoinRule.ts @@ -0,0 +1,30 @@ +/* +Copyright 2025 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 { useEffect, useState } from "react"; +import { EventType, type MatrixEvent, type Room, RoomStateEvent, type JoinRule } from "matrix-js-sdk/src/matrix"; +import { type Optional } from "matrix-events-sdk"; + +import { useTypedEventEmitter } from "../useEventEmitter"; + +/** + * Helper to retrieve the join rules for given room + * @param room + * @returns the current join rule + */ +export function useJoinRule(room?: Room): Optional { + const [topic, setJoinRule] = useState(room?.getJoinRule()); + useTypedEventEmitter(room?.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => { + if (ev.getType() !== EventType.RoomJoinRules) return; + setJoinRule(room?.getJoinRule()); + }); + useEffect(() => { + setJoinRule(room?.getJoinRule()); + }, [room]); + + return topic; +} diff --git a/src/hooks/room/useRoomAvatar.ts b/src/hooks/room/useRoomAvatar.ts new file mode 100644 index 0000000000..9236654cca --- /dev/null +++ b/src/hooks/room/useRoomAvatar.ts @@ -0,0 +1,30 @@ +/* +Copyright 2025 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 { useEffect, useState } from "react"; +import { EventType, type MatrixEvent, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { type Optional } from "matrix-events-sdk"; + +import { useTypedEventEmitter } from "../useEventEmitter"; + +/** + * Helper to retrieve the avatar for given room + * @param room + * @returns the current avatar + */ +export function useRoomAvatar(room?: Room): Optional { + const [topic, setAvatar] = useState(room?.getMxcAvatarUrl()); + useTypedEventEmitter(room?.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => { + if (ev.getType() !== EventType.RoomAvatar) return; + setAvatar(room?.getMxcAvatarUrl()); + }); + useEffect(() => { + setAvatar(room?.getMxcAvatarUrl()); + }, [room]); + + return topic; +} diff --git a/src/hooks/useMediaVisible.ts b/src/hooks/useMediaVisible.ts index b34f561e6c..75237e33b2 100644 --- a/src/hooks/useMediaVisible.ts +++ b/src/hooks/useMediaVisible.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import { JoinRule } from "matrix-js-sdk/src/matrix"; import { SettingLevel } from "../settings/SettingLevel"; @@ -13,6 +13,7 @@ import { useSettingValue } from "./useSettings"; import SettingsStore from "../settings/SettingsStore"; import { useMatrixClientContext } from "../contexts/MatrixClientContext"; import { MediaPreviewValue } from "../@types/media_preview"; +import { useJoinRule } from "./room/useJoinRule"; const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRule.Restricted]; @@ -25,6 +26,7 @@ export function useMediaVisible(eventId: string, roomId: string): [boolean, (vis const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId); const client = useMatrixClientContext(); const eventVisibility = useSettingValue("showMediaEventIds"); + const joinRule = useJoinRule(client.getRoom(roomId) ?? undefined); const setMediaVisible = useCallback( (visible: boolean) => { SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, { @@ -35,15 +37,7 @@ export function useMediaVisible(eventId: string, roomId: string): [boolean, (vis [eventId, eventVisibility], ); - const roomIsPrivate = useMemo(() => { - const joinRule = client?.getRoom(roomId)?.getJoinRule(); - if (PRIVATE_JOIN_RULES.includes(joinRule as JoinRule)) { - return true; - } else { - // All other join rules, and unknown will default to hiding. - return false; - } - }, [client, roomId]); + const roomIsPrivate = joinRule ? PRIVATE_JOIN_RULES.includes(joinRule) : false; const explicitEventVisiblity = eventVisibility[eventId]; // Always prefer the explicit per-event user preference here. diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 89cd5a1f0c..17859f1b3f 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -374,10 +374,6 @@ export default class SettingsStore { roomId: string | null = null, excludeDefault = false, ): Settings[S]["default"] | undefined { - if (settingName === "mediaPreviewConfig") { - console.log("GET VALUE", SETTINGS[settingName]); - } - // Verify that the setting is actually a setting if (!SETTINGS[settingName]) { throw new Error("Setting '" + settingName + "' does not appear to be a setting."); diff --git a/src/settings/controllers/MediaPreviewConfigController.ts b/src/settings/controllers/MediaPreviewConfigController.ts index 9771df59ba..ec9a234aaa 100644 --- a/src/settings/controllers/MediaPreviewConfigController.ts +++ b/src/settings/controllers/MediaPreviewConfigController.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { ClientEvent, type MatrixEvent, type MatrixClient, IContent } from "matrix-js-sdk/src/matrix"; +import { type IContent } from "matrix-js-sdk/src/matrix"; import { type AccountDataEvents } from "matrix-js-sdk/src/types"; import { @@ -31,17 +31,23 @@ export default class MediaPreviewConfigController extends MatrixClientBackedCont const inviteAvatars: MediaPreviewValue = content.invite_avatars; const validValues = Object.values(MediaPreviewValue); return { - invite_avatars: validValues.includes(inviteAvatars) ? inviteAvatars : MediaPreviewConfigController.default.invite_avatars, - media_previews: validValues.includes(mediaPreviews) ? mediaPreviews : MediaPreviewConfigController.default.media_previews, + invite_avatars: validValues.includes(inviteAvatars) + ? inviteAvatars + : MediaPreviewConfigController.default.invite_avatars, + media_previews: validValues.includes(mediaPreviews) + ? mediaPreviews + : MediaPreviewConfigController.default.media_previews, }; } + public constructor() { + super(); + } + private getValue = (roomId?: string): MediaPreviewConfig | null => { const source = roomId ? this.client?.getRoom(roomId) : this.client; - const value = source - ?.getAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE) - ?.getContent(); - + const value = source?.getAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE)?.getContent(); + if (!value) { return null; } else { @@ -49,20 +55,17 @@ export default class MediaPreviewConfigController extends MatrixClientBackedCont } }; - - protected async initMatrixClient(newClient: MatrixClient, oldClient?: MatrixClient): Promise { - + protected async initMatrixClient(): Promise { + // Unused } public getValueOverride(_level: SettingLevel, roomId: string | null): MediaPreviewConfig { - if (roomId) { - // Use globals for any undefined setting - return { - ...this.getRoomValue(roomId), - ...this.globalSetting, - }; + const roomConfig = roomId && this.getValue(roomId); + if (roomConfig) { + return roomConfig; } - return this.globalSetting; + // If no room config, or global settings request then return global. + return this.getValue() ?? MediaPreviewConfigController.default; } public get settingDisabled(): false { @@ -79,9 +82,7 @@ export default class MediaPreviewConfigController extends MatrixClientBackedCont return false; } if (roomId) { - await this.client.setRoomAccountData(roomId, MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, { - value: newValue, - }); + await this.client.setRoomAccountData(roomId, MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue); return true; } await this.client.setAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue); diff --git a/test/unit-tests/settings/controllers/MediaPreviewConfigController-test.ts b/test/unit-tests/settings/controllers/MediaPreviewConfigController-test.ts new file mode 100644 index 0000000000..444fa6b75a --- /dev/null +++ b/test/unit-tests/settings/controllers/MediaPreviewConfigController-test.ts @@ -0,0 +1,165 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2024 The Matrix.org Foundation C.I.C. + +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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController"; +import MediaPreviewConfigController from "../../../../src/settings/controllers/MediaPreviewConfigController"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import { getMockClientWithEventEmitter, mockClientMethodsServer } from "../../../test-utils"; +import { MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, MediaPreviewValue } from "../../../../src/@types/media_preview"; + +describe("MediaPreviewConfigController", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + const ROOM_ID = "!room:example.org"; + + it("gets the default settings when none are specified.", () => { + const controller = new MediaPreviewConfigController(); + + MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({ + ...mockClientMethodsServer(), + getAccountData: jest.fn().mockReturnValue(null), + }); + + const value = controller.getValueOverride(SettingLevel.ACCOUNT, null); + expect(value).toEqual(MediaPreviewConfigController.default); + }); + + it("gets the default settings when the setting is empty.", () => { + const controller = new MediaPreviewConfigController(); + + MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({ + ...mockClientMethodsServer(), + getAccountData: jest + .fn() + .mockReturnValue(new MatrixEvent({ type: MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, content: {} })), + }); + + const value = controller.getValueOverride(SettingLevel.ACCOUNT, null); + expect(value).toEqual(MediaPreviewConfigController.default); + }); + + it.each([["media_previews"], ["invite_avatars"]])("gets the correct value for %s at the global level", (key) => { + const controller = new MediaPreviewConfigController(); + + MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({ + ...mockClientMethodsServer(), + getAccountData: jest.fn().mockReturnValue( + new MatrixEvent({ + type: MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, + content: { + [key]: MediaPreviewValue.Private, + }, + }), + ), + getRoom: jest.fn().mockReturnValue({ + getAccountData: jest.fn().mockReturnValue(null), + }), + }); + + const globalValue = controller.getValueOverride(SettingLevel.ACCOUNT, null); + expect(globalValue[key]).toEqual(MediaPreviewValue.Private); + + // Should follow the global value. + const roomValue = controller.getValueOverride(SettingLevel.ROOM_ACCOUNT, ROOM_ID); + expect(roomValue[key]).toEqual(MediaPreviewValue.Private); + }); + + it.each([["media_previews"], ["invite_avatars"]])("gets the correct value for %s at the room level", (key) => { + const controller = new MediaPreviewConfigController(); + + MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({ + ...mockClientMethodsServer(), + getAccountData: jest.fn().mockReturnValue(null), + getRoom: jest.fn().mockReturnValue({ + getAccountData: jest.fn().mockReturnValue( + new MatrixEvent({ + type: MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, + content: { + [key]: MediaPreviewValue.Private, + }, + }), + ), + }), + }); + + const globalValue = controller.getValueOverride(SettingLevel.ACCOUNT, null); + expect(globalValue[key]).toEqual(MediaPreviewValue.On); + + // Should follow the global value. + const roomValue = controller.getValueOverride(SettingLevel.ROOM_ACCOUNT, ROOM_ID); + expect(roomValue[key]).toEqual(MediaPreviewValue.Private); + }); + + it.each([["media_previews"], ["invite_avatars"]])( + "uses defaults when an invalid value is set on the global level", + (key) => { + const controller = new MediaPreviewConfigController(); + + MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({ + ...mockClientMethodsServer(), + getAccountData: jest.fn().mockReturnValue( + new MatrixEvent({ + type: MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, + content: { + [key]: "bibble", + }, + }), + ), + getRoom: jest.fn().mockReturnValue({ + getAccountData: jest.fn().mockReturnValue(null), + }), + }); + + const globalValue = controller.getValueOverride(SettingLevel.ACCOUNT, null); + expect(globalValue[key]).toEqual(MediaPreviewValue.On); + + // Should follow the global value. + const roomValue = controller.getValueOverride(SettingLevel.ROOM_ACCOUNT, ROOM_ID); + expect(roomValue[key]).toEqual(MediaPreviewValue.On); + }, + ); + it.each([["media_previews"], ["invite_avatars"]])( + "uses global value when an invalid value is set on the room level", + (key) => { + const controller = new MediaPreviewConfigController(); + + MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({ + ...mockClientMethodsServer(), + getAccountData: jest.fn().mockReturnValue( + new MatrixEvent({ + type: MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, + content: { + [key]: MediaPreviewValue.Private, + }, + }), + ), + getRoom: jest.fn().mockReturnValue({ + getAccountData: jest.fn().mockReturnValue( + new MatrixEvent({ + type: MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, + content: { + [key]: "bibble", + }, + }), + ), + }), + }); + + const globalValue = controller.getValueOverride(SettingLevel.ACCOUNT, null); + expect(globalValue[key]).toEqual(MediaPreviewValue.Private); + + // Should follow the global value. + const roomValue = controller.getValueOverride(SettingLevel.ROOM_ACCOUNT, ROOM_ID); + expect(roomValue[key]).toEqual(MediaPreviewValue.On); + }, + ); +});