Global configuration flag for media previews (#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
This commit is contained in:
Will Hunt
2025-04-22 10:37:47 +01:00
committed by GitHub
parent da6ac36f11
commit 75d9898dff
44 changed files with 1427 additions and 422 deletions

View File

@@ -12,6 +12,7 @@ import parse from "html-react-parser";
import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils";
import SettingsStore from "../../src/settings/SettingsStore";
import { getMockClientWithEventEmitter } from "../test-utils";
import { SettingLevel } from "../../src/settings/SettingLevel";
import SdkConfig from "../../src/SdkConfig";
@@ -231,6 +232,37 @@ describe("bodyToNode", () => {
expect(asFragment()).toMatchSnapshot();
});
it.each([[true], [false]])("should handle inline media when mediaIsVisible is %s", (mediaIsVisible) => {
const cli = getMockClientWithEventEmitter({
mxcUrlToHttp: jest.fn().mockReturnValue("https://example.org/img"),
});
const { className, formattedBody } = bodyToNode(
{
"body": "![foo](mxc://going/knowwhere) Hello there",
"format": "org.matrix.custom.html",
"formatted_body": `<img src="mxc://going/knowwhere">foo</img> Hello there`,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$eventId",
},
},
"msgtype": "m.text",
},
[],
{
mediaIsVisible,
},
);
const { asFragment } = render(
<span className={className} dir="auto" dangerouslySetInnerHTML={{ __html: formattedBody! }} />,
);
expect(asFragment()).toMatchSnapshot();
// We do not want to download untrusted media.
// eslint-disable-next-line no-restricted-properties
expect(cli.mxcUrlToHttp).toHaveBeenCalledTimes(mediaIsVisible ? 1 : 0);
});
afterEach(() => {
jest.resetAllMocks();
});

View File

@@ -64,3 +64,30 @@ exports[`bodyToNode should generate big emoji for an emoji-only reply to a messa
</span>
</DocumentFragment>
`;
exports[`bodyToNode should handle inline media when mediaIsVisible is false 1`] = `
<DocumentFragment>
<span
class="mx_EventTile_body markdown-body translate"
dir="auto"
>
<img />
foo Hello there
</span>
</DocumentFragment>
`;
exports[`bodyToNode should handle inline media when mediaIsVisible is true 1`] = `
<DocumentFragment>
<span
class="mx_EventTile_body markdown-body translate"
dir="auto"
>
<img
src="https://example.org/img"
style="max-width:800px;max-height:600px"
/>
foo Hello there
</span>
</DocumentFragment>
`;

View File

@@ -17,6 +17,7 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
import * as AvatarModule from "../../../../../src/Avatar";
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
import { MediaPreviewValue } from "../../../../../src/@types/media_preview";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
@@ -37,18 +38,18 @@ describe("RoomAvatar", () => {
});
afterAll(() => {
SettingsStore.setValue(
"mediaPreviewConfig",
null,
SettingLevel.ACCOUNT,
SettingsStore.getDefaultValue("mediaPreviewConfig"),
);
jest.restoreAllMocks();
});
afterEach(() => {
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
SettingsStore.setValue(
"showAvatarsOnInvites",
null,
SettingLevel.ACCOUNT,
SettingsStore.getDefaultValue("showAvatarsOnInvites"),
);
});
it("should render as expected for a Room", () => {
@@ -74,7 +75,6 @@ describe("RoomAvatar", () => {
expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot();
});
it("should render an avatar for a room the user is invited to", () => {
SettingsStore.setValue("showAvatarsOnInvites", null, SettingLevel.ACCOUNT, true);
const room = new Room("!room:example.com", client, client.getSafeUserId());
jest.spyOn(room, "getMxcAvatarUrl").mockImplementation(() => "mxc://example.com/foobar");
room.name = "test room";
@@ -93,7 +93,9 @@ describe("RoomAvatar", () => {
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
});
it("should not render an invite avatar if the user has disabled it", () => {
SettingsStore.setValue("showAvatarsOnInvites", null, SettingLevel.ACCOUNT, false);
SettingsStore.setValue("mediaPreviewConfig", null, SettingLevel.ACCOUNT, {
invite_avatars: MediaPreviewValue.Off,
});
const room = new Room("!room:example.com", client, client.getSafeUserId());
room.name = "test room";
room.updateMyMembership("invite");

View File

@@ -8,20 +8,20 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { fireEvent, render, screen } from "jest-matrix-react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { HideActionButton } from "../../../../../src/components/views/messages/HideActionButton";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import type { Settings } from "../../../../../src/settings/Settings";
import { MediaPreviewValue } from "../../../../../src/@types/media_preview";
import { getMockClientWithEventEmitter, withClientContextRenderOptions } from "../../../../test-utils";
import type { MockedObject } from "jest-mock";
function mockSetting(
showImages: Settings["showImages"]["default"],
showMediaEventIds: Settings["showMediaEventIds"]["default"],
) {
function mockSetting(mediaPreviews: MediaPreviewValue, showMediaEventIds: Settings["showMediaEventIds"]["default"]) {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
if (settingName === "showImages") {
return showImages;
if (settingName === "mediaPreviewConfig") {
return { media_previews: mediaPreviews, invite_avatars: MediaPreviewValue.Off };
} else if (settingName === "showMediaEventIds") {
return showMediaEventIds;
}
@@ -29,8 +29,10 @@ function mockSetting(
});
}
const EVENT_ID = "$foo:bar";
const event = new MatrixEvent({
event_id: "$foo:bar",
event_id: EVENT_ID,
room_id: "!room:id",
sender: "@user:id",
type: "m.room.message",
@@ -42,32 +44,38 @@ const event = new MatrixEvent({
});
describe("HideActionButton", () => {
let cli: MockedObject<MatrixClient>;
beforeEach(() => {
cli = getMockClientWithEventEmitter({
getRoom: jest.fn(),
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should show button when event is visible by showMediaEventIds setting", async () => {
mockSetting(false, { "$foo:bar": true });
render(<HideActionButton mxEvent={event} />);
mockSetting(MediaPreviewValue.Off, { [EVENT_ID]: true });
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
expect(screen.getByRole("button")).toBeVisible();
});
it("should show button when event is visible by showImages setting", async () => {
mockSetting(true, {});
render(<HideActionButton mxEvent={event} />);
it("should show button when event is visible by mediaPreviewConfig setting", async () => {
mockSetting(MediaPreviewValue.On, {});
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
expect(screen.getByRole("button")).toBeVisible();
});
it("should hide button when event is hidden by showMediaEventIds setting", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue({ "$foo:bar": false });
render(<HideActionButton mxEvent={event} />);
mockSetting(MediaPreviewValue.Off, { [EVENT_ID]: false });
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
expect(screen.queryByRole("button")).toBeNull();
});
it("should hide button when event is hidden by showImages setting", async () => {
mockSetting(false, {});
render(<HideActionButton mxEvent={event} />);
mockSetting(MediaPreviewValue.Off, {});
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
expect(screen.queryByRole("button")).toBeNull();
});
it("should store event as hidden when clicked", async () => {
const spy = jest.spyOn(SettingsStore, "setValue");
render(<HideActionButton mxEvent={event} />);
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
fireEvent.click(screen.getByRole("button"));
expect(spy).toHaveBeenCalledWith("showMediaEventIds", null, SettingLevel.DEVICE, { "$foo:bar": false });
// Button should be hidden after the setting is set.

View File

@@ -1,12 +1,12 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022 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 React, { act } from "react";
import React from "react";
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import fetchMock from "fetch-mock-jest";
@@ -24,10 +24,11 @@ import {
mockClientMethodsDevice,
mockClientMethodsServer,
mockClientMethodsUser,
withClientContextRenderOptions,
} from "../../../../test-utils";
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import { MediaPreviewValue } from "../../../../../src/@types/media_preview";
jest.mock("matrix-encrypt-attachment", () => ({
decryptAttachment: jest.fn(),
@@ -42,6 +43,7 @@ describe("<MImageBody/>", () => {
...mockClientMethodsDevice(deviceId),
...mockClientMethodsCrypto(),
getRooms: jest.fn().mockReturnValue([]),
getRoom: jest.fn(),
getIgnoredUsers: jest.fn(),
getVersions: jest.fn().mockResolvedValue({
unstable_features: {
@@ -85,6 +87,7 @@ describe("<MImageBody/>", () => {
});
afterEach(() => {
SettingsStore.reset();
mocked(encrypt.decryptAttachment).mockReset();
});
@@ -97,6 +100,7 @@ describe("<MImageBody/>", () => {
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
// thumbnail with dimensions present
@@ -112,6 +116,7 @@ describe("<MImageBody/>", () => {
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
expect(fetchMock).toHaveBeenCalledWith(url);
@@ -129,6 +134,7 @@ describe("<MImageBody/>", () => {
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
await screen.findByText("Error decrypting image");
@@ -136,25 +142,12 @@ describe("<MImageBody/>", () => {
describe("with image previews/thumbnails disabled", () => {
beforeEach(() => {
act(() => {
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
});
});
afterEach(() => {
act(() => {
SettingsStore.setValue(
"showImages",
null,
SettingLevel.DEVICE,
SettingsStore.getDefaultValue("showImages"),
);
SettingsStore.setValue(
"showMediaEventIds",
null,
SettingLevel.DEVICE,
SettingsStore.getDefaultValue("showMediaEventIds"),
);
const origFn = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, ...args) => {
if (setting === "mediaPreviewConfig") {
return { invite_avatars: MediaPreviewValue.Off, media_previews: MediaPreviewValue.Off };
}
return origFn(setting, ...args);
});
});
@@ -167,6 +160,7 @@ describe("<MImageBody/>", () => {
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
expect(screen.getByText("Show image")).toBeInTheDocument();
@@ -183,6 +177,7 @@ describe("<MImageBody/>", () => {
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
expect(screen.getByText("Show image")).toBeInTheDocument();
@@ -220,6 +215,7 @@ describe("<MImageBody/>", () => {
const { container } = render(
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
withClientContextRenderOptions(cli),
);
const img = container.querySelector(".mx_MImageBody_thumbnail")!;
@@ -273,6 +269,7 @@ describe("<MImageBody/>", () => {
const { container } = render(
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
withClientContextRenderOptions(cli),
);
// Wait for spinners to go away
@@ -298,6 +295,7 @@ describe("<MImageBody/>", () => {
const { container } = render(
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
withClientContextRenderOptions(cli),
);
const img = container.querySelector(".mx_MImageBody_thumbnail")!;

View File

@@ -19,6 +19,7 @@ import {
mockClientMethodsDevice,
mockClientMethodsServer,
mockClientMethodsUser,
withClientContextRenderOptions,
} from "../../../../test-utils";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import MStickerBody from "../../../../../src/components/views/messages/MStickerBody";
@@ -31,6 +32,7 @@ describe("<MStickerBody/>", () => {
...mockClientMethodsServer(),
...mockClientMethodsDevice(deviceId),
...mockClientMethodsCrypto(),
getRoom: jest.fn(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getVersions: jest.fn().mockResolvedValue({
@@ -76,7 +78,7 @@ describe("<MStickerBody/>", () => {
it("should show a tooltip on hover", async () => {
fetchMock.getOnce(url, { status: 200 });
render(<MStickerBody {...props} mxEvent={mediaEvent} />);
render(<MStickerBody {...props} mxEvent={mediaEvent} />, withClientContextRenderOptions(cli));
expect(screen.queryByRole("tooltip")).toBeNull();
await userEvent.hover(screen.getByRole("img"));

View File

@@ -1,15 +1,16 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022 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 React, { act } from "react";
import { EventType, getHttpUriForMxc, type IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import React from "react";
import { EventType, getHttpUriForMxc, type IContent, type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { fireEvent, render, screen, type RenderResult } from "jest-matrix-react";
import fetchMock from "fetch-mock-jest";
import { type MockedObject } from "jest-mock";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { type RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
@@ -20,11 +21,12 @@ import {
mockClientMethodsDevice,
mockClientMethodsServer,
mockClientMethodsUser,
withClientContextRenderOptions,
} from "../../../../test-utils";
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
import type { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { MediaPreviewValue } from "../../../../../src/@types/media_preview";
// Needed so we don't throw an error about failing to decrypt.
jest.mock("matrix-encrypt-attachment", () => ({
@@ -36,13 +38,15 @@ describe("MVideoBody", () => {
const deviceId = "DEADB33F";
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
let cli: MockedObject<MatrixClient>;
beforeEach(() => {
const cli = getMockClientWithEventEmitter({
cli = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
...mockClientMethodsServer(),
...mockClientMethodsDevice(deviceId),
...mockClientMethodsCrypto(),
getRoom: jest.fn(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getVersions: jest.fn().mockResolvedValue({
@@ -65,6 +69,7 @@ describe("MVideoBody", () => {
room_id: "!room:server",
sender: userId,
type: EventType.RoomMessage,
event_id: "$foo:bar",
content: {
body: "alt for a test video",
info: {
@@ -93,32 +98,25 @@ describe("MVideoBody", () => {
fetchMock.getOnce(thumbUrl, { status: 200 });
const { asFragment } = render(
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
withClientContextRenderOptions(cli),
);
expect(asFragment()).toMatchSnapshot();
});
describe("with video previews/thumbnails disabled", () => {
beforeEach(() => {
act(() => {
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
const origFn = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, ...args) => {
if (setting === "mediaPreviewConfig") {
return { invite_avatars: MediaPreviewValue.Off, media_previews: MediaPreviewValue.Off };
}
return origFn(setting, ...args);
});
});
afterEach(() => {
act(() => {
SettingsStore.setValue(
"showImages",
null,
SettingLevel.DEVICE,
SettingsStore.getDefaultValue("showImages"),
);
SettingsStore.setValue(
"showMediaEventIds",
null,
SettingLevel.DEVICE,
SettingsStore.getDefaultValue("showMediaEventIds"),
);
});
SettingsStore.reset();
jest.restoreAllMocks();
});
it("should not download video", async () => {
@@ -129,6 +127,7 @@ describe("MVideoBody", () => {
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
expect(screen.getByText("Show video")).toBeInTheDocument();
@@ -144,6 +143,7 @@ describe("MVideoBody", () => {
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
const placeholderButton = screen.getByRole("button", { name: "Show video" });
@@ -191,6 +191,7 @@ function makeMVideoBody(w: number, h: number): RenderResult {
const mockClient = getMockClientWithEventEmitter({
mxcUrlToHttp: jest.fn(),
getRoom: jest.fn(),
});
return render(

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -44,9 +44,13 @@ describe("RoomPreviewCard", () => {
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
enabledFeatures = [];
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any =>
enabledFeatures.includes(settingName) ? true : undefined,
);
const origFn = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => {
if (enabledFeatures.includes(settingName)) {
return true;
}
return origFn(settingName);
});
});
afterEach(() => {

View File

@@ -0,0 +1,99 @@
/*
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 { render } from "jest-matrix-react";
import React from "react";
import userEvent from "@testing-library/user-event";
import { type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MediaPreviewAccountSettings } from "../../../../../../../src/components/views/settings/tabs/user/MediaPreviewAccountSettings";
import {
getMockClientWithEventEmitter,
mockClientMethodsServer,
mockClientMethodsUser,
} from "../../../../../../test-utils";
import MatrixClientBackedController from "../../../../../../../src/settings/controllers/MatrixClientBackedController";
import MatrixClientBackedSettingsHandler from "../../../../../../../src/settings/handlers/MatrixClientBackedSettingsHandler";
import type { MockedObject } from "jest-mock";
import {
MEDIA_PREVIEW_ACCOUNT_DATA_TYPE,
type MediaPreviewConfig,
MediaPreviewValue,
} from "../../../../../../../src/@types/media_preview";
import MediaPreviewConfigController from "../../../../../../../src/settings/controllers/MediaPreviewConfigController";
describe("MediaPreviewAccountSettings", () => {
let client: MockedObject<MatrixClient>;
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsServer(),
...mockClientMethodsUser(),
getRoom: jest.fn(),
setAccountData: jest.fn(),
isVersionSupported: jest.fn().mockResolvedValue(true),
});
MatrixClientBackedController.matrixClient = client;
MatrixClientBackedSettingsHandler.matrixClient = client;
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should render", () => {
const { getByLabelText } = render(<MediaPreviewAccountSettings />);
// Defaults
expect(getByLabelText("Hide avatars of room and inviter")).not.toBeChecked();
expect(getByLabelText("Always hide")).not.toBeChecked();
expect(getByLabelText("In private rooms")).not.toBeChecked();
expect(getByLabelText("Always show")).toBeChecked();
});
it("should be able to toggle hide avatar", async () => {
const { getByLabelText } = render(<MediaPreviewAccountSettings />);
// Defaults
const element = getByLabelText("Hide avatars of room and inviter");
await userEvent.click(element);
expect(client.setAccountData).toHaveBeenCalledWith(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, {
invite_avatars: MediaPreviewValue.Off,
media_previews: MediaPreviewValue.On,
});
// Ensure we don't double set the account data.
expect(client.setAccountData).toHaveBeenCalledTimes(1);
});
// Skip the default.
it.each([
["Always hide", MediaPreviewValue.Off],
["In private rooms", MediaPreviewValue.Private],
["Always show", MediaPreviewValue.On],
])("should be able to toggle media preview option %s", async (key, value) => {
if (value === MediaPreviewConfigController.default.media_previews) {
// This is the default, so switch away first.
client.getAccountData.mockImplementation((type) => {
if (type === MEDIA_PREVIEW_ACCOUNT_DATA_TYPE) {
return new MatrixEvent({
content: {
media_previews: MediaPreviewValue.Off,
} satisfies Partial<MediaPreviewConfig>,
});
}
return undefined;
});
}
const { getByLabelText } = render(<MediaPreviewAccountSettings />);
const element = getByLabelText(key);
await userEvent.click(element);
expect(client.setAccountData).toHaveBeenCalledWith(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, {
invite_avatars: MediaPreviewValue.On,
media_previews: value,
});
// Ensure we don't double set the account data.
expect(client.setAccountData).toHaveBeenCalledTimes(1);
});
});

View File

@@ -95,33 +95,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_QgU2PomxwKpa"
>
<span
class="mx_SettingsFlag_labelText"
>
Show avatars of rooms you have been invited to
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show avatars of rooms you have been invited to"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_QgU2PomxwKpa"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
<div
@@ -144,7 +117,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_6hpi3YEetmBG"
for="mx_SettingsFlag_QgU2PomxwKpa"
>
<span
class="mx_SettingsFlag_labelText"
@@ -162,7 +135,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show all rooms in Home"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_6hpi3YEetmBG"
id="mx_SettingsFlag_QgU2PomxwKpa"
role="switch"
tabindex="0"
>
@@ -212,7 +185,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_4yVCeEefiPqp"
for="mx_SettingsFlag_6hpi3YEetmBG"
>
<span
class="mx_SettingsFlag_labelText"
@@ -225,7 +198,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Use Ctrl + F to search timeline"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_4yVCeEefiPqp"
id="mx_SettingsFlag_6hpi3YEetmBG"
role="switch"
tabindex="0"
>
@@ -285,7 +258,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_MRMwbPDmfGtm"
for="mx_SettingsFlag_4yVCeEefiPqp"
>
<span
class="mx_SettingsFlag_labelText"
@@ -298,6 +271,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show timestamps in 12 hour format (e.g. 2:30pm)"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_4yVCeEefiPqp"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_MRMwbPDmfGtm"
>
<span
class="mx_SettingsFlag_labelText"
>
Always show message timestamps
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Always show message timestamps"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_MRMwbPDmfGtm"
role="switch"
tabindex="0"
@@ -313,33 +313,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_GQvdMWe954DV"
>
<span
class="mx_SettingsFlag_labelText"
>
Always show message timestamps
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Always show message timestamps"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_GQvdMWe954DV"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_IAu5CsiHRD7n"
>
<span
class="mx_SettingsFlag_labelText"
@@ -352,7 +325,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Publish timezone on public profile"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_IAu5CsiHRD7n"
id="mx_SettingsFlag_GQvdMWe954DV"
role="switch"
tabindex="0"
>
@@ -392,7 +365,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_yrA2ohjWVJIP"
for="mx_SettingsFlag_IAu5CsiHRD7n"
>
<span
class="mx_SettingsFlag_labelText"
@@ -405,7 +378,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Send read receipts"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_yrA2ohjWVJIP"
id="mx_SettingsFlag_IAu5CsiHRD7n"
role="switch"
tabindex="0"
>
@@ -419,7 +392,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_auy1OmnTidX4"
for="mx_SettingsFlag_yrA2ohjWVJIP"
>
<span
class="mx_SettingsFlag_labelText"
@@ -432,7 +405,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Send typing notifications"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_auy1OmnTidX4"
id="mx_SettingsFlag_yrA2ohjWVJIP"
role="switch"
tabindex="0"
>
@@ -463,7 +436,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_ePDS0OpWwAHG"
for="mx_SettingsFlag_auy1OmnTidX4"
>
<span
class="mx_SettingsFlag_labelText"
@@ -476,7 +449,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Automatically replace plain text Emoji"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_ePDS0OpWwAHG"
id="mx_SettingsFlag_auy1OmnTidX4"
role="switch"
tabindex="0"
>
@@ -490,7 +463,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_75JNTNkNU64r"
for="mx_SettingsFlag_ePDS0OpWwAHG"
>
<span
class="mx_SettingsFlag_labelText"
@@ -514,6 +487,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Enable Markdown"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_ePDS0OpWwAHG"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_75JNTNkNU64r"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable Emoji suggestions while typing
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Enable Emoji suggestions while typing"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_75JNTNkNU64r"
role="switch"
tabindex="0"
@@ -533,14 +533,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Enable Emoji suggestions while typing
Use Ctrl + Enter to send a message
</span>
</label>
<div
aria-checked="true"
aria-checked="false"
aria-disabled="true"
aria-label="Enable Emoji suggestions while typing"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
aria-label="Use Ctrl + Enter to send a message"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_aTLcRsQRlYy7"
role="switch"
tabindex="0"
@@ -560,13 +560,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Use Ctrl + Enter to send a message
Surround selected text when typing special characters
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Use Ctrl + Enter to send a message"
aria-label="Surround selected text when typing special characters"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_5nfv5bOEPN1s"
role="switch"
@@ -587,14 +587,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Surround selected text when typing special characters
Show stickers button
</span>
</label>
<div
aria-checked="false"
aria-checked="true"
aria-disabled="true"
aria-label="Surround selected text when typing special characters"
class="mx_AccessibleButton mx_ToggleSwitch"
aria-label="Show stickers button"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_u1JYVtOyR5kb"
role="switch"
tabindex="0"
@@ -610,33 +610,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_u3pEwuLn9Enn"
>
<span
class="mx_SettingsFlag_labelText"
>
Show stickers button
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show stickers button"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_u3pEwuLn9Enn"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_YuxfFEpOsztW"
>
<span
class="mx_SettingsFlag_labelText"
@@ -649,7 +622,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Insert a trailing colon after user mentions at the start of a message"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_YuxfFEpOsztW"
id="mx_SettingsFlag_u3pEwuLn9Enn"
role="switch"
tabindex="0"
>
@@ -680,7 +653,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_hQkBerF1ejc4"
for="mx_SettingsFlag_YuxfFEpOsztW"
>
<span
class="mx_SettingsFlag_labelText"
@@ -693,6 +666,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Enable automatic language detection for syntax highlighting"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_YuxfFEpOsztW"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_hQkBerF1ejc4"
>
<span
class="mx_SettingsFlag_labelText"
>
Expand code blocks by default
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Expand code blocks by default"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_hQkBerF1ejc4"
role="switch"
tabindex="0"
@@ -708,33 +708,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_GFes1UFzOK2n"
>
<span
class="mx_SettingsFlag_labelText"
>
Expand code blocks by default
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Expand code blocks by default"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_GFes1UFzOK2n"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vfGFMldL2r2v"
>
<span
class="mx_SettingsFlag_labelText"
@@ -747,7 +720,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show line numbers in code blocks"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_vfGFMldL2r2v"
id="mx_SettingsFlag_GFes1UFzOK2n"
role="switch"
tabindex="0"
>
@@ -778,7 +751,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_bsSwicmKUiOB"
for="mx_SettingsFlag_vfGFMldL2r2v"
>
<span
class="mx_SettingsFlag_labelText"
@@ -791,6 +764,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_vfGFMldL2r2v"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_bsSwicmKUiOB"
>
<span
class="mx_SettingsFlag_labelText"
>
Autoplay GIFs
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Autoplay GIFs"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_bsSwicmKUiOB"
role="switch"
tabindex="0"
@@ -806,33 +806,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_dvqsxEaZtl3A"
>
<span
class="mx_SettingsFlag_labelText"
>
Autoplay GIFs
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Autoplay GIFs"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_dvqsxEaZtl3A"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_NIiWzqsApP1c"
>
<span
class="mx_SettingsFlag_labelText"
@@ -845,34 +818,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Autoplay videos"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_NIiWzqsApP1c"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_q1SIAPqLMVXh"
>
<span
class="mx_SettingsFlag_labelText"
>
Show previews/thumbnails for images
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show previews/thumbnails for images"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_q1SIAPqLMVXh"
id="mx_SettingsFlag_dvqsxEaZtl3A"
role="switch"
tabindex="0"
>
@@ -903,7 +849,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_dXFDGgBsKXay"
for="mx_SettingsFlag_NIiWzqsApP1c"
>
<span
class="mx_SettingsFlag_labelText"
@@ -916,6 +862,60 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show typing notifications"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_NIiWzqsApP1c"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_q1SIAPqLMVXh"
>
<span
class="mx_SettingsFlag_labelText"
>
Show a placeholder for removed messages
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show a placeholder for removed messages"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_q1SIAPqLMVXh"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_dXFDGgBsKXay"
>
<span
class="mx_SettingsFlag_labelText"
>
Show read receipts sent by other users
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show read receipts sent by other users"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_dXFDGgBsKXay"
role="switch"
tabindex="0"
@@ -935,13 +935,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show a placeholder for removed messages
Show join/leave messages (invites/removes/bans unaffected)
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show a placeholder for removed messages"
aria-label="Show join/leave messages (invites/removes/bans unaffected)"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_7Az0xw4Bs4Tt"
role="switch"
@@ -962,13 +962,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show read receipts sent by other users
Show display name changes
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show read receipts sent by other users"
aria-label="Show display name changes"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_8jmzPIlPoBCv"
role="switch"
@@ -989,13 +989,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show join/leave messages (invites/removes/bans unaffected)
Show chat effects (animations when receiving e.g. confetti)
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show join/leave messages (invites/removes/bans unaffected)"
aria-label="Show chat effects (animations when receiving e.g. confetti)"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_enFRaTjdsFou"
role="switch"
@@ -1016,13 +1016,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show display name changes
Show profile picture changes
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show display name changes"
aria-label="Show profile picture changes"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_bfwnd5rz4XNX"
role="switch"
@@ -1043,13 +1043,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show chat effects (animations when receiving e.g. confetti)
Show avatars in user, room and event mentions
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show chat effects (animations when receiving e.g. confetti)"
aria-label="Show avatars in user, room and event mentions"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_gs5uWEzYzZrS"
role="switch"
@@ -1070,13 +1070,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show profile picture changes
Enable big emoji in chat
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show profile picture changes"
aria-label="Enable big emoji in chat"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_qWg7OgID1yRR"
role="switch"
@@ -1097,13 +1097,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show avatars in user, room and event mentions
Jump to the bottom of the timeline when you send a message
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show avatars in user, room and event mentions"
aria-label="Jump to the bottom of the timeline when you send a message"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_pOPewl7rtMbV"
role="switch"
@@ -1124,14 +1124,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Enable big emoji in chat
Show current profile picture and name for users in message history
</span>
</label>
<div
aria-checked="true"
aria-checked="false"
aria-disabled="true"
aria-label="Enable big emoji in chat"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
aria-label="Show current profile picture and name for users in message history"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_cmt3PZSyNp3v"
role="switch"
tabindex="0"
@@ -1141,62 +1141,170 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_dJJz3lHUv9XX"
>
<span
class="mx_SettingsFlag_labelText"
>
Jump to the bottom of the timeline when you send a message
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Jump to the bottom of the timeline when you send a message"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_dJJz3lHUv9XX"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_SBSSOZDRlzlA"
>
<span
class="mx_SettingsFlag_labelText"
>
Show current profile picture and name for users in message history
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Show current profile picture and name for users in message history"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_SBSSOZDRlzlA"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
<div
class="mx_SettingsSubsection mx_SettingsSubsection_newUi"
>
<div
class="mx_SettingsSubsectionHeading"
>
<h3
class="mx_Heading_h4 mx_SettingsSubsectionHeading_heading"
>
Moderation and safety
</h3>
</div>
<div
class="mx_SettingsSubsection_content mx_SettingsSubsection_content_newUi"
>
<form
class="_root_19upo_16 mx_MediaPreviewAccountSetting_Form"
>
<div
class="mx_SettingsFlag mx_MediaPreviewAccountSetting_ToggleSwitch"
>
<span
class="mx_SettingsFlag_label"
>
<div
id="mx_LabelledToggleSwitch_«r8»"
>
Hide avatars of room and inviter
</div>
</span>
<div
aria-checked="false"
aria-disabled="false"
aria-labelledby="mx_LabelledToggleSwitch_«r8»"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
aria-label="Show media in timeline"
class="_field_19upo_26"
id="mx_media_previews"
role="radiogroup"
>
<label
class="_label_19upo_59"
for="radix-«r9»"
>
Show media in timeline
</label>
<span
class="_message_19upo_85 _help-message_19upo_91 mx_MediaPreviewAccountSetting_RadioHelp"
id="radix-«ra»"
>
A hidden media can always be shown by tapping on it
</span>
<div
class="_inline-field_19upo_32 mx_MediaPreviewAccountSetting_Radio"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_1e0uz_10"
>
<input
class="_input_1e0uz_18"
id="mx_media_previews_off"
type="radio"
/>
<div
class="_ui_1e0uz_19"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_media_previews_off"
>
Always hide
</label>
</div>
</div>
<div
class="_inline-field_19upo_32 mx_MediaPreviewAccountSetting_Radio"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_1e0uz_10"
>
<input
class="_input_1e0uz_18"
id="mx_media_previews_private"
type="radio"
/>
<div
class="_ui_1e0uz_19"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_media_previews_private"
>
In private rooms
</label>
</div>
</div>
<div
class="_inline-field_19upo_32 mx_MediaPreviewAccountSetting_Radio"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_1e0uz_10"
>
<input
checked=""
class="_input_1e0uz_18"
id="mx_media_previews_on"
type="radio"
/>
<div
class="_ui_1e0uz_19"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="mx_media_previews_on"
>
Always show
</label>
</div>
</div>
</div>
</form>
</div>
<div
class="_separator_7ckbw_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
</div>
<div
class="mx_SettingsSubsection"
>
@@ -1217,7 +1325,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_FLEpLCb0jpp6"
for="mx_SettingsFlag_dJJz3lHUv9XX"
>
<span
class="mx_SettingsFlag_labelText"
@@ -1230,7 +1338,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show NSFW content"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_FLEpLCb0jpp6"
id="mx_SettingsFlag_dJJz3lHUv9XX"
role="switch"
tabindex="0"
>
@@ -1261,7 +1369,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_NQFWldEwbV3q"
for="mx_SettingsFlag_SBSSOZDRlzlA"
>
<span
class="mx_SettingsFlag_labelText"
@@ -1274,7 +1382,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Prompt before sending invites to potentially invalid matrix IDs"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_NQFWldEwbV3q"
id="mx_SettingsFlag_SBSSOZDRlzlA"
role="switch"
tabindex="0"
>

View File

@@ -6,48 +6,74 @@ Please see LICENSE files in the repository root for full details.
*/
import { act, renderHook, waitFor } from "jest-matrix-react";
import { JoinRule, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { useMediaVisible } from "../../../src/hooks/useMediaVisible";
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../test-utils";
import { type MediaPreviewConfig, MediaPreviewValue } from "../../../src/@types/media_preview";
import MediaPreviewConfigController from "../../../src/settings/controllers/MediaPreviewConfigController";
import SettingsStore from "../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../src/settings/SettingLevel";
const EVENT_ID = "$fibble:example.org";
function render() {
return renderHook(() => useMediaVisible(EVENT_ID));
}
const ROOM_ID = "!foobar:example.org";
describe("useMediaVisible", () => {
afterEach(() => {
// Using act here as otherwise React warns about state updates not being wrapped.
act(() => {
SettingsStore.setValue(
"showMediaEventIds",
null,
SettingLevel.DEVICE,
SettingsStore.getDefaultValue("showMediaEventIds"),
);
SettingsStore.setValue(
"showImages",
null,
SettingLevel.DEVICE,
SettingsStore.getDefaultValue("showImages"),
);
let matrixClient: MatrixClient;
let room: Room;
const mediaPreviewConfig: MediaPreviewConfig = MediaPreviewConfigController.default;
function render() {
return renderHook(() => useMediaVisible(EVENT_ID, ROOM_ID), withClientContextRenderOptions(matrixClient));
}
beforeEach(() => {
matrixClient = createTestClient();
room = mkStubRoom(ROOM_ID, undefined, matrixClient);
matrixClient.getRoom = jest.fn().mockReturnValue(room);
const origFn = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, ...args) => {
if (setting === "mediaPreviewConfig") {
return mediaPreviewConfig;
}
return origFn(setting, ...args);
});
});
it("should display images by default", async () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("should display media by default", async () => {
const { result } = render();
expect(result.current[0]).toEqual(true);
});
it("should hide images when the default is changed", async () => {
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
it("should hide media when media previews are Off", async () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const { result } = render();
expect(result.current[0]).toEqual(false);
});
it("should hide images after function is called", async () => {
it.each([[JoinRule.Invite], [JoinRule.Knock], [JoinRule.Restricted]])(
"should display media when media previews are Private and the join rule is %s",
async (rule) => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Private;
room.currentState.getJoinRule = jest.fn().mockReturnValue(rule);
const { result } = render();
expect(result.current[0]).toEqual(true);
},
);
it.each([[JoinRule.Public], ["anything_else"]])(
"should hide media when media previews are Private and the join rule is %s",
async (rule) => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Private;
room.currentState.getJoinRule = jest.fn().mockReturnValue(rule);
const { result } = render();
expect(result.current[0]).toEqual(false);
},
);
it("should hide media after function is called", async () => {
const { result } = render();
expect(result.current[0]).toEqual(true);
act(() => {
@@ -57,8 +83,8 @@ describe("useMediaVisible", () => {
expect(result.current[0]).toEqual(false);
});
});
it("should show images after function is called", async () => {
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
it("should show media after function is called", async () => {
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
const { result } = render();
expect(result.current[0]).toEqual(false);
act(() => {

View File

@@ -0,0 +1,164 @@
/*
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 { 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.Off,
},
}),
),
getRoom: jest.fn().mockReturnValue({
getAccountData: jest.fn().mockReturnValue(null),
}),
});
const globalValue = controller.getValueOverride(SettingLevel.ACCOUNT, null);
expect(globalValue[key]).toEqual(MediaPreviewValue.Off);
// Should follow the global value.
const roomValue = controller.getValueOverride(SettingLevel.ROOM_ACCOUNT, ROOM_ID);
expect(roomValue[key]).toEqual(MediaPreviewValue.Off);
});
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.Off,
},
}),
),
}),
});
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.Off);
});
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.Off,
},
}),
),
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.Off);
// Should follow the global value.
const roomValue = controller.getValueOverride(SettingLevel.ROOM_ACCOUNT, ROOM_ID);
expect(roomValue[key]).toEqual(MediaPreviewValue.Off);
},
);
});