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:
@@ -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.
|
||||
|
||||
@@ -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")!;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user