You've already forked element-web
mirror of
https://github.com/element-hq/element-web.git
synced 2025-08-08 03:42:14 +03:00
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:
122
playwright/e2e/timeline/media-preview-settings.spec.ts
Normal file
122
playwright/e2e/timeline/media-preview-settings.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
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 * as fs from "node:fs";
|
||||||
|
import { type EventType, type MsgType, type RoomJoinRulesEventContent } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png");
|
||||||
|
|
||||||
|
test.describe("Media preview settings", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alan",
|
||||||
|
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: "Test room",
|
||||||
|
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);
|
||||||
|
await expect(
|
||||||
|
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
|
||||||
|
).toMatchScreenshot("invite-no-avatar.png");
|
||||||
|
await 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);
|
||||||
|
await expect(
|
||||||
|
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
|
||||||
|
).toMatchScreenshot("invite-with-avatar.png");
|
||||||
|
await 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 ["invite", "knock", "restricted"] as RoomJoinRulesEventContent["join_rule"][]) {
|
||||||
|
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
|
||||||
|
join_rule: joinRule,
|
||||||
|
} satisfies RoomJoinRulesEventContent);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
Binary file not shown.
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 241 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@@ -378,6 +378,7 @@
|
|||||||
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss";
|
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss";
|
||||||
@import "./views/settings/tabs/user/_HelpUserSettingsTab.pcss";
|
@import "./views/settings/tabs/user/_HelpUserSettingsTab.pcss";
|
||||||
@import "./views/settings/tabs/user/_KeyboardUserSettingsTab.pcss";
|
@import "./views/settings/tabs/user/_KeyboardUserSettingsTab.pcss";
|
||||||
|
@import "./views/settings/tabs/user/_MediaPreviewAccountSettings.pcss";
|
||||||
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss";
|
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.pcss";
|
||||||
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.pcss";
|
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.pcss";
|
||||||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss";
|
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.pcss";
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_MediaPreviewAccountSetting_Radio {
|
||||||
|
margin: var(--cpd-space-1x) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MediaPreviewAccountSetting {
|
||||||
|
margin-top: var(--cpd-space-1x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MediaPreviewAccountSetting_RadioHelp {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: var(--cpd-space-1x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MediaPreviewAccountSetting_Form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MediaPreviewAccountSetting_ToggleSwitch {
|
||||||
|
font: var(--cpd-font-body-md-medium);
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
||||||
|
}
|
5
src/@types/matrix-js-sdk.d.ts
vendored
5
src/@types/matrix-js-sdk.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
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
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -14,6 +14,7 @@ import type { EncryptedFile } from "matrix-js-sdk/src/types";
|
|||||||
import type { EmptyObject } from "matrix-js-sdk/src/matrix";
|
import type { EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||||
import type { DeviceClientInformation } from "../utils/device/types.ts";
|
import type { DeviceClientInformation } from "../utils/device/types.ts";
|
||||||
import type { UserWidget } from "../utils/WidgetUtils-types.ts";
|
import type { UserWidget } from "../utils/WidgetUtils-types.ts";
|
||||||
|
import { type MediaPreviewConfig } from "./media_preview.ts";
|
||||||
|
|
||||||
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
||||||
declare module "matrix-js-sdk/src/types" {
|
declare module "matrix-js-sdk/src/types" {
|
||||||
@@ -87,6 +88,8 @@ declare module "matrix-js-sdk/src/types" {
|
|||||||
"m.accepted_terms": {
|
"m.accepted_terms": {
|
||||||
accepted: string[];
|
accepted: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"io.element.msc4278.media_preview_config": MediaPreviewConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioContent {
|
export interface AudioContent {
|
||||||
|
33
src/@types/media_preview.ts
Normal file
33
src/@types/media_preview.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum MediaPreviewValue {
|
||||||
|
/**
|
||||||
|
* Media previews should be enabled.
|
||||||
|
*/
|
||||||
|
On = "on",
|
||||||
|
/**
|
||||||
|
* Media previews should only be enabled for rooms with non-public join rules.
|
||||||
|
*/
|
||||||
|
Private = "private",
|
||||||
|
/**
|
||||||
|
* Media previews should be disabled.
|
||||||
|
*/
|
||||||
|
Off = "off",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MEDIA_PREVIEW_ACCOUNT_DATA_TYPE = "io.element.msc4278.media_preview_config";
|
||||||
|
export interface MediaPreviewConfig extends Record<string, unknown> {
|
||||||
|
/**
|
||||||
|
* Media preview setting for thumbnails of media in rooms.
|
||||||
|
*/
|
||||||
|
media_previews: MediaPreviewValue;
|
||||||
|
/**
|
||||||
|
* Media preview settings for avatars of rooms we have been invited to.
|
||||||
|
*/
|
||||||
|
invite_avatars: MediaPreviewValue.On | MediaPreviewValue.Off;
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
@@ -294,6 +294,10 @@ export interface EventRenderOpts {
|
|||||||
disableBigEmoji?: boolean;
|
disableBigEmoji?: boolean;
|
||||||
stripReplyFallback?: boolean;
|
stripReplyFallback?: boolean;
|
||||||
forComposerQuote?: boolean;
|
forComposerQuote?: boolean;
|
||||||
|
/**
|
||||||
|
* Should inline media be rendered?
|
||||||
|
*/
|
||||||
|
mediaIsVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): EventAnalysis {
|
function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): EventAnalysis {
|
||||||
@@ -302,6 +306,20 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
|
|||||||
sanitizeParams = composerSanitizeHtmlParams;
|
sanitizeParams = composerSanitizeHtmlParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.mediaIsVisible === false && sanitizeParams.transformTags?.["img"]) {
|
||||||
|
// Prevent mutating the source of sanitizeParams.
|
||||||
|
sanitizeParams = {
|
||||||
|
...sanitizeParams,
|
||||||
|
transformTags: {
|
||||||
|
...sanitizeParams.transformTags,
|
||||||
|
img: (tagName) => {
|
||||||
|
// Remove element
|
||||||
|
return { tagName, attribs: {} };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isFormattedBody =
|
const isFormattedBody =
|
||||||
content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string";
|
content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string";
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
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
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -12,7 +12,6 @@ import { merge } from "lodash";
|
|||||||
import _Linkify from "linkify-react";
|
import _Linkify from "linkify-react";
|
||||||
|
|
||||||
import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix";
|
import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
||||||
@@ -47,10 +46,7 @@ export const transformTags: NonNullable<IOptions["transformTags"]> = {
|
|||||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||||
// we don't want to allow images with `https?` `src`s.
|
// we don't want to allow images with `https?` `src`s.
|
||||||
// We also drop inline images (as if they were not present at all) when the "show
|
if (!src) {
|
||||||
// images" preference is disabled. Future work might expose some UI to reveal them
|
|
||||||
// like standalone image events have.
|
|
||||||
if (!src || !SettingsStore.getValue("showImages")) {
|
|
||||||
return { tagName, attribs: {} };
|
return { tagName, attribs: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +74,6 @@ export const transformTags: NonNullable<IOptions["transformTags"]> = {
|
|||||||
if (requestedHeight) {
|
if (requestedHeight) {
|
||||||
attribs.style += "height: 100%;";
|
attribs.style += "height: 100%;";
|
||||||
}
|
}
|
||||||
|
|
||||||
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!;
|
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!;
|
||||||
return { tagName, attribs };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
|
@@ -20,6 +20,7 @@ import { filterBoolean } from "../../../utils/arrays";
|
|||||||
import { useSettingValue } from "../../../hooks/useSettings";
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
import { useRoomState } from "../../../hooks/useRoomState";
|
import { useRoomState } from "../../../hooks/useRoomState";
|
||||||
import { useRoomIdName } from "../../../hooks/room/useRoomIdName";
|
import { useRoomIdName } from "../../../hooks/room/useRoomIdName";
|
||||||
|
import { MediaPreviewValue } from "../../../@types/media_preview";
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick" | "size"> {
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick" | "size"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
@@ -40,7 +41,8 @@ const RoomAvatar: React.FC<IProps> = ({ room, viewAvatarOnClick, onClick, oobDat
|
|||||||
const avatarEvent = useRoomState(room, (state) => state.getStateEvents(EventType.RoomAvatar, ""));
|
const avatarEvent = useRoomState(room, (state) => state.getStateEvents(EventType.RoomAvatar, ""));
|
||||||
const roomIdName = useRoomIdName(room, oobData);
|
const roomIdName = useRoomIdName(room, oobData);
|
||||||
|
|
||||||
const showAvatarsOnInvites = useSettingValue("showAvatarsOnInvites", room?.roomId);
|
const showAvatarsOnInvites =
|
||||||
|
useSettingValue("mediaPreviewConfig", room?.roomId).invite_avatars === MediaPreviewValue.On;
|
||||||
|
|
||||||
const onRoomAvatarClick = useCallback(() => {
|
const onRoomAvatarClick = useCallback(() => {
|
||||||
const avatarUrl = Avatar.avatarUrlForRoom(room ?? null);
|
const avatarUrl = Avatar.avatarUrlForRoom(room ?? null);
|
||||||
@@ -63,7 +65,6 @@ const RoomAvatar: React.FC<IProps> = ({ room, viewAvatarOnClick, onClick, oobDat
|
|||||||
// parseInt ignores suffixes.
|
// parseInt ignores suffixes.
|
||||||
const sizeInt = parseInt(size, 10);
|
const sizeInt = parseInt(size, 10);
|
||||||
let oobAvatar: string | null = null;
|
let oobAvatar: string | null = null;
|
||||||
|
|
||||||
if (oobData?.avatarUrl) {
|
if (oobData?.avatarUrl) {
|
||||||
oobAvatar = mediaFromMxc(oobData?.avatarUrl).getThumbnailOfSourceHttp(sizeInt, sizeInt, "crop");
|
oobAvatar = mediaFromMxc(oobData?.avatarUrl).getThumbnailOfSourceHttp(sizeInt, sizeInt, "crop");
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,7 @@ import {
|
|||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext.tsx";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext.tsx";
|
||||||
import { useSettingValue } from "../../../hooks/useSettings.ts";
|
import { useSettingValue } from "../../../hooks/useSettings.ts";
|
||||||
import { filterBoolean } from "../../../utils/arrays.ts";
|
import { filterBoolean } from "../../../utils/arrays.ts";
|
||||||
|
import { useMediaVisible } from "../../../hooks/useMediaVisible.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a RegExp pattern for the keyword in the push rule of the given Matrix event, if any
|
* Returns a RegExp pattern for the keyword in the push rule of the given Matrix event, if any
|
||||||
@@ -150,6 +151,7 @@ const EventContentBody = memo(
|
|||||||
forwardRef<HTMLElement, Props>(
|
forwardRef<HTMLElement, Props>(
|
||||||
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ...options }, ref) => {
|
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ...options }, ref) => {
|
||||||
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
|
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
|
||||||
|
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
|
||||||
|
|
||||||
const replacer = useReplacer(content, mxEvent, options);
|
const replacer = useReplacer(content, mxEvent, options);
|
||||||
const linkifyOptions = useMemo(
|
const linkifyOptions = useMemo(
|
||||||
@@ -167,8 +169,9 @@ const EventContentBody = memo(
|
|||||||
disableBigEmoji: isEmote || !enableBigEmoji,
|
disableBigEmoji: isEmote || !enableBigEmoji,
|
||||||
// Part of Replies fallback support
|
// Part of Replies fallback support
|
||||||
stripReplyFallback: stripReply,
|
stripReplyFallback: stripReply,
|
||||||
|
mediaIsVisible,
|
||||||
}),
|
}),
|
||||||
[content, enableBigEmoji, highlights, isEmote, stripReply],
|
[content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (as === "div") includeDir = true; // force dir="auto" on divs
|
if (as === "div") includeDir = true; // force dir="auto" on divs
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -25,7 +25,7 @@ interface IProps {
|
|||||||
* Quick action button for marking a media event as hidden.
|
* Quick action button for marking a media event as hidden.
|
||||||
*/
|
*/
|
||||||
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
|
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
|
||||||
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId()!);
|
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
|
||||||
|
|
||||||
if (!mediaIsVisible) {
|
if (!mediaIsVisible) {
|
||||||
return;
|
return;
|
||||||
|
@@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
// Wrap MImageBody component so we can use a hook here.
|
// Wrap MImageBody component so we can use a hook here.
|
||||||
const MImageBody: React.FC<IBodyProps> = (props) => {
|
const MImageBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||||
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||||
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MStickerBody: React.FC<IBodyProps> = (props) => {
|
const MStickerBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||||
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {
|
|||||||
|
|
||||||
// Wrap MVideoBody component so we can use a hook here.
|
// Wrap MVideoBody component so we can use a hook here.
|
||||||
const MVideoBody: React.FC<IBodyProps> = (props) => {
|
const MVideoBody: React.FC<IBodyProps> = (props) => {
|
||||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||||
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -17,6 +17,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
|
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||||
|
|
||||||
const INITIAL_NUM_PREVIEWS = 2;
|
const INITIAL_NUM_PREVIEWS = 2;
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ interface IProps {
|
|||||||
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
|
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const [expanded, toggleExpanded] = useStateToggle();
|
const [expanded, toggleExpanded] = useStateToggle();
|
||||||
|
const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
|
||||||
|
|
||||||
const ts = mxEvent.getTs();
|
const ts = mxEvent.getTs();
|
||||||
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
|
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
|
||||||
@@ -55,7 +57,13 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) =
|
|||||||
return (
|
return (
|
||||||
<div className="mx_LinkPreviewGroup">
|
<div className="mx_LinkPreviewGroup">
|
||||||
{showPreviews.map(([link, preview], i) => (
|
{showPreviews.map(([link, preview], i) => (
|
||||||
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
|
<LinkPreviewWidget
|
||||||
|
mediaVisible={mediaVisible}
|
||||||
|
key={link}
|
||||||
|
link={link}
|
||||||
|
preview={preview}
|
||||||
|
mxEvent={mxEvent}
|
||||||
|
>
|
||||||
{i === 0 ? (
|
{i === 0 ? (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_LinkPreviewGroup_hide"
|
className="mx_LinkPreviewGroup_hide"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
|
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -11,7 +11,6 @@ import { decode } from "html-entities";
|
|||||||
import { type MatrixEvent, type IPreviewUrlResponse } from "matrix-js-sdk/src/matrix";
|
import { type MatrixEvent, type IPreviewUrlResponse } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { Linkify } from "../../../HtmlUtils";
|
import { Linkify } from "../../../HtmlUtils";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import * as ImageUtils from "../../../ImageUtils";
|
import * as ImageUtils from "../../../ImageUtils";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
@@ -24,6 +23,7 @@ interface IProps {
|
|||||||
preview: IPreviewUrlResponse;
|
preview: IPreviewUrlResponse;
|
||||||
mxEvent: MatrixEvent; // the Event associated with the preview
|
mxEvent: MatrixEvent; // the Event associated with the preview
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
mediaVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LinkPreviewWidget extends React.Component<IProps> {
|
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
@@ -69,7 +69,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||||||
|
|
||||||
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
|
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
|
||||||
let image: string | null = p["og:image"] ?? null;
|
let image: string | null = p["og:image"] ?? null;
|
||||||
if (!SettingsStore.getValue("showImages")) {
|
if (!this.props.mediaVisible) {
|
||||||
image = null; // Don't render a button to show the image, just hide it outright
|
image = null; // Don't render a button to show the image, just hide it outright
|
||||||
}
|
}
|
||||||
const imageMaxWidth = 100;
|
const imageMaxWidth = 100;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2019-2024 New Vector Ltd.
|
Copyright 2019-2025 New Vector Ltd.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
@@ -22,6 +22,7 @@ import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
|||||||
import SettingsTab from "../SettingsTab";
|
import SettingsTab from "../SettingsTab";
|
||||||
import { SettingsSection } from "../../shared/SettingsSection";
|
import { SettingsSection } from "../../shared/SettingsSection";
|
||||||
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
|
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
|
||||||
|
import { MediaPreviewAccountSettings } from "../user/MediaPreviewAccountSettings";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -92,6 +93,9 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
|||||||
|
|
||||||
<SettingsSection heading={_t("room_settings|general|other_section")}>
|
<SettingsSection heading={_t("room_settings|general|other_section")}>
|
||||||
{urlPreviewSettings}
|
{urlPreviewSettings}
|
||||||
|
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
||||||
|
<MediaPreviewAccountSettings roomId={room.roomId} />
|
||||||
|
</SettingsSubsection>
|
||||||
{leaveSection}
|
{leaveSection}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
|
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
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 React, { type ChangeEventHandler, useCallback } from "react";
|
||||||
|
import { Field, HelpMessage, InlineField, Label, RadioInput, Root } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
|
import { type MediaPreviewConfig, MediaPreviewValue } from "../../../../../@types/media_preview";
|
||||||
|
import { _t } from "../../../../../languageHandler";
|
||||||
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
changeSetting({
|
||||||
|
...currentMediaPreview,
|
||||||
|
// Switch is inverted. "Hide avatars..."
|
||||||
|
invite_avatars: c ? MediaPreviewValue.Off : MediaPreviewValue.On,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[changeSetting, currentMediaPreview],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediaPreviewOnChangeOff = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(event) => {
|
||||||
|
if (!event.target.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changeSetting({
|
||||||
|
...currentMediaPreview,
|
||||||
|
media_previews: MediaPreviewValue.Off,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[changeSetting, currentMediaPreview],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediaPreviewOnChangePrivate = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(event) => {
|
||||||
|
if (!event.target.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changeSetting({
|
||||||
|
...currentMediaPreview,
|
||||||
|
media_previews: MediaPreviewValue.Private,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[changeSetting, currentMediaPreview],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediaPreviewOnChangeOn = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(event) => {
|
||||||
|
if (!event.target.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
changeSetting({
|
||||||
|
...currentMediaPreview,
|
||||||
|
media_previews: MediaPreviewValue.On,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[changeSetting, currentMediaPreview],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root className="mx_MediaPreviewAccountSetting_Form">
|
||||||
|
{!roomId && (
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
className="mx_MediaPreviewAccountSetting_ToggleSwitch"
|
||||||
|
label={_t("settings|media_preview|hide_avatars")}
|
||||||
|
value={currentMediaPreview.invite_avatars === MediaPreviewValue.Off}
|
||||||
|
onChange={avatarOnChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* Explict label here because htmlFor is not supported for linking to radiogroups */}
|
||||||
|
<Field
|
||||||
|
id="mx_media_previews"
|
||||||
|
role="radiogroup"
|
||||||
|
name="media_previews"
|
||||||
|
aria-label={_t("settings|media_preview|media_preview_label")}
|
||||||
|
>
|
||||||
|
<Label>{_t("settings|media_preview|media_preview_label")}</Label>
|
||||||
|
<HelpMessage className="mx_MediaPreviewAccountSetting_RadioHelp">
|
||||||
|
{_t("settings|media_preview|media_preview_description")}
|
||||||
|
</HelpMessage>
|
||||||
|
<InlineField
|
||||||
|
name="media_preview_off"
|
||||||
|
className="mx_MediaPreviewAccountSetting_Radio"
|
||||||
|
control={
|
||||||
|
<RadioInput
|
||||||
|
id="mx_media_previews_off"
|
||||||
|
checked={currentMediaPreview.media_previews === MediaPreviewValue.Off}
|
||||||
|
onChange={mediaPreviewOnChangeOff}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Label htmlFor="mx_media_previews_off">{_t("settings|media_preview|hide_media")}</Label>
|
||||||
|
</InlineField>
|
||||||
|
{!roomId && (
|
||||||
|
<InlineField
|
||||||
|
name="mx_media_previews_private"
|
||||||
|
className="mx_MediaPreviewAccountSetting_Radio"
|
||||||
|
control={
|
||||||
|
<RadioInput
|
||||||
|
id="mx_media_previews_private"
|
||||||
|
checked={currentMediaPreview.media_previews === MediaPreviewValue.Private}
|
||||||
|
onChange={mediaPreviewOnChangePrivate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Label htmlFor="mx_media_previews_private">
|
||||||
|
{_t("settings|media_preview|show_in_private")}
|
||||||
|
</Label>
|
||||||
|
</InlineField>
|
||||||
|
)}
|
||||||
|
<InlineField
|
||||||
|
name="media_preview_on"
|
||||||
|
className="mx_MediaPreviewAccountSetting_Radio"
|
||||||
|
control={
|
||||||
|
<RadioInput
|
||||||
|
id="mx_media_previews_on"
|
||||||
|
checked={currentMediaPreview.media_previews === MediaPreviewValue.On}
|
||||||
|
onChange={mediaPreviewOnChangeOn}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Label htmlFor="mx_media_previews_on">{_t("settings|media_preview|show_media")}</Label>
|
||||||
|
</InlineField>
|
||||||
|
</Field>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
@@ -32,6 +32,7 @@ import SpellCheckSettings from "../../SpellCheckSettings";
|
|||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
||||||
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
|
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
|
||||||
|
import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
closeSettingsFn(success: boolean): void;
|
closeSettingsFn(success: boolean): void;
|
||||||
@@ -116,7 +117,7 @@ const SpellCheckSection: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
|
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs", "showAvatarsOnInvites"];
|
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs"];
|
||||||
|
|
||||||
private static SPACES_SETTINGS: BooleanSettingKey[] = ["Spaces.allRoomsInHome"];
|
private static SPACES_SETTINGS: BooleanSettingKey[] = ["Spaces.allRoomsInHome"];
|
||||||
|
|
||||||
@@ -146,7 +147,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||||||
"urlPreviewsEnabled",
|
"urlPreviewsEnabled",
|
||||||
"autoplayGifs",
|
"autoplayGifs",
|
||||||
"autoplayVideo",
|
"autoplayVideo",
|
||||||
"showImages",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
|
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
|
||||||
@@ -335,6 +335,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||||||
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
|
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
||||||
|
<MediaPreviewAccountSettings />
|
||||||
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|room_directory_heading")}>
|
<SettingsSubsection heading={_t("settings|preferences|room_directory_heading")}>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.ROOM_DIRECTORY_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.ROOM_DIRECTORY_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
@@ -6,30 +6,52 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { SettingLevel } from "../settings/SettingLevel";
|
import { SettingLevel } from "../settings/SettingLevel";
|
||||||
import { useSettingValue } from "./useSettings";
|
import { useSettingValue } from "./useSettings";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
||||||
|
import { MediaPreviewValue } from "../@types/media_preview";
|
||||||
|
import { useRoomState } from "./useRoomState";
|
||||||
|
|
||||||
|
const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRule.Restricted];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should the media event be visible in the client, or hidden.
|
* Should the media event be visible in the client, or hidden.
|
||||||
* @param eventId The eventId of the media event.
|
* @param eventId The eventId of the media event.
|
||||||
* @returns A boolean describing the hidden status, and a function to set the visiblity.
|
* @returns A boolean describing the hidden status, and a function to set the visiblity.
|
||||||
*/
|
*/
|
||||||
export function useMediaVisible(eventId: string): [boolean, (visible: boolean) => void] {
|
export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] {
|
||||||
const defaultShowImages = useSettingValue("showImages", SettingLevel.DEVICE);
|
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId);
|
||||||
const eventVisibility = useSettingValue("showMediaEventIds", SettingLevel.DEVICE);
|
const client = useMatrixClientContext();
|
||||||
|
const eventVisibility = useSettingValue("showMediaEventIds");
|
||||||
|
const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule());
|
||||||
const setMediaVisible = useCallback(
|
const setMediaVisible = useCallback(
|
||||||
(visible: boolean) => {
|
(visible: boolean) => {
|
||||||
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
|
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
|
||||||
...eventVisibility,
|
...eventVisibility,
|
||||||
[eventId]: visible,
|
[eventId!]: visible,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[eventId, eventVisibility],
|
[eventId, eventVisibility],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const roomIsPrivate = joinRule ? PRIVATE_JOIN_RULES.includes(joinRule) : false;
|
||||||
|
|
||||||
|
const explicitEventVisiblity = eventId ? eventVisibility[eventId] : undefined;
|
||||||
// Always prefer the explicit per-event user preference here.
|
// Always prefer the explicit per-event user preference here.
|
||||||
const imgIsVisible = eventVisibility[eventId] ?? defaultShowImages;
|
if (explicitEventVisiblity !== undefined) {
|
||||||
return [imgIsVisible, setMediaVisible];
|
return [explicitEventVisiblity, setMediaVisible];
|
||||||
|
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Off) {
|
||||||
|
return [false, setMediaVisible];
|
||||||
|
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.On) {
|
||||||
|
return [true, setMediaVisible];
|
||||||
|
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Private) {
|
||||||
|
return [roomIsPrivate, setMediaVisible];
|
||||||
|
} else {
|
||||||
|
// Invalid setting.
|
||||||
|
console.warn("Invalid media visibility setting", mediaPreviewSetting.media_previews);
|
||||||
|
return [false, setMediaVisible];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -525,6 +525,7 @@
|
|||||||
"message_timestamp_invalid": "Invalid timestamp",
|
"message_timestamp_invalid": "Invalid timestamp",
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
|
"moderation_and_safety": "Moderation and safety",
|
||||||
"modern": "Modern",
|
"modern": "Modern",
|
||||||
"mute": "Mute",
|
"mute": "Mute",
|
||||||
"n_members": {
|
"n_members": {
|
||||||
@@ -2668,12 +2669,10 @@
|
|||||||
"unable_to_load_msisdns": "Unable to load phone numbers",
|
"unable_to_load_msisdns": "Unable to load phone numbers",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
},
|
},
|
||||||
"image_thumbnails": "Show previews/thumbnails for images",
|
|
||||||
"inline_url_previews_default": "Enable inline URL previews by default",
|
"inline_url_previews_default": "Enable inline URL previews by default",
|
||||||
"inline_url_previews_room": "Enable URL previews by default for participants in this room",
|
"inline_url_previews_room": "Enable URL previews by default for participants in this room",
|
||||||
"inline_url_previews_room_account": "Enable URL previews for this room (only affects you)",
|
"inline_url_previews_room_account": "Enable URL previews for this room (only affects you)",
|
||||||
"insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message",
|
"insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message",
|
||||||
"invite_avatars": "Show avatars of rooms you have been invited to",
|
|
||||||
"jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message",
|
"jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message",
|
||||||
"key_backup": {
|
"key_backup": {
|
||||||
"backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).",
|
"backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).",
|
||||||
@@ -2732,6 +2731,14 @@
|
|||||||
"labs_mjolnir": {
|
"labs_mjolnir": {
|
||||||
"dialog_title": "<strong>Settings:</strong> Ignored Users"
|
"dialog_title": "<strong>Settings:</strong> Ignored Users"
|
||||||
},
|
},
|
||||||
|
"media_preview": {
|
||||||
|
"hide_avatars": "Hide avatars of room and inviter",
|
||||||
|
"hide_media": "Always hide",
|
||||||
|
"media_preview_description": "A hidden media can always be shown by tapping on it",
|
||||||
|
"media_preview_label": "Show media in timeline",
|
||||||
|
"show_in_private": "In private rooms",
|
||||||
|
"show_media": "Always show"
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"default_setting_description": "This setting will be applied by default to all your rooms.",
|
"default_setting_description": "This setting will be applied by default to all your rooms.",
|
||||||
"default_setting_section": "I want to be notified for (Default Setting)",
|
"default_setting_section": "I want to be notified for (Default Setting)",
|
||||||
|
@@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React, { type ReactNode } from "react";
|
import React, { type ReactNode } from "react";
|
||||||
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
|
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { type MediaPreviewConfig } from "../@types/media_preview.ts";
|
||||||
import { _t, _td, type TranslationKey } from "../languageHandler";
|
import { _t, _td, type TranslationKey } from "../languageHandler";
|
||||||
import DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts";
|
import DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts";
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +46,7 @@ import { type Json, type JsonValue } from "../@types/json.ts";
|
|||||||
import { type RecentEmojiData } from "../emojipicker/recent.ts";
|
import { type RecentEmojiData } from "../emojipicker/recent.ts";
|
||||||
import { type Assignable } from "../@types/common.ts";
|
import { type Assignable } from "../@types/common.ts";
|
||||||
import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts";
|
import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts";
|
||||||
|
import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts";
|
||||||
|
|
||||||
export const defaultWatchManager = new WatchManager();
|
export const defaultWatchManager = new WatchManager();
|
||||||
|
|
||||||
@@ -312,8 +314,6 @@ export interface Settings {
|
|||||||
"showHiddenEventsInTimeline": IBaseSetting<boolean>;
|
"showHiddenEventsInTimeline": IBaseSetting<boolean>;
|
||||||
"lowBandwidth": IBaseSetting<boolean>;
|
"lowBandwidth": IBaseSetting<boolean>;
|
||||||
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
|
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
|
||||||
"showImages": IBaseSetting<boolean>;
|
|
||||||
"showAvatarsOnInvites": IBaseSetting<boolean>;
|
|
||||||
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
|
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
|
||||||
"RoomList.showMessagePreview": IBaseSetting<boolean>;
|
"RoomList.showMessagePreview": IBaseSetting<boolean>;
|
||||||
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
|
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
|
||||||
@@ -349,6 +349,7 @@ export interface Settings {
|
|||||||
"Electron.alwaysShowMenuBar": IBaseSetting<boolean>;
|
"Electron.alwaysShowMenuBar": IBaseSetting<boolean>;
|
||||||
"Electron.showTrayIcon": IBaseSetting<boolean>;
|
"Electron.showTrayIcon": IBaseSetting<boolean>;
|
||||||
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
|
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
|
||||||
|
"mediaPreviewConfig": IBaseSetting<MediaPreviewConfig>;
|
||||||
"Developer.elementCallUrl": IBaseSetting<string>;
|
"Developer.elementCallUrl": IBaseSetting<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +428,11 @@ export const SETTINGS: Settings = {
|
|||||||
supportedLevelsAreOrdered: true,
|
supportedLevelsAreOrdered: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"mediaPreviewConfig": {
|
||||||
|
controller: new MediaPreviewConfigController(),
|
||||||
|
supportedLevels: LEVELS_ROOM_SETTINGS,
|
||||||
|
default: MediaPreviewConfigController.default,
|
||||||
|
},
|
||||||
"feature_report_to_moderators": {
|
"feature_report_to_moderators": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Moderation,
|
labsGroup: LabGroup.Moderation,
|
||||||
@@ -1123,16 +1129,6 @@ export const SETTINGS: Settings = {
|
|||||||
default: null,
|
default: null,
|
||||||
controller: new FallbackIceServerController(),
|
controller: new FallbackIceServerController(),
|
||||||
},
|
},
|
||||||
"showImages": {
|
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
|
||||||
displayName: _td("settings|image_thumbnails"),
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
"showAvatarsOnInvites": {
|
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
|
||||||
displayName: _td("settings|invite_avatars"),
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
"RoomList.preferredSorting": {
|
"RoomList.preferredSorting": {
|
||||||
supportedLevels: [SettingLevel.DEVICE],
|
supportedLevels: [SettingLevel.DEVICE],
|
||||||
default: SortingAlgorithm.Recency,
|
default: SortingAlgorithm.Recency,
|
||||||
@@ -1386,7 +1382,6 @@ export const SETTINGS: Settings = {
|
|||||||
displayName: _td("settings|preferences|enable_hardware_acceleration"),
|
displayName: _td("settings|preferences|enable_hardware_acceleration"),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"Developer.elementCallUrl": {
|
"Developer.elementCallUrl": {
|
||||||
supportedLevels: [SettingLevel.DEVICE],
|
supportedLevels: [SettingLevel.DEVICE],
|
||||||
displayName: _td("devtools|settings|elementCallUrl"),
|
displayName: _td("devtools|settings|elementCallUrl"),
|
||||||
|
@@ -38,6 +38,7 @@ import { Action } from "../dispatcher/actions";
|
|||||||
import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler";
|
import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler";
|
||||||
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
|
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
import { MediaPreviewValue } from "../@types/media_preview";
|
||||||
|
|
||||||
// Convert the settings to easier to manage objects for the handlers
|
// Convert the settings to easier to manage objects for the handlers
|
||||||
const defaultSettings: Record<string, any> = {};
|
const defaultSettings: Record<string, any> = {};
|
||||||
@@ -715,6 +716,29 @@ export default class SettingsStore {
|
|||||||
localStorage.setItem(MIGRATION_DONE_FLAG, "true");
|
localStorage.setItem(MIGRATION_DONE_FLAG, "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate the setting for visible images to a setting.
|
||||||
|
*/
|
||||||
|
private static migrateMediaControlsToSetting(): void {
|
||||||
|
const MIGRATION_DONE_FLAG = "mx_migrate_media_controls";
|
||||||
|
if (localStorage.getItem(MIGRATION_DONE_FLAG)) return;
|
||||||
|
|
||||||
|
logger.info("Performing one-time settings migration of show images and invite avatars to account data");
|
||||||
|
const handler = LEVEL_HANDLERS[SettingLevel.ACCOUNT];
|
||||||
|
const showImages = handler.getValue("showImages", null);
|
||||||
|
const showAvatarsOnInvites = handler.getValue("showAvatarsOnInvites", null);
|
||||||
|
|
||||||
|
const AccountHandler = LEVEL_HANDLERS[SettingLevel.ACCOUNT];
|
||||||
|
if (showImages !== null || showAvatarsOnInvites !== null) {
|
||||||
|
AccountHandler.setValue("mediaPreviewConfig", null, {
|
||||||
|
invite_avatars: showAvatarsOnInvites === false ? MediaPreviewValue.Off : MediaPreviewValue.On,
|
||||||
|
media_previews: showImages === false ? MediaPreviewValue.Off : MediaPreviewValue.On,
|
||||||
|
});
|
||||||
|
} // else, we don't set anything and use the server value
|
||||||
|
|
||||||
|
localStorage.setItem(MIGRATION_DONE_FLAG, "true");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs or queues any setting migrations needed.
|
* Runs or queues any setting migrations needed.
|
||||||
*/
|
*/
|
||||||
@@ -732,6 +756,12 @@ export default class SettingsStore {
|
|||||||
// will now be hidden again, so this fails safely.
|
// will now be hidden again, so this fails safely.
|
||||||
SettingsStore.migrateShowImagesToSettings();
|
SettingsStore.migrateShowImagesToSettings();
|
||||||
|
|
||||||
|
// This can be removed once enough users have run a version of Element with
|
||||||
|
// this migration.
|
||||||
|
// The consequences of missing the migration are that the previously set
|
||||||
|
// media controls for this user will be missing
|
||||||
|
SettingsStore.migrateMediaControlsToSetting();
|
||||||
|
|
||||||
// Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and
|
// Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and
|
||||||
// add a comment to note when it can be removed.
|
// add a comment to note when it can be removed.
|
||||||
return;
|
return;
|
||||||
|
@@ -26,7 +26,7 @@ export default abstract class MatrixClientBackedController extends SettingContro
|
|||||||
MatrixClientBackedController._matrixClient = client;
|
MatrixClientBackedController._matrixClient = client;
|
||||||
|
|
||||||
for (const instance of MatrixClientBackedController.instances) {
|
for (const instance of MatrixClientBackedController.instances) {
|
||||||
instance.initMatrixClient(client, oldClient);
|
instance.initMatrixClient?.(client, oldClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,5 +40,5 @@ export default abstract class MatrixClientBackedController extends SettingContro
|
|||||||
return MatrixClientBackedController._matrixClient;
|
return MatrixClientBackedController._matrixClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract initMatrixClient(newClient: MatrixClient, oldClient?: MatrixClient): void;
|
protected initMatrixClient?(newClient: MatrixClient, oldClient?: MatrixClient): void;
|
||||||
}
|
}
|
||||||
|
100
src/settings/controllers/MediaPreviewConfigController.ts
Normal file
100
src/settings/controllers/MediaPreviewConfigController.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
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 { type IContent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { type AccountDataEvents } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MEDIA_PREVIEW_ACCOUNT_DATA_TYPE,
|
||||||
|
type MediaPreviewConfig,
|
||||||
|
MediaPreviewValue,
|
||||||
|
} from "../../@types/media_preview.ts";
|
||||||
|
import { type SettingLevel } from "../SettingLevel.ts";
|
||||||
|
import MatrixClientBackedController from "./MatrixClientBackedController.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles media preview settings provided by MSC4278.
|
||||||
|
* This uses both account-level and room-level account data.
|
||||||
|
*/
|
||||||
|
export default class MediaPreviewConfigController extends MatrixClientBackedController {
|
||||||
|
public static readonly default: AccountDataEvents[typeof MEDIA_PREVIEW_ACCOUNT_DATA_TYPE] = {
|
||||||
|
media_previews: MediaPreviewValue.On,
|
||||||
|
invite_avatars: MediaPreviewValue.On,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static getValidSettingData(content: IContent): Partial<MediaPreviewConfig> {
|
||||||
|
const mediaPreviews: MediaPreviewConfig["media_previews"] = content.media_previews;
|
||||||
|
const inviteAvatars: MediaPreviewConfig["invite_avatars"] = content.invite_avatars;
|
||||||
|
const validMediaPreviews = Object.values(MediaPreviewValue);
|
||||||
|
const validInviteAvatars = [MediaPreviewValue.Off, MediaPreviewValue.On];
|
||||||
|
return {
|
||||||
|
invite_avatars: validInviteAvatars.includes(inviteAvatars) ? inviteAvatars : undefined,
|
||||||
|
media_previews: validMediaPreviews.includes(mediaPreviews) ? mediaPreviews : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getValue = (roomId?: string): MediaPreviewConfig => {
|
||||||
|
const source = roomId ? this.client?.getRoom(roomId) : this.client;
|
||||||
|
const accountData =
|
||||||
|
source?.getAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE)?.getContent<MediaPreviewConfig>() ?? {};
|
||||||
|
|
||||||
|
const calculatedConfig = MediaPreviewConfigController.getValidSettingData(accountData);
|
||||||
|
|
||||||
|
// Save an account data fetch if we have all the values.
|
||||||
|
if (calculatedConfig.invite_avatars && calculatedConfig.media_previews) {
|
||||||
|
return calculatedConfig as MediaPreviewConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're missing some keys.
|
||||||
|
if (roomId) {
|
||||||
|
const globalConfig = this.getValue();
|
||||||
|
return {
|
||||||
|
invite_avatars:
|
||||||
|
calculatedConfig.invite_avatars ??
|
||||||
|
globalConfig.invite_avatars ??
|
||||||
|
MediaPreviewConfigController.default.invite_avatars,
|
||||||
|
media_previews:
|
||||||
|
calculatedConfig.media_previews ??
|
||||||
|
globalConfig.media_previews ??
|
||||||
|
MediaPreviewConfigController.default.media_previews,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
invite_avatars: calculatedConfig.invite_avatars ?? MediaPreviewConfigController.default.invite_avatars,
|
||||||
|
media_previews: calculatedConfig.media_previews ?? MediaPreviewConfigController.default.media_previews,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public getValueOverride(_level: SettingLevel, roomId: string | null): MediaPreviewConfig {
|
||||||
|
return this.getValue(roomId ?? undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get settingDisabled(): false {
|
||||||
|
// No homeserver support is required for this MSC.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async beforeChange(
|
||||||
|
_level: SettingLevel,
|
||||||
|
roomId: string | null,
|
||||||
|
newValue: MediaPreviewConfig,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!this.client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (roomId) {
|
||||||
|
await this.client.setRoomAccountData(roomId, MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await this.client.setAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Travis Ralston
|
Copyright 2017 Travis Ralston
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandl
|
|||||||
import { objectClone, objectKeyChanges } from "../../utils/objects";
|
import { objectClone, objectKeyChanges } from "../../utils/objects";
|
||||||
import { SettingLevel } from "../SettingLevel";
|
import { SettingLevel } from "../SettingLevel";
|
||||||
import { type WatchManager } from "../WatchManager";
|
import { type WatchManager } from "../WatchManager";
|
||||||
|
import { MEDIA_PREVIEW_ACCOUNT_DATA_TYPE } from "../../@types/media_preview";
|
||||||
|
|
||||||
const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms";
|
const BREADCRUMBS_LEGACY_EVENT_TYPE = "im.vector.riot.breadcrumb_rooms";
|
||||||
const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs";
|
const BREADCRUMBS_EVENT_TYPE = "im.vector.setting.breadcrumbs";
|
||||||
@@ -68,6 +69,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
|||||||
} else if (event.getType() === RECENT_EMOJI_EVENT_TYPE) {
|
} else if (event.getType() === RECENT_EMOJI_EVENT_TYPE) {
|
||||||
const val = event.getContent()["enabled"];
|
const val = event.getContent()["enabled"];
|
||||||
this.watchers.notifyUpdate("recent_emoji", null, SettingLevel.ACCOUNT, val);
|
this.watchers.notifyUpdate("recent_emoji", null, SettingLevel.ACCOUNT, val);
|
||||||
|
} else if (event.getType() === MEDIA_PREVIEW_ACCOUNT_DATA_TYPE) {
|
||||||
|
this.watchers.notifyUpdate("mediaPreviewConfig", null, SettingLevel.ROOM_ACCOUNT, event.getContent());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,7 +176,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
|||||||
await deferred.promise;
|
await deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
||||||
switch (settingName) {
|
switch (settingName) {
|
||||||
// Special case URL previews
|
// Special case URL previews
|
||||||
case "urlPreviewsEnabled":
|
case "urlPreviewsEnabled":
|
||||||
@@ -199,7 +202,9 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
|||||||
// Special case analytics
|
// Special case analytics
|
||||||
case "pseudonymousAnalyticsOptIn":
|
case "pseudonymousAnalyticsOptIn":
|
||||||
return this.setAccountData(ANALYTICS_EVENT_TYPE, "pseudonymousAnalyticsOptIn", newValue);
|
return this.setAccountData(ANALYTICS_EVENT_TYPE, "pseudonymousAnalyticsOptIn", newValue);
|
||||||
|
case "mediaPreviewConfig":
|
||||||
|
// Handled in MediaPreviewConfigController.
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return this.setAccountData(DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
return this.setAccountData(DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Travis Ralston
|
Copyright 2017 Travis Ralston
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandl
|
|||||||
import { objectClone, objectKeyChanges } from "../../utils/objects";
|
import { objectClone, objectKeyChanges } from "../../utils/objects";
|
||||||
import { SettingLevel } from "../SettingLevel";
|
import { SettingLevel } from "../SettingLevel";
|
||||||
import { type WatchManager } from "../WatchManager";
|
import { type WatchManager } from "../WatchManager";
|
||||||
|
import { MEDIA_PREVIEW_ACCOUNT_DATA_TYPE } from "../../@types/media_preview";
|
||||||
|
|
||||||
const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets";
|
const ALLOWED_WIDGETS_EVENT_TYPE = "im.vector.setting.allowed_widgets";
|
||||||
const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings";
|
const DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings";
|
||||||
@@ -56,6 +57,8 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
|||||||
}
|
}
|
||||||
} else if (event.getType() === ALLOWED_WIDGETS_EVENT_TYPE) {
|
} else if (event.getType() === ALLOWED_WIDGETS_EVENT_TYPE) {
|
||||||
this.watchers.notifyUpdate("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent());
|
this.watchers.notifyUpdate("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent());
|
||||||
|
} else if (event.getType() === MEDIA_PREVIEW_ACCOUNT_DATA_TYPE) {
|
||||||
|
this.watchers.notifyUpdate("mediaPreviewConfig", roomId, SettingLevel.ROOM_ACCOUNT, event.getContent());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
|||||||
await deferred.promise;
|
await deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
||||||
switch (settingName) {
|
switch (settingName) {
|
||||||
// Special case URL previews
|
// Special case URL previews
|
||||||
case "urlPreviewsEnabled":
|
case "urlPreviewsEnabled":
|
||||||
@@ -117,7 +120,9 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
|||||||
// Special case allowed widgets
|
// Special case allowed widgets
|
||||||
case "allowedWidgets":
|
case "allowedWidgets":
|
||||||
return this.setRoomAccountData(roomId, ALLOWED_WIDGETS_EVENT_TYPE, null, newValue);
|
return this.setRoomAccountData(roomId, ALLOWED_WIDGETS_EVENT_TYPE, null, newValue);
|
||||||
|
case "mediaPreviewConfig":
|
||||||
|
// Handled in MediaPreviewConfigController.
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return this.setRoomAccountData(roomId, DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
return this.setRoomAccountData(roomId, DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import parse from "html-react-parser";
|
|||||||
|
|
||||||
import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils";
|
import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils";
|
||||||
import SettingsStore from "../../src/settings/SettingsStore";
|
import SettingsStore from "../../src/settings/SettingsStore";
|
||||||
|
import { getMockClientWithEventEmitter } from "../test-utils";
|
||||||
import { SettingLevel } from "../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../src/settings/SettingLevel";
|
||||||
import SdkConfig from "../../src/SdkConfig";
|
import SdkConfig from "../../src/SdkConfig";
|
||||||
|
|
||||||
@@ -231,6 +232,37 @@ describe("bodyToNode", () => {
|
|||||||
expect(asFragment()).toMatchSnapshot();
|
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": " 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(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
@@ -64,3 +64,30 @@ exports[`bodyToNode should generate big emoji for an emoji-only reply to a messa
|
|||||||
</span>
|
</span>
|
||||||
</DocumentFragment>
|
</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>
|
||||||
|
`;
|
||||||
|
@@ -17,6 +17,7 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
|||||||
import { LocalRoom } from "../../../../../src/models/LocalRoom";
|
import { LocalRoom } from "../../../../../src/models/LocalRoom";
|
||||||
import * as AvatarModule from "../../../../../src/Avatar";
|
import * as AvatarModule from "../../../../../src/Avatar";
|
||||||
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
|
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
|
||||||
|
import { MediaPreviewValue } from "../../../../../src/@types/media_preview";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
@@ -37,18 +38,18 @@ describe("RoomAvatar", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"mediaPreviewConfig",
|
||||||
|
null,
|
||||||
|
SettingLevel.ACCOUNT,
|
||||||
|
SettingsStore.getDefaultValue("mediaPreviewConfig"),
|
||||||
|
);
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
|
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
|
||||||
mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
|
mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
|
||||||
SettingsStore.setValue(
|
|
||||||
"showAvatarsOnInvites",
|
|
||||||
null,
|
|
||||||
SettingLevel.ACCOUNT,
|
|
||||||
SettingsStore.getDefaultValue("showAvatarsOnInvites"),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render as expected for a Room", () => {
|
it("should render as expected for a Room", () => {
|
||||||
@@ -74,7 +75,6 @@ describe("RoomAvatar", () => {
|
|||||||
expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot();
|
expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("should render an avatar for a room the user is invited to", () => {
|
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());
|
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||||
jest.spyOn(room, "getMxcAvatarUrl").mockImplementation(() => "mxc://example.com/foobar");
|
jest.spyOn(room, "getMxcAvatarUrl").mockImplementation(() => "mxc://example.com/foobar");
|
||||||
room.name = "test room";
|
room.name = "test room";
|
||||||
@@ -93,7 +93,9 @@ describe("RoomAvatar", () => {
|
|||||||
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("should not render an invite avatar if the user has disabled it", () => {
|
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());
|
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||||
room.name = "test room";
|
room.name = "test room";
|
||||||
room.updateMyMembership("invite");
|
room.updateMyMembership("invite");
|
||||||
|
@@ -8,20 +8,20 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { fireEvent, render, screen } from "jest-matrix-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 { HideActionButton } from "../../../../../src/components/views/messages/HideActionButton";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||||
import type { Settings } from "../../../../../src/settings/Settings";
|
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(
|
function mockSetting(mediaPreviews: MediaPreviewValue, showMediaEventIds: Settings["showMediaEventIds"]["default"]) {
|
||||||
showImages: Settings["showImages"]["default"],
|
|
||||||
showMediaEventIds: Settings["showMediaEventIds"]["default"],
|
|
||||||
) {
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
|
||||||
if (settingName === "showImages") {
|
if (settingName === "mediaPreviewConfig") {
|
||||||
return showImages;
|
return { media_previews: mediaPreviews, invite_avatars: MediaPreviewValue.Off };
|
||||||
} else if (settingName === "showMediaEventIds") {
|
} else if (settingName === "showMediaEventIds") {
|
||||||
return showMediaEventIds;
|
return showMediaEventIds;
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,10 @@ function mockSetting(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EVENT_ID = "$foo:bar";
|
||||||
|
|
||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
event_id: "$foo:bar",
|
event_id: EVENT_ID,
|
||||||
room_id: "!room:id",
|
room_id: "!room:id",
|
||||||
sender: "@user:id",
|
sender: "@user:id",
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
@@ -42,32 +44,38 @@ const event = new MatrixEvent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("HideActionButton", () => {
|
describe("HideActionButton", () => {
|
||||||
|
let cli: MockedObject<MatrixClient>;
|
||||||
|
beforeEach(() => {
|
||||||
|
cli = getMockClientWithEventEmitter({
|
||||||
|
getRoom: jest.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
it("should show button when event is visible by showMediaEventIds setting", async () => {
|
it("should show button when event is visible by showMediaEventIds setting", async () => {
|
||||||
mockSetting(false, { "$foo:bar": true });
|
mockSetting(MediaPreviewValue.Off, { [EVENT_ID]: true });
|
||||||
render(<HideActionButton mxEvent={event} />);
|
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
|
||||||
expect(screen.getByRole("button")).toBeVisible();
|
expect(screen.getByRole("button")).toBeVisible();
|
||||||
});
|
});
|
||||||
it("should show button when event is visible by showImages setting", async () => {
|
it("should show button when event is visible by mediaPreviewConfig setting", async () => {
|
||||||
mockSetting(true, {});
|
mockSetting(MediaPreviewValue.On, {});
|
||||||
render(<HideActionButton mxEvent={event} />);
|
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
|
||||||
expect(screen.getByRole("button")).toBeVisible();
|
expect(screen.getByRole("button")).toBeVisible();
|
||||||
});
|
});
|
||||||
it("should hide button when event is hidden by showMediaEventIds setting", async () => {
|
it("should hide button when event is hidden by showMediaEventIds setting", async () => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({ "$foo:bar": false });
|
mockSetting(MediaPreviewValue.Off, { [EVENT_ID]: false });
|
||||||
render(<HideActionButton mxEvent={event} />);
|
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
|
||||||
expect(screen.queryByRole("button")).toBeNull();
|
expect(screen.queryByRole("button")).toBeNull();
|
||||||
});
|
});
|
||||||
it("should hide button when event is hidden by showImages setting", async () => {
|
it("should hide button when event is hidden by showImages setting", async () => {
|
||||||
mockSetting(false, {});
|
mockSetting(MediaPreviewValue.Off, {});
|
||||||
render(<HideActionButton mxEvent={event} />);
|
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
|
||||||
expect(screen.queryByRole("button")).toBeNull();
|
expect(screen.queryByRole("button")).toBeNull();
|
||||||
});
|
});
|
||||||
it("should store event as hidden when clicked", async () => {
|
it("should store event as hidden when clicked", async () => {
|
||||||
const spy = jest.spyOn(SettingsStore, "setValue");
|
const spy = jest.spyOn(SettingsStore, "setValue");
|
||||||
render(<HideActionButton mxEvent={event} />);
|
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
|
||||||
fireEvent.click(screen.getByRole("button"));
|
fireEvent.click(screen.getByRole("button"));
|
||||||
expect(spy).toHaveBeenCalledWith("showMediaEventIds", null, SettingLevel.DEVICE, { "$foo:bar": false });
|
expect(spy).toHaveBeenCalledWith("showMediaEventIds", null, SettingLevel.DEVICE, { "$foo:bar": false });
|
||||||
// Button should be hidden after the setting is set.
|
// 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.
|
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
|
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.
|
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 { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||||
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
@@ -24,10 +24,11 @@ import {
|
|||||||
mockClientMethodsDevice,
|
mockClientMethodsDevice,
|
||||||
mockClientMethodsServer,
|
mockClientMethodsServer,
|
||||||
mockClientMethodsUser,
|
mockClientMethodsUser,
|
||||||
|
withClientContextRenderOptions,
|
||||||
} from "../../../../test-utils";
|
} from "../../../../test-utils";
|
||||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
import { MediaPreviewValue } from "../../../../../src/@types/media_preview";
|
||||||
|
|
||||||
jest.mock("matrix-encrypt-attachment", () => ({
|
jest.mock("matrix-encrypt-attachment", () => ({
|
||||||
decryptAttachment: jest.fn(),
|
decryptAttachment: jest.fn(),
|
||||||
@@ -42,6 +43,7 @@ describe("<MImageBody/>", () => {
|
|||||||
...mockClientMethodsDevice(deviceId),
|
...mockClientMethodsDevice(deviceId),
|
||||||
...mockClientMethodsCrypto(),
|
...mockClientMethodsCrypto(),
|
||||||
getRooms: jest.fn().mockReturnValue([]),
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
|
getRoom: jest.fn(),
|
||||||
getIgnoredUsers: jest.fn(),
|
getIgnoredUsers: jest.fn(),
|
||||||
getVersions: jest.fn().mockResolvedValue({
|
getVersions: jest.fn().mockResolvedValue({
|
||||||
unstable_features: {
|
unstable_features: {
|
||||||
@@ -85,6 +87,7 @@ describe("<MImageBody/>", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
SettingsStore.reset();
|
||||||
mocked(encrypt.decryptAttachment).mockReset();
|
mocked(encrypt.decryptAttachment).mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,6 +100,7 @@ describe("<MImageBody/>", () => {
|
|||||||
mxEvent={encryptedMediaEvent}
|
mxEvent={encryptedMediaEvent}
|
||||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
/>,
|
/>,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
// thumbnail with dimensions present
|
// thumbnail with dimensions present
|
||||||
@@ -112,6 +116,7 @@ describe("<MImageBody/>", () => {
|
|||||||
mxEvent={encryptedMediaEvent}
|
mxEvent={encryptedMediaEvent}
|
||||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
/>,
|
/>,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(url);
|
expect(fetchMock).toHaveBeenCalledWith(url);
|
||||||
@@ -129,6 +134,7 @@ describe("<MImageBody/>", () => {
|
|||||||
mxEvent={encryptedMediaEvent}
|
mxEvent={encryptedMediaEvent}
|
||||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
/>,
|
/>,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
await screen.findByText("Error decrypting image");
|
await screen.findByText("Error decrypting image");
|
||||||
@@ -136,25 +142,12 @@ describe("<MImageBody/>", () => {
|
|||||||
|
|
||||||
describe("with image previews/thumbnails disabled", () => {
|
describe("with image previews/thumbnails disabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
act(() => {
|
const origFn = SettingsStore.getValue;
|
||||||
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, ...args) => {
|
||||||
});
|
if (setting === "mediaPreviewConfig") {
|
||||||
});
|
return { invite_avatars: MediaPreviewValue.Off, media_previews: MediaPreviewValue.Off };
|
||||||
|
}
|
||||||
afterEach(() => {
|
return origFn(setting, ...args);
|
||||||
act(() => {
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"showImages",
|
|
||||||
null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
SettingsStore.getDefaultValue("showImages"),
|
|
||||||
);
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"showMediaEventIds",
|
|
||||||
null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
SettingsStore.getDefaultValue("showMediaEventIds"),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,6 +160,7 @@ describe("<MImageBody/>", () => {
|
|||||||
mxEvent={encryptedMediaEvent}
|
mxEvent={encryptedMediaEvent}
|
||||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
/>,
|
/>,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Show image")).toBeInTheDocument();
|
expect(screen.getByText("Show image")).toBeInTheDocument();
|
||||||
@@ -183,6 +177,7 @@ describe("<MImageBody/>", () => {
|
|||||||
mxEvent={encryptedMediaEvent}
|
mxEvent={encryptedMediaEvent}
|
||||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
/>,
|
/>,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Show image")).toBeInTheDocument();
|
expect(screen.getByText("Show image")).toBeInTheDocument();
|
||||||
@@ -220,6 +215,7 @@ describe("<MImageBody/>", () => {
|
|||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
const img = container.querySelector(".mx_MImageBody_thumbnail")!;
|
const img = container.querySelector(".mx_MImageBody_thumbnail")!;
|
||||||
@@ -273,6 +269,7 @@ describe("<MImageBody/>", () => {
|
|||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for spinners to go away
|
// Wait for spinners to go away
|
||||||
@@ -298,6 +295,7 @@ describe("<MImageBody/>", () => {
|
|||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
const img = container.querySelector(".mx_MImageBody_thumbnail")!;
|
const img = container.querySelector(".mx_MImageBody_thumbnail")!;
|
||||||
|
@@ -19,6 +19,7 @@ import {
|
|||||||
mockClientMethodsDevice,
|
mockClientMethodsDevice,
|
||||||
mockClientMethodsServer,
|
mockClientMethodsServer,
|
||||||
mockClientMethodsUser,
|
mockClientMethodsUser,
|
||||||
|
withClientContextRenderOptions,
|
||||||
} from "../../../../test-utils";
|
} from "../../../../test-utils";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
import MStickerBody from "../../../../../src/components/views/messages/MStickerBody";
|
import MStickerBody from "../../../../../src/components/views/messages/MStickerBody";
|
||||||
@@ -31,6 +32,7 @@ describe("<MStickerBody/>", () => {
|
|||||||
...mockClientMethodsServer(),
|
...mockClientMethodsServer(),
|
||||||
...mockClientMethodsDevice(deviceId),
|
...mockClientMethodsDevice(deviceId),
|
||||||
...mockClientMethodsCrypto(),
|
...mockClientMethodsCrypto(),
|
||||||
|
getRoom: jest.fn(),
|
||||||
getRooms: jest.fn().mockReturnValue([]),
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
getIgnoredUsers: jest.fn(),
|
getIgnoredUsers: jest.fn(),
|
||||||
getVersions: jest.fn().mockResolvedValue({
|
getVersions: jest.fn().mockResolvedValue({
|
||||||
@@ -76,7 +78,7 @@ describe("<MStickerBody/>", () => {
|
|||||||
it("should show a tooltip on hover", async () => {
|
it("should show a tooltip on hover", async () => {
|
||||||
fetchMock.getOnce(url, { status: 200 });
|
fetchMock.getOnce(url, { status: 200 });
|
||||||
|
|
||||||
render(<MStickerBody {...props} mxEvent={mediaEvent} />);
|
render(<MStickerBody {...props} mxEvent={mediaEvent} />, withClientContextRenderOptions(cli));
|
||||||
|
|
||||||
expect(screen.queryByRole("tooltip")).toBeNull();
|
expect(screen.queryByRole("tooltip")).toBeNull();
|
||||||
await userEvent.hover(screen.getByRole("img"));
|
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.
|
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
|
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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { act } from "react";
|
import React from "react";
|
||||||
import { EventType, getHttpUriForMxc, type IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
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 { fireEvent, render, screen, type RenderResult } from "jest-matrix-react";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
import { type MockedObject } from "jest-mock";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||||
import { type RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
import { type RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||||
@@ -20,11 +21,12 @@ import {
|
|||||||
mockClientMethodsDevice,
|
mockClientMethodsDevice,
|
||||||
mockClientMethodsServer,
|
mockClientMethodsServer,
|
||||||
mockClientMethodsUser,
|
mockClientMethodsUser,
|
||||||
|
withClientContextRenderOptions,
|
||||||
} from "../../../../test-utils";
|
} from "../../../../test-utils";
|
||||||
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
|
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
|
||||||
import type { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps";
|
import type { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps";
|
||||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
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.
|
// Needed so we don't throw an error about failing to decrypt.
|
||||||
jest.mock("matrix-encrypt-attachment", () => ({
|
jest.mock("matrix-encrypt-attachment", () => ({
|
||||||
@@ -36,13 +38,15 @@ describe("MVideoBody", () => {
|
|||||||
const deviceId = "DEADB33F";
|
const deviceId = "DEADB33F";
|
||||||
|
|
||||||
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
||||||
|
let cli: MockedObject<MatrixClient>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const cli = getMockClientWithEventEmitter({
|
cli = getMockClientWithEventEmitter({
|
||||||
...mockClientMethodsUser(userId),
|
...mockClientMethodsUser(userId),
|
||||||
...mockClientMethodsServer(),
|
...mockClientMethodsServer(),
|
||||||
...mockClientMethodsDevice(deviceId),
|
...mockClientMethodsDevice(deviceId),
|
||||||
...mockClientMethodsCrypto(),
|
...mockClientMethodsCrypto(),
|
||||||
|
getRoom: jest.fn(),
|
||||||
getRooms: jest.fn().mockReturnValue([]),
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
getIgnoredUsers: jest.fn(),
|
getIgnoredUsers: jest.fn(),
|
||||||
getVersions: jest.fn().mockResolvedValue({
|
getVersions: jest.fn().mockResolvedValue({
|
||||||
@@ -65,6 +69,7 @@ describe("MVideoBody", () => {
|
|||||||
room_id: "!room:server",
|
room_id: "!room:server",
|
||||||
sender: userId,
|
sender: userId,
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
|
event_id: "$foo:bar",
|
||||||
content: {
|
content: {
|
||||||
body: "alt for a test video",
|
body: "alt for a test video",
|
||||||
info: {
|
info: {
|
||||||
@@ -93,32 +98,25 @@ describe("MVideoBody", () => {
|
|||||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
|
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with video previews/thumbnails disabled", () => {
|
describe("with video previews/thumbnails disabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
act(() => {
|
const origFn = SettingsStore.getValue;
|
||||||
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, ...args) => {
|
||||||
|
if (setting === "mediaPreviewConfig") {
|
||||||
|
return { invite_avatars: MediaPreviewValue.Off, media_previews: MediaPreviewValue.Off };
|
||||||
|
}
|
||||||
|
return origFn(setting, ...args);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
act(() => {
|
SettingsStore.reset();
|
||||||
SettingsStore.setValue(
|
jest.restoreAllMocks();
|
||||||
"showImages",
|
|
||||||
null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
SettingsStore.getDefaultValue("showImages"),
|
|
||||||
);
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"showMediaEventIds",
|
|
||||||
null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
SettingsStore.getDefaultValue("showMediaEventIds"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not download video", async () => {
|
it("should not download video", async () => {
|
||||||
@@ -129,6 +127,7 @@ describe("MVideoBody", () => {
|
|||||||
mxEvent={encryptedMediaEvent}
|
mxEvent={encryptedMediaEvent}
|
||||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
/>,
|
/>,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Show video")).toBeInTheDocument();
|
expect(screen.getByText("Show video")).toBeInTheDocument();
|
||||||
@@ -144,6 +143,7 @@ describe("MVideoBody", () => {
|
|||||||
mxEvent={encryptedMediaEvent}
|
mxEvent={encryptedMediaEvent}
|
||||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||||
/>,
|
/>,
|
||||||
|
withClientContextRenderOptions(cli),
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholderButton = screen.getByRole("button", { name: "Show video" });
|
const placeholderButton = screen.getByRole("button", { name: "Show video" });
|
||||||
@@ -191,6 +191,7 @@ function makeMVideoBody(w: number, h: number): RenderResult {
|
|||||||
|
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
mxcUrlToHttp: jest.fn(),
|
mxcUrlToHttp: jest.fn(),
|
||||||
|
getRoom: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
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
|
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]);
|
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||||
|
|
||||||
enabledFeatures = [];
|
enabledFeatures = [];
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any =>
|
const origFn = SettingsStore.getValue;
|
||||||
enabledFeatures.includes(settingName) ? true : undefined,
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => {
|
||||||
);
|
if (enabledFeatures.includes(settingName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return origFn(settingName);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
@@ -95,33 +95,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -144,7 +117,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_6hpi3YEetmBG"
|
for="mx_SettingsFlag_QgU2PomxwKpa"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -162,7 +135,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Show all rooms in Home"
|
aria-label="Show all rooms in Home"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_6hpi3YEetmBG"
|
id="mx_SettingsFlag_QgU2PomxwKpa"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -212,7 +185,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_4yVCeEefiPqp"
|
for="mx_SettingsFlag_6hpi3YEetmBG"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -225,7 +198,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Use Ctrl + F to search timeline"
|
aria-label="Use Ctrl + F to search timeline"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_4yVCeEefiPqp"
|
id="mx_SettingsFlag_6hpi3YEetmBG"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -285,7 +258,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_MRMwbPDmfGtm"
|
for="mx_SettingsFlag_4yVCeEefiPqp"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -298,6 +271,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Show timestamps in 12 hour format (e.g. 2:30pm)"
|
aria-label="Show timestamps in 12 hour format (e.g. 2:30pm)"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
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"
|
id="mx_SettingsFlag_MRMwbPDmfGtm"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -313,33 +313,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_GQvdMWe954DV"
|
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
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -352,7 +325,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Publish timezone on public profile"
|
aria-label="Publish timezone on public profile"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_IAu5CsiHRD7n"
|
id="mx_SettingsFlag_GQvdMWe954DV"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -392,7 +365,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_yrA2ohjWVJIP"
|
for="mx_SettingsFlag_IAu5CsiHRD7n"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -405,7 +378,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Send read receipts"
|
aria-label="Send read receipts"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_yrA2ohjWVJIP"
|
id="mx_SettingsFlag_IAu5CsiHRD7n"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -419,7 +392,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_auy1OmnTidX4"
|
for="mx_SettingsFlag_yrA2ohjWVJIP"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -432,7 +405,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Send typing notifications"
|
aria-label="Send typing notifications"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_auy1OmnTidX4"
|
id="mx_SettingsFlag_yrA2ohjWVJIP"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -463,7 +436,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_ePDS0OpWwAHG"
|
for="mx_SettingsFlag_auy1OmnTidX4"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -476,7 +449,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Automatically replace plain text Emoji"
|
aria-label="Automatically replace plain text Emoji"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_ePDS0OpWwAHG"
|
id="mx_SettingsFlag_auy1OmnTidX4"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -490,7 +463,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_75JNTNkNU64r"
|
for="mx_SettingsFlag_ePDS0OpWwAHG"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -514,6 +487,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Enable Markdown"
|
aria-label="Enable Markdown"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
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"
|
id="mx_SettingsFlag_75JNTNkNU64r"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -533,14 +533,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Enable Emoji suggestions while typing
|
Use Ctrl + Enter to send a message
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="false"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Enable Emoji suggestions while typing"
|
aria-label="Use Ctrl + Enter to send a message"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_aTLcRsQRlYy7"
|
id="mx_SettingsFlag_aTLcRsQRlYy7"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -560,13 +560,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Use Ctrl + Enter to send a message
|
Surround selected text when typing special characters
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="false"
|
aria-checked="false"
|
||||||
aria-disabled="true"
|
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"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_5nfv5bOEPN1s"
|
id="mx_SettingsFlag_5nfv5bOEPN1s"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -587,14 +587,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Surround selected text when typing special characters
|
Show stickers button
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="false"
|
aria-checked="true"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Surround selected text when typing special characters"
|
aria-label="Show stickers button"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_u1JYVtOyR5kb"
|
id="mx_SettingsFlag_u1JYVtOyR5kb"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -610,33 +610,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_u3pEwuLn9Enn"
|
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
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -649,7 +622,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Insert a trailing colon after user mentions at the start of a message"
|
aria-label="Insert a trailing colon after user mentions at the start of a message"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_YuxfFEpOsztW"
|
id="mx_SettingsFlag_u3pEwuLn9Enn"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -680,7 +653,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_hQkBerF1ejc4"
|
for="mx_SettingsFlag_YuxfFEpOsztW"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -693,6 +666,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Enable automatic language detection for syntax highlighting"
|
aria-label="Enable automatic language detection for syntax highlighting"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
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"
|
id="mx_SettingsFlag_hQkBerF1ejc4"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -708,33 +708,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_GFes1UFzOK2n"
|
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
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -747,7 +720,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Show line numbers in code blocks"
|
aria-label="Show line numbers in code blocks"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_vfGFMldL2r2v"
|
id="mx_SettingsFlag_GFes1UFzOK2n"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -778,7 +751,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_bsSwicmKUiOB"
|
for="mx_SettingsFlag_vfGFMldL2r2v"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -791,6 +764,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Enable inline URL previews by default"
|
aria-label="Enable inline URL previews by default"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
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"
|
id="mx_SettingsFlag_bsSwicmKUiOB"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -806,33 +806,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_dvqsxEaZtl3A"
|
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
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -845,34 +818,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Autoplay videos"
|
aria-label="Autoplay videos"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_NIiWzqsApP1c"
|
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_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"
|
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -903,7 +849,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_dXFDGgBsKXay"
|
for="mx_SettingsFlag_NIiWzqsApP1c"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -916,6 +862,60 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Show typing notifications"
|
aria-label="Show typing notifications"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
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"
|
id="mx_SettingsFlag_dXFDGgBsKXay"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -935,13 +935,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Show a placeholder for removed messages
|
Show join/leave messages (invites/removes/bans unaffected)
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled="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"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_7Az0xw4Bs4Tt"
|
id="mx_SettingsFlag_7Az0xw4Bs4Tt"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -962,13 +962,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Show read receipts sent by other users
|
Show display name changes
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled="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"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_8jmzPIlPoBCv"
|
id="mx_SettingsFlag_8jmzPIlPoBCv"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -989,13 +989,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Show join/leave messages (invites/removes/bans unaffected)
|
Show chat effects (animations when receiving e.g. confetti)
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled="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"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_enFRaTjdsFou"
|
id="mx_SettingsFlag_enFRaTjdsFou"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -1016,13 +1016,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Show display name changes
|
Show profile picture changes
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Show display name changes"
|
aria-label="Show profile picture changes"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_bfwnd5rz4XNX"
|
id="mx_SettingsFlag_bfwnd5rz4XNX"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -1043,13 +1043,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Show chat effects (animations when receiving e.g. confetti)
|
Show avatars in user, room and event mentions
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled="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"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_gs5uWEzYzZrS"
|
id="mx_SettingsFlag_gs5uWEzYzZrS"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -1070,13 +1070,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Show profile picture changes
|
Enable big emoji in chat
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled="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"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_qWg7OgID1yRR"
|
id="mx_SettingsFlag_qWg7OgID1yRR"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -1097,13 +1097,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
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>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled="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"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_pOPewl7rtMbV"
|
id="mx_SettingsFlag_pOPewl7rtMbV"
|
||||||
role="switch"
|
role="switch"
|
||||||
@@ -1124,14 +1124,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
>
|
>
|
||||||
Enable big emoji in chat
|
Show current profile picture and name for users in message history
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
aria-checked="true"
|
aria-checked="false"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Enable big emoji in chat"
|
aria-label="Show current profile picture and name for users in message history"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_cmt3PZSyNp3v"
|
id="mx_SettingsFlag_cmt3PZSyNp3v"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -1141,62 +1141,170 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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
|
<div
|
||||||
class="mx_SettingsSubsection"
|
class="mx_SettingsSubsection"
|
||||||
>
|
>
|
||||||
@@ -1217,7 +1325,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_FLEpLCb0jpp6"
|
for="mx_SettingsFlag_dJJz3lHUv9XX"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -1230,7 +1338,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Show NSFW content"
|
aria-label="Show NSFW content"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch"
|
class="mx_AccessibleButton mx_ToggleSwitch"
|
||||||
id="mx_SettingsFlag_FLEpLCb0jpp6"
|
id="mx_SettingsFlag_dJJz3lHUv9XX"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@@ -1261,7 +1369,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mx_SettingsFlag_label"
|
class="mx_SettingsFlag_label"
|
||||||
for="mx_SettingsFlag_NQFWldEwbV3q"
|
for="mx_SettingsFlag_SBSSOZDRlzlA"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_SettingsFlag_labelText"
|
class="mx_SettingsFlag_labelText"
|
||||||
@@ -1274,7 +1382,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Prompt before sending invites to potentially invalid matrix IDs"
|
aria-label="Prompt before sending invites to potentially invalid matrix IDs"
|
||||||
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
id="mx_SettingsFlag_NQFWldEwbV3q"
|
id="mx_SettingsFlag_SBSSOZDRlzlA"
|
||||||
role="switch"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
@@ -6,48 +6,74 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { act, renderHook, waitFor } from "jest-matrix-react";
|
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 { 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 SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
||||||
|
|
||||||
const EVENT_ID = "$fibble:example.org";
|
const EVENT_ID = "$fibble:example.org";
|
||||||
|
const ROOM_ID = "!foobar:example.org";
|
||||||
function render() {
|
|
||||||
return renderHook(() => useMediaVisible(EVENT_ID));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("useMediaVisible", () => {
|
describe("useMediaVisible", () => {
|
||||||
afterEach(() => {
|
let matrixClient: MatrixClient;
|
||||||
// Using act here as otherwise React warns about state updates not being wrapped.
|
let room: Room;
|
||||||
act(() => {
|
const mediaPreviewConfig: MediaPreviewConfig = MediaPreviewConfigController.default;
|
||||||
SettingsStore.setValue(
|
|
||||||
"showMediaEventIds",
|
function render() {
|
||||||
null,
|
return renderHook(() => useMediaVisible(EVENT_ID, ROOM_ID), withClientContextRenderOptions(matrixClient));
|
||||||
SettingLevel.DEVICE,
|
}
|
||||||
SettingsStore.getDefaultValue("showMediaEventIds"),
|
beforeEach(() => {
|
||||||
);
|
matrixClient = createTestClient();
|
||||||
SettingsStore.setValue(
|
room = mkStubRoom(ROOM_ID, undefined, matrixClient);
|
||||||
"showImages",
|
matrixClient.getRoom = jest.fn().mockReturnValue(room);
|
||||||
null,
|
const origFn = SettingsStore.getValue;
|
||||||
SettingLevel.DEVICE,
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, ...args) => {
|
||||||
SettingsStore.getDefaultValue("showImages"),
|
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();
|
const { result } = render();
|
||||||
expect(result.current[0]).toEqual(true);
|
expect(result.current[0]).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should hide images when the default is changed", async () => {
|
it("should hide media when media previews are Off", async () => {
|
||||||
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
|
||||||
const { result } = render();
|
const { result } = render();
|
||||||
expect(result.current[0]).toEqual(false);
|
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();
|
const { result } = render();
|
||||||
expect(result.current[0]).toEqual(true);
|
expect(result.current[0]).toEqual(true);
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -57,8 +83,8 @@ describe("useMediaVisible", () => {
|
|||||||
expect(result.current[0]).toEqual(false);
|
expect(result.current[0]).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("should show images after function is called", async () => {
|
it("should show media after function is called", async () => {
|
||||||
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
mediaPreviewConfig.media_previews = MediaPreviewValue.Off;
|
||||||
const { result } = render();
|
const { result } = render();
|
||||||
expect(result.current[0]).toEqual(false);
|
expect(result.current[0]).toEqual(false);
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
Reference in New Issue
Block a user