Compare commits

..

2 Commits

Author SHA1 Message Date
RiotRobot
55c4b2fac0 v1.11.98-rc.0 2025-04-15 13:29:49 +00:00
RiotRobot
84479a86f3 Upgrade dependency to matrix-js-sdk@37.4.0-rc.0 2025-04-15 13:22:58 +00:00
44 changed files with 420 additions and 1379 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.97",
"version": "1.11.98-rc.0",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -130,7 +130,7 @@
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-js-sdk": "37.4.0-rc.0",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",

View File

@@ -1,122 +0,0 @@
/*
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: 244 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -14,7 +14,6 @@ import type { EncryptedFile } from "matrix-js-sdk/src/types";
import type { EmptyObject } from "matrix-js-sdk/src/matrix";
import type { DeviceClientInformation } from "../utils/device/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
declare module "matrix-js-sdk/src/types" {
@@ -88,8 +87,6 @@ declare module "matrix-js-sdk/src/types" {
"m.accepted_terms": {
accepted: string[];
};
"io.element.msc4278.media_preview_config": MediaPreviewConfig;
}
export interface AudioContent {

View File

@@ -1,33 +0,0 @@
/*
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;
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2017, 2018 New Vector Ltd
@@ -294,10 +294,6 @@ export interface EventRenderOpts {
disableBigEmoji?: boolean;
stripReplyFallback?: boolean;
forComposerQuote?: boolean;
/**
* Should inline media be rendered?
*/
mediaIsVisible?: boolean;
}
function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: EventRenderOpts = {}): EventAnalysis {
@@ -306,16 +302,6 @@ function analyseEvent(content: IContent, highlights: Optional<string[]>, opts: E
sanitizeParams = composerSanitizeHtmlParams;
}
if (opts.mediaIsVisible === false && sanitizeParams.transformTags?.["img"]) {
// Prevent mutating the source of sanitizeParams.
sanitizeParams.transformTags = {
...sanitizeParams.transformTags,
img: (tagName) => {
return { tagName, attribs: {} };
},
};
}
try {
const isFormattedBody =
content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string";

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -12,6 +12,7 @@ import { merge } from "lodash";
import _Linkify from "linkify-react";
import { _linkifyString, ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from "./linkify-matrix";
import SettingsStore from "./settings/SettingsStore";
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { mediaFromMxc } from "./customisations/Media";
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
@@ -46,7 +47,10 @@ export const transformTags: NonNullable<IOptions["transformTags"]> = {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s.
if (!src) {
// We also drop inline images (as if they were not present at all) when the "show
// 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: {} };
}
@@ -74,6 +78,7 @@ export const transformTags: NonNullable<IOptions["transformTags"]> = {
if (requestedHeight) {
attribs.style += "height: 100%;";
}
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!;
return { tagName, attribs };
},

View File

@@ -20,7 +20,6 @@ import { filterBoolean } from "../../../utils/arrays";
import { useSettingValue } from "../../../hooks/useSettings";
import { useRoomState } from "../../../hooks/useRoomState";
import { useRoomIdName } from "../../../hooks/room/useRoomIdName";
import { MediaPreviewValue } from "../../../@types/media_preview";
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick" | "size"> {
// Room may be left unset here, but if it is,
@@ -41,8 +40,7 @@ const RoomAvatar: React.FC<IProps> = ({ room, viewAvatarOnClick, onClick, oobDat
const avatarEvent = useRoomState(room, (state) => state.getStateEvents(EventType.RoomAvatar, ""));
const roomIdName = useRoomIdName(room, oobData);
const showAvatarsOnInvites =
useSettingValue("mediaPreviewConfig", room?.roomId).invite_avatars === MediaPreviewValue.On;
const showAvatarsOnInvites = useSettingValue("showAvatarsOnInvites", room?.roomId);
const onRoomAvatarClick = useCallback(() => {
const avatarUrl = Avatar.avatarUrlForRoom(room ?? null);
@@ -65,6 +63,7 @@ const RoomAvatar: React.FC<IProps> = ({ room, viewAvatarOnClick, onClick, oobDat
// parseInt ignores suffixes.
const sizeInt = parseInt(size, 10);
let oobAvatar: string | null = null;
if (oobData?.avatarUrl) {
oobAvatar = mediaFromMxc(oobData?.avatarUrl).getThumbnailOfSourceHttp(sizeInt, sizeInt, "crop");
}

View File

@@ -28,7 +28,6 @@ import {
import MatrixClientContext from "../../../contexts/MatrixClientContext.tsx";
import { useSettingValue } from "../../../hooks/useSettings.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
@@ -151,7 +150,6 @@ const EventContentBody = memo(
forwardRef<HTMLElement, Props>(
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ...options }, ref) => {
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
const replacer = useReplacer(content, mxEvent, options);
const linkifyOptions = useMemo(
@@ -169,9 +167,8 @@ const EventContentBody = memo(
disableBigEmoji: isEmote || !enableBigEmoji,
// Part of Replies fallback support
stripReplyFallback: stripReply,
mediaIsVisible,
}),
[content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply],
[content, enableBigEmoji, highlights, isEmote, stripReply],
);
if (as === "div") includeDir = true; // force dir="auto" on divs

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
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
@@ -25,7 +25,7 @@ interface IProps {
* Quick action button for marking a media event as hidden.
*/
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId()!);
if (!mediaIsVisible) {
return;

View File

@@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
// Wrap MImageBody component so we can use a hook here.
const MImageBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
}
}
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
}
const MStickerBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {
// Wrap MVideoBody component so we can use a hook here.
const MVideoBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
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
@@ -17,7 +17,6 @@ import AccessibleButton from "../elements/AccessibleButton";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import { useMediaVisible } from "../../../hooks/useMediaVisible";
const INITIAL_NUM_PREVIEWS = 2;
@@ -30,7 +29,6 @@ interface IProps {
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
const cli = useContext(MatrixClientContext);
const [expanded, toggleExpanded] = useStateToggle();
const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
@@ -57,13 +55,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) =
return (
<div className="mx_LinkPreviewGroup">
{showPreviews.map(([link, preview], i) => (
<LinkPreviewWidget
mediaVisible={mediaVisible}
key={link}
link={link}
preview={preview}
mxEvent={mxEvent}
>
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
{i === 0 ? (
<AccessibleButton
className="mx_LinkPreviewGroup_hide"

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
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
@@ -11,6 +11,7 @@ import { decode } from "html-entities";
import { type MatrixEvent, type IPreviewUrlResponse } from "matrix-js-sdk/src/matrix";
import { Linkify } from "../../../HtmlUtils";
import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils";
import { mediaFromMxc } from "../../../customisations/Media";
@@ -23,7 +24,6 @@ interface IProps {
preview: IPreviewUrlResponse;
mxEvent: MatrixEvent; // the Event associated with the preview
children?: ReactNode;
mediaVisible: boolean;
}
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?
let image: string | null = p["og:image"] ?? null;
if (!this.props.mediaVisible) {
if (!SettingsStore.getValue("showImages")) {
image = null; // Don't render a button to show the image, just hide it outright
}
const imageMaxWidth = 100;

View File

@@ -1,5 +1,5 @@
/*
Copyright 2019-2025 New Vector Ltd.
Copyright 2019-2024 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.
@@ -22,7 +22,6 @@ import { SettingsSubsection } from "../../shared/SettingsSubsection";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
import { MediaPreviewAccountSettings } from "../user/MediaPreviewAccountSettings";
interface IProps {
room: Room;
@@ -93,9 +92,6 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
<SettingsSection heading={_t("room_settings|general|other_section")}>
{urlPreviewSettings}
<SettingsSubsection heading={_t("common|moderation_and_safety")}>
<MediaPreviewAccountSettings roomId={room.roomId} />
</SettingsSubsection>
{leaveSection}
</SettingsSection>
</SettingsTab>

View File

@@ -1,144 +0,0 @@
/*
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>
{!roomId && (
<LabelledToggleSwitch
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>{_t("settings|media_preview|media_preview_description")}</HelpMessage>
<InlineField
name="media_preview_off"
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"
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"
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>
);
};

View File

@@ -32,7 +32,6 @@ import SpellCheckSettings from "../../SpellCheckSettings";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import * as TimezoneHandler from "../../../../../TimezoneHandler";
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx";
interface IProps {
closeSettingsFn(success: boolean): void;
@@ -117,7 +116,7 @@ const SpellCheckSection: React.FC = () => {
};
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs"];
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs", "showAvatarsOnInvites"];
private static SPACES_SETTINGS: BooleanSettingKey[] = ["Spaces.allRoomsInHome"];
@@ -147,6 +146,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
"urlPreviewsEnabled",
"autoplayGifs",
"autoplayVideo",
"showImages",
];
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
@@ -335,10 +335,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
</SettingsSubsection>
<SettingsSubsection heading={_t("common|moderation_and_safety")}>
<MediaPreviewAccountSettings />
</SettingsSubsection>
<SettingsSubsection heading={_t("settings|preferences|room_directory_heading")}>
{this.renderGroup(PreferencesUserSettingsTab.ROOM_DIRECTORY_SETTINGS)}
</SettingsSubsection>

View File

@@ -6,52 +6,30 @@ Please see LICENSE files in the repository root for full details.
*/
import { useCallback } from "react";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../settings/SettingLevel";
import { useSettingValue } from "./useSettings";
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.
* @param eventId The eventId of the media event.
* @returns A boolean describing the hidden status, and a function to set the visiblity.
*/
export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] {
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId);
const client = useMatrixClientContext();
const eventVisibility = useSettingValue("showMediaEventIds");
const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule());
export function useMediaVisible(eventId: string): [boolean, (visible: boolean) => void] {
const defaultShowImages = useSettingValue("showImages", SettingLevel.DEVICE);
const eventVisibility = useSettingValue("showMediaEventIds", SettingLevel.DEVICE);
const setMediaVisible = useCallback(
(visible: boolean) => {
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
...eventVisibility,
[eventId!]: visible,
[eventId]: visible,
});
},
[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.
if (explicitEventVisiblity !== undefined) {
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];
}
const imgIsVisible = eventVisibility[eventId] ?? defaultShowImages;
return [imgIsVisible, setMediaVisible];
}

View File

@@ -525,7 +525,6 @@
"message_timestamp_invalid": "Invalid timestamp",
"microphone": "Microphone",
"model": "Model",
"moderation_and_safety": "Moderation and safety",
"modern": "Modern",
"mute": "Mute",
"n_members": {
@@ -2669,10 +2668,12 @@
"unable_to_load_msisdns": "Unable to load phone numbers",
"username": "Username"
},
"image_thumbnails": "Show previews/thumbnails for images",
"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_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",
"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",
"key_backup": {
"backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).",
@@ -2731,14 +2732,6 @@
"labs_mjolnir": {
"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": {
"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)",

View File

@@ -10,7 +10,6 @@ Please see LICENSE files in the repository root for full details.
import React, { type ReactNode } from "react";
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 DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts";
import {
@@ -46,7 +45,6 @@ import { type Json, type JsonValue } from "../@types/json.ts";
import { type RecentEmojiData } from "../emojipicker/recent.ts";
import { type Assignable } from "../@types/common.ts";
import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts";
import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts";
export const defaultWatchManager = new WatchManager();
@@ -314,6 +312,8 @@ export interface Settings {
"showHiddenEventsInTimeline": IBaseSetting<boolean>;
"lowBandwidth": IBaseSetting<boolean>;
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
"showImages": IBaseSetting<boolean>;
"showAvatarsOnInvites": IBaseSetting<boolean>;
"RoomList.preferredSorting": IBaseSetting<SortingAlgorithm>;
"RoomList.showMessagePreview": IBaseSetting<boolean>;
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
@@ -349,7 +349,6 @@ export interface Settings {
"Electron.alwaysShowMenuBar": IBaseSetting<boolean>;
"Electron.showTrayIcon": IBaseSetting<boolean>;
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
"mediaPreviewConfig": IBaseSetting<MediaPreviewConfig>;
"Developer.elementCallUrl": IBaseSetting<string>;
}
@@ -428,11 +427,6 @@ export const SETTINGS: Settings = {
supportedLevelsAreOrdered: true,
default: false,
},
"mediaPreviewConfig": {
controller: new MediaPreviewConfigController(),
supportedLevels: LEVELS_ROOM_SETTINGS,
default: MediaPreviewConfigController.default,
},
"feature_report_to_moderators": {
isFeature: true,
labsGroup: LabGroup.Moderation,
@@ -1129,6 +1123,16 @@ export const SETTINGS: Settings = {
default: null,
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": {
supportedLevels: [SettingLevel.DEVICE],
default: SortingAlgorithm.Recency,
@@ -1382,6 +1386,7 @@ export const SETTINGS: Settings = {
displayName: _td("settings|preferences|enable_hardware_acceleration"),
default: true,
},
"Developer.elementCallUrl": {
supportedLevels: [SettingLevel.DEVICE],
displayName: _td("devtools|settings|elementCallUrl"),

View File

@@ -38,7 +38,6 @@ import { Action } from "../dispatcher/actions";
import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler";
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { MediaPreviewValue } from "../@types/media_preview";
// Convert the settings to easier to manage objects for the handlers
const defaultSettings: Record<string, any> = {};
@@ -716,29 +715,6 @@ export default class SettingsStore {
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.
*/
@@ -756,12 +732,6 @@ export default class SettingsStore {
// will now be hidden again, so this fails safely.
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
// add a comment to note when it can be removed.
return;

View File

@@ -26,7 +26,7 @@ export default abstract class MatrixClientBackedController extends SettingContro
MatrixClientBackedController._matrixClient = client;
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;
}
protected initMatrixClient?(newClient: MatrixClient, oldClient?: MatrixClient): void;
protected abstract initMatrixClient(newClient: MatrixClient, oldClient?: MatrixClient): void;
}

View File

@@ -1,100 +0,0 @@
/*
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;
}
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 Travis Ralston
@@ -15,7 +15,6 @@ import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandl
import { objectClone, objectKeyChanges } from "../../utils/objects";
import { SettingLevel } from "../SettingLevel";
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_EVENT_TYPE = "im.vector.setting.breadcrumbs";
@@ -69,8 +68,6 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
} else if (event.getType() === RECENT_EMOJI_EVENT_TYPE) {
const val = event.getContent()["enabled"];
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());
}
};
@@ -176,7 +173,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
await deferred.promise;
}
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
switch (settingName) {
// Special case URL previews
case "urlPreviewsEnabled":
@@ -202,9 +199,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
// Special case analytics
case "pseudonymousAnalyticsOptIn":
return this.setAccountData(ANALYTICS_EVENT_TYPE, "pseudonymousAnalyticsOptIn", newValue);
case "mediaPreviewConfig":
// Handled in MediaPreviewConfigController.
return;
default:
return this.setAccountData(DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 Travis Ralston
@@ -14,7 +14,6 @@ import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandl
import { objectClone, objectKeyChanges } from "../../utils/objects";
import { SettingLevel } from "../SettingLevel";
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 DEFAULT_SETTINGS_EVENT_TYPE = "im.vector.web.settings";
@@ -57,8 +56,6 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
}
} else if (event.getType() === ALLOWED_WIDGETS_EVENT_TYPE) {
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());
}
};
@@ -111,7 +108,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
await deferred.promise;
}
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
switch (settingName) {
// Special case URL previews
case "urlPreviewsEnabled":
@@ -120,9 +117,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
// Special case allowed widgets
case "allowedWidgets":
return this.setRoomAccountData(roomId, ALLOWED_WIDGETS_EVENT_TYPE, null, newValue);
case "mediaPreviewConfig":
// Handled in MediaPreviewConfigController.
return;
default:
return this.setRoomAccountData(roomId, DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,164 +0,0 @@
/*
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);
},
);
});

View File

@@ -3738,7 +3738,7 @@
classnames "^2.5.1"
vaul "^1.0.0"
"@vector-im/matrix-wysiwyg-wasm@link:../../../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm":
"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm":
version "0.0.0"
uid ""
@@ -9067,9 +9067,10 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "37.3.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/eb793aaa08c377b0d9d66a4bbbe6854286523221"
matrix-js-sdk@37.4.0-rc.0:
version "37.4.0-rc.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-37.4.0-rc.0.tgz#dcbb4bc7813179eb54d3ea093d0e0e18815749c1"
integrity sha512-d4cxNfhqGXk7NdLaqGpMAByMNGlJlLNmgeQm/8cURYxEX6SaHCpVDRX3AfYtwKYWVi1MUgy+NQpZ37fR2y3BCg==
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^14.0.1"