Global configuration flag for media previews (#29582)
* Modify useMediaVisible to take a room. * Add initial support for a account data level key. * Update controls. * Update settings * Lint and fixes * make some tests go happy * lint * i18n * update preferences * prettier * Update settings tab. * update screenshot * Update docs * Rewrite controller * Rewrite tons of tests * Rewrite RoomAvatar to be a functional component This is so we can use hooks to determine the setting state. * lint * lint * Tidy up comments * Apply media visible hook to inline images. * Move conditionals. * copyright all the things * Review changes * Update html utils to properly discard media. * Types fix * Fixing tests that break settings getValue expectations * Fix logic around media preview calculation * Fix room header tests * Fixup tests for timelinePanel * Clear settings in matrixchat * Update tests to use SettingsStore where possible. * fix bug * revert changes to client.ts * copyright years * Add header * Add a test for MediaPreviewAccountSettingsTab * Mark initMatrixClient as optional * Improve on types * Ensure we do not set the account data twice. * lint * Review changes * Ensure we include the client on rendered messages. * Fix test * update labels * clean designs * update settings tab * update snapshot * copyright * prevent mutation
This commit is contained in:
5
src/@types/matrix-js-sdk.d.ts
vendored
5
src/@types/matrix-js-sdk.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -14,6 +14,7 @@ import type { EncryptedFile } from "matrix-js-sdk/src/types";
|
||||
import type { EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||
import type { 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" {
|
||||
@@ -87,6 +88,8 @@ declare module "matrix-js-sdk/src/types" {
|
||||
"m.accepted_terms": {
|
||||
accepted: string[];
|
||||
};
|
||||
|
||||
"io.element.msc4278.media_preview_config": MediaPreviewConfig;
|
||||
}
|
||||
|
||||
export interface AudioContent {
|
||||
|
||||
33
src/@types/media_preview.ts
Normal file
33
src/@types/media_preview.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export enum MediaPreviewValue {
|
||||
/**
|
||||
* Media previews should be enabled.
|
||||
*/
|
||||
On = "on",
|
||||
/**
|
||||
* Media previews should only be enabled for rooms with non-public join rules.
|
||||
*/
|
||||
Private = "private",
|
||||
/**
|
||||
* Media previews should be disabled.
|
||||
*/
|
||||
Off = "off",
|
||||
}
|
||||
|
||||
export const MEDIA_PREVIEW_ACCOUNT_DATA_TYPE = "io.element.msc4278.media_preview_config";
|
||||
export interface MediaPreviewConfig extends Record<string, unknown> {
|
||||
/**
|
||||
* Media preview setting for thumbnails of media in rooms.
|
||||
*/
|
||||
media_previews: MediaPreviewValue;
|
||||
/**
|
||||
* Media preview settings for avatars of rooms we have been invited to.
|
||||
*/
|
||||
invite_avatars: MediaPreviewValue.On | MediaPreviewValue.Off;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
@@ -294,6 +294,10 @@ 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 {
|
||||
@@ -302,6 +306,20 @@ 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 = {
|
||||
...sanitizeParams,
|
||||
transformTags: {
|
||||
...sanitizeParams.transformTags,
|
||||
img: (tagName) => {
|
||||
// Remove element
|
||||
return { tagName, attribs: {} };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const isFormattedBody =
|
||||
content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -12,7 +12,6 @@ import { merge } from "lodash";
|
||||
import _Linkify from "linkify-react";
|
||||
|
||||
import { _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";
|
||||
@@ -47,10 +46,7 @@ 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.
|
||||
// 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")) {
|
||||
if (!src) {
|
||||
return { tagName, attribs: {} };
|
||||
}
|
||||
|
||||
@@ -78,7 +74,6 @@ export const transformTags: NonNullable<IOptions["transformTags"]> = {
|
||||
if (requestedHeight) {
|
||||
attribs.style += "height: 100%;";
|
||||
}
|
||||
|
||||
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!;
|
||||
return { tagName, attribs };
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ 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,
|
||||
@@ -40,7 +41,8 @@ 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("showAvatarsOnInvites", room?.roomId);
|
||||
const showAvatarsOnInvites =
|
||||
useSettingValue("mediaPreviewConfig", room?.roomId).invite_avatars === MediaPreviewValue.On;
|
||||
|
||||
const onRoomAvatarClick = useCallback(() => {
|
||||
const avatarUrl = Avatar.avatarUrlForRoom(room ?? null);
|
||||
@@ -63,7 +65,6 @@ 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");
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
@@ -150,6 +151,7 @@ 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(
|
||||
@@ -167,8 +169,9 @@ const EventContentBody = memo(
|
||||
disableBigEmoji: isEmote || !enableBigEmoji,
|
||||
// Part of Replies fallback support
|
||||
stripReplyFallback: stripReply,
|
||||
mediaIsVisible,
|
||||
}),
|
||||
[content, enableBigEmoji, highlights, isEmote, stripReply],
|
||||
[content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply],
|
||||
);
|
||||
|
||||
if (as === "div") includeDir = true; // force dir="auto" on divs
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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()!);
|
||||
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
|
||||
|
||||
if (!mediaIsVisible) {
|
||||
return;
|
||||
|
||||
@@ -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()!);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
|
||||
}
|
||||
}
|
||||
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
|
||||
}
|
||||
|
||||
const MStickerBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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()!);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -17,6 +17,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||
|
||||
const INITIAL_NUM_PREVIEWS = 2;
|
||||
|
||||
@@ -29,6 +30,7 @@ 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][]>(
|
||||
@@ -55,7 +57,13 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) =
|
||||
return (
|
||||
<div className="mx_LinkPreviewGroup">
|
||||
{showPreviews.map(([link, preview], i) => (
|
||||
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
|
||||
<LinkPreviewWidget
|
||||
mediaVisible={mediaVisible}
|
||||
key={link}
|
||||
link={link}
|
||||
preview={preview}
|
||||
mxEvent={mxEvent}
|
||||
>
|
||||
{i === 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_LinkPreviewGroup_hide"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -11,7 +11,6 @@ import { decode } from "html-entities";
|
||||
import { type MatrixEvent, type IPreviewUrlResponse } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { Linkify } from "../../../HtmlUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
@@ -24,6 +23,7 @@ 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 (!SettingsStore.getValue("showImages")) {
|
||||
if (!this.props.mediaVisible) {
|
||||
image = null; // Don't render a button to show the image, just hide it outright
|
||||
}
|
||||
const imageMaxWidth = 100;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019-2024 New Vector Ltd.
|
||||
Copyright 2019-2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
@@ -22,6 +22,7 @@ import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";
|
||||
import { MediaPreviewAccountSettings } from "../user/MediaPreviewAccountSettings";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
@@ -92,6 +93,9 @@ 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")} legacy={false}>
|
||||
<MediaPreviewAccountSettings roomId={room.roomId} />
|
||||
</SettingsSubsection>
|
||||
{leaveSection}
|
||||
</SettingsSection>
|
||||
</SettingsTab>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ChangeEventHandler, useCallback } from "react";
|
||||
import { Field, HelpMessage, InlineField, Label, RadioInput, Root } from "@vector-im/compound-web";
|
||||
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import { type MediaPreviewConfig, MediaPreviewValue } from "../../../../../@types/media_preview";
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
|
||||
export const MediaPreviewAccountSettings: React.FC<{ roomId?: string }> = ({ roomId }) => {
|
||||
const currentMediaPreview = useSettingValue("mediaPreviewConfig", roomId);
|
||||
|
||||
const changeSetting = useCallback(
|
||||
(newValue: MediaPreviewConfig) => {
|
||||
SettingsStore.setValue(
|
||||
"mediaPreviewConfig",
|
||||
roomId ?? null,
|
||||
roomId ? SettingLevel.ROOM_ACCOUNT : SettingLevel.ACCOUNT,
|
||||
newValue,
|
||||
);
|
||||
},
|
||||
[roomId],
|
||||
);
|
||||
|
||||
const avatarOnChange = useCallback(
|
||||
(c: boolean) => {
|
||||
changeSetting({
|
||||
...currentMediaPreview,
|
||||
// Switch is inverted. "Hide avatars..."
|
||||
invite_avatars: c ? MediaPreviewValue.Off : MediaPreviewValue.On,
|
||||
});
|
||||
},
|
||||
[changeSetting, currentMediaPreview],
|
||||
);
|
||||
|
||||
const mediaPreviewOnChangeOff = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
(event) => {
|
||||
if (!event.target.checked) {
|
||||
return;
|
||||
}
|
||||
changeSetting({
|
||||
...currentMediaPreview,
|
||||
media_previews: MediaPreviewValue.Off,
|
||||
});
|
||||
},
|
||||
[changeSetting, currentMediaPreview],
|
||||
);
|
||||
|
||||
const mediaPreviewOnChangePrivate = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
(event) => {
|
||||
if (!event.target.checked) {
|
||||
return;
|
||||
}
|
||||
changeSetting({
|
||||
...currentMediaPreview,
|
||||
media_previews: MediaPreviewValue.Private,
|
||||
});
|
||||
},
|
||||
[changeSetting, currentMediaPreview],
|
||||
);
|
||||
|
||||
const mediaPreviewOnChangeOn = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
(event) => {
|
||||
if (!event.target.checked) {
|
||||
return;
|
||||
}
|
||||
changeSetting({
|
||||
...currentMediaPreview,
|
||||
media_previews: MediaPreviewValue.On,
|
||||
});
|
||||
},
|
||||
[changeSetting, currentMediaPreview],
|
||||
);
|
||||
|
||||
return (
|
||||
<Root className="mx_MediaPreviewAccountSetting_Form">
|
||||
{!roomId && (
|
||||
<LabelledToggleSwitch
|
||||
className="mx_MediaPreviewAccountSetting_ToggleSwitch"
|
||||
label={_t("settings|media_preview|hide_avatars")}
|
||||
value={currentMediaPreview.invite_avatars === MediaPreviewValue.Off}
|
||||
onChange={avatarOnChange}
|
||||
/>
|
||||
)}
|
||||
{/* Explict label here because htmlFor is not supported for linking to radiogroups */}
|
||||
<Field
|
||||
id="mx_media_previews"
|
||||
role="radiogroup"
|
||||
name="media_previews"
|
||||
aria-label={_t("settings|media_preview|media_preview_label")}
|
||||
>
|
||||
<Label>{_t("settings|media_preview|media_preview_label")}</Label>
|
||||
<HelpMessage className="mx_MediaPreviewAccountSetting_RadioHelp">
|
||||
{_t("settings|media_preview|media_preview_description")}
|
||||
</HelpMessage>
|
||||
<InlineField
|
||||
name="media_preview_off"
|
||||
className="mx_MediaPreviewAccountSetting_Radio"
|
||||
control={
|
||||
<RadioInput
|
||||
id="mx_media_previews_off"
|
||||
checked={currentMediaPreview.media_previews === MediaPreviewValue.Off}
|
||||
onChange={mediaPreviewOnChangeOff}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Label htmlFor="mx_media_previews_off">{_t("settings|media_preview|hide_media")}</Label>
|
||||
</InlineField>
|
||||
{!roomId && (
|
||||
<InlineField
|
||||
name="mx_media_previews_private"
|
||||
className="mx_MediaPreviewAccountSetting_Radio"
|
||||
control={
|
||||
<RadioInput
|
||||
id="mx_media_previews_private"
|
||||
checked={currentMediaPreview.media_previews === MediaPreviewValue.Private}
|
||||
onChange={mediaPreviewOnChangePrivate}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Label htmlFor="mx_media_previews_private">
|
||||
{_t("settings|media_preview|show_in_private")}
|
||||
</Label>
|
||||
</InlineField>
|
||||
)}
|
||||
<InlineField
|
||||
name="media_preview_on"
|
||||
className="mx_MediaPreviewAccountSetting_Radio"
|
||||
control={
|
||||
<RadioInput
|
||||
id="mx_media_previews_on"
|
||||
checked={currentMediaPreview.media_previews === MediaPreviewValue.On}
|
||||
onChange={mediaPreviewOnChangeOn}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Label htmlFor="mx_media_previews_on">{_t("settings|media_preview|show_media")}</Label>
|
||||
</InlineField>
|
||||
</Field>
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
@@ -32,6 +32,7 @@ import SpellCheckSettings from "../../SpellCheckSettings";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
||||
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
|
||||
import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn(success: boolean): void;
|
||||
@@ -116,7 +117,7 @@ const SpellCheckSection: React.FC = () => {
|
||||
};
|
||||
|
||||
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
|
||||
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs", "showAvatarsOnInvites"];
|
||||
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs"];
|
||||
|
||||
private static SPACES_SETTINGS: BooleanSettingKey[] = ["Spaces.allRoomsInHome"];
|
||||
|
||||
@@ -146,7 +147,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||
"urlPreviewsEnabled",
|
||||
"autoplayGifs",
|
||||
"autoplayVideo",
|
||||
"showImages",
|
||||
];
|
||||
|
||||
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
|
||||
@@ -335,6 +335,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||
</SettingsSubsection>
|
||||
|
||||
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
||||
<MediaPreviewAccountSettings />
|
||||
</SettingsSubsection>
|
||||
|
||||
<SettingsSubsection heading={_t("settings|preferences|room_directory_heading")}>
|
||||
{this.renderGroup(PreferencesUserSettingsTab.ROOM_DIRECTORY_SETTINGS)}
|
||||
</SettingsSubsection>
|
||||
|
||||
@@ -6,30 +6,52 @@ 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): [boolean, (visible: boolean) => void] {
|
||||
const defaultShowImages = useSettingValue("showImages", SettingLevel.DEVICE);
|
||||
const eventVisibility = useSettingValue("showMediaEventIds", SettingLevel.DEVICE);
|
||||
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());
|
||||
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.
|
||||
const imgIsVisible = eventVisibility[eventId] ?? defaultShowImages;
|
||||
return [imgIsVisible, setMediaVisible];
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,6 +525,7 @@
|
||||
"message_timestamp_invalid": "Invalid timestamp",
|
||||
"microphone": "Microphone",
|
||||
"model": "Model",
|
||||
"moderation_and_safety": "Moderation and safety",
|
||||
"modern": "Modern",
|
||||
"mute": "Mute",
|
||||
"n_members": {
|
||||
@@ -2668,12 +2669,10 @@
|
||||
"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).",
|
||||
@@ -2732,6 +2731,14 @@
|
||||
"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)",
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 {
|
||||
@@ -45,6 +46,7 @@ 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();
|
||||
|
||||
@@ -312,8 +314,6 @@ 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,6 +349,7 @@ export interface Settings {
|
||||
"Electron.alwaysShowMenuBar": IBaseSetting<boolean>;
|
||||
"Electron.showTrayIcon": IBaseSetting<boolean>;
|
||||
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
|
||||
"mediaPreviewConfig": IBaseSetting<MediaPreviewConfig>;
|
||||
"Developer.elementCallUrl": IBaseSetting<string>;
|
||||
}
|
||||
|
||||
@@ -427,6 +428,11 @@ 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,
|
||||
@@ -1123,16 +1129,6 @@ 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,
|
||||
@@ -1386,7 +1382,6 @@ export const SETTINGS: Settings = {
|
||||
displayName: _td("settings|preferences|enable_hardware_acceleration"),
|
||||
default: true,
|
||||
},
|
||||
|
||||
"Developer.elementCallUrl": {
|
||||
supportedLevels: [SettingLevel.DEVICE],
|
||||
displayName: _td("devtools|settings|elementCallUrl"),
|
||||
|
||||
@@ -38,6 +38,7 @@ 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> = {};
|
||||
@@ -715,6 +716,29 @@ 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.
|
||||
*/
|
||||
@@ -732,6 +756,12 @@ 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;
|
||||
|
||||
@@ -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 abstract initMatrixClient(newClient: MatrixClient, oldClient?: MatrixClient): void;
|
||||
protected initMatrixClient?(newClient: MatrixClient, oldClient?: MatrixClient): void;
|
||||
}
|
||||
|
||||
100
src/settings/controllers/MediaPreviewConfigController.ts
Normal file
100
src/settings/controllers/MediaPreviewConfigController.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type IContent } from "matrix-js-sdk/src/matrix";
|
||||
import { type AccountDataEvents } from "matrix-js-sdk/src/types";
|
||||
|
||||
import {
|
||||
MEDIA_PREVIEW_ACCOUNT_DATA_TYPE,
|
||||
type MediaPreviewConfig,
|
||||
MediaPreviewValue,
|
||||
} from "../../@types/media_preview.ts";
|
||||
import { type SettingLevel } from "../SettingLevel.ts";
|
||||
import MatrixClientBackedController from "./MatrixClientBackedController.ts";
|
||||
|
||||
/**
|
||||
* Handles media preview settings provided by MSC4278.
|
||||
* This uses both account-level and room-level account data.
|
||||
*/
|
||||
export default class MediaPreviewConfigController extends MatrixClientBackedController {
|
||||
public static readonly default: AccountDataEvents[typeof MEDIA_PREVIEW_ACCOUNT_DATA_TYPE] = {
|
||||
media_previews: MediaPreviewValue.On,
|
||||
invite_avatars: MediaPreviewValue.On,
|
||||
};
|
||||
|
||||
private static getValidSettingData(content: IContent): Partial<MediaPreviewConfig> {
|
||||
const mediaPreviews: MediaPreviewConfig["media_previews"] = content.media_previews;
|
||||
const inviteAvatars: MediaPreviewConfig["invite_avatars"] = content.invite_avatars;
|
||||
const validMediaPreviews = Object.values(MediaPreviewValue);
|
||||
const validInviteAvatars = [MediaPreviewValue.Off, MediaPreviewValue.On];
|
||||
return {
|
||||
invite_avatars: validInviteAvatars.includes(inviteAvatars) ? inviteAvatars : undefined,
|
||||
media_previews: validMediaPreviews.includes(mediaPreviews) ? mediaPreviews : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private getValue = (roomId?: string): MediaPreviewConfig => {
|
||||
const source = roomId ? this.client?.getRoom(roomId) : this.client;
|
||||
const accountData =
|
||||
source?.getAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE)?.getContent<MediaPreviewConfig>() ?? {};
|
||||
|
||||
const calculatedConfig = MediaPreviewConfigController.getValidSettingData(accountData);
|
||||
|
||||
// Save an account data fetch if we have all the values.
|
||||
if (calculatedConfig.invite_avatars && calculatedConfig.media_previews) {
|
||||
return calculatedConfig as MediaPreviewConfig;
|
||||
}
|
||||
|
||||
// We're missing some keys.
|
||||
if (roomId) {
|
||||
const globalConfig = this.getValue();
|
||||
return {
|
||||
invite_avatars:
|
||||
calculatedConfig.invite_avatars ??
|
||||
globalConfig.invite_avatars ??
|
||||
MediaPreviewConfigController.default.invite_avatars,
|
||||
media_previews:
|
||||
calculatedConfig.media_previews ??
|
||||
globalConfig.media_previews ??
|
||||
MediaPreviewConfigController.default.media_previews,
|
||||
};
|
||||
}
|
||||
return {
|
||||
invite_avatars: calculatedConfig.invite_avatars ?? MediaPreviewConfigController.default.invite_avatars,
|
||||
media_previews: calculatedConfig.media_previews ?? MediaPreviewConfigController.default.media_previews,
|
||||
};
|
||||
};
|
||||
|
||||
public getValueOverride(_level: SettingLevel, roomId: string | null): MediaPreviewConfig {
|
||||
return this.getValue(roomId ?? undefined);
|
||||
}
|
||||
|
||||
public get settingDisabled(): false {
|
||||
// No homeserver support is required for this MSC.
|
||||
return false;
|
||||
}
|
||||
|
||||
public async beforeChange(
|
||||
_level: SettingLevel,
|
||||
roomId: string | null,
|
||||
newValue: MediaPreviewConfig,
|
||||
): Promise<boolean> {
|
||||
if (!this.client) {
|
||||
return false;
|
||||
}
|
||||
if (roomId) {
|
||||
await this.client.setRoomAccountData(roomId, MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue);
|
||||
return true;
|
||||
}
|
||||
await this.client.setAccountData(MEDIA_PREVIEW_ACCOUNT_DATA_TYPE, newValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 Travis Ralston
|
||||
|
||||
@@ -15,6 +15,7 @@ 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";
|
||||
@@ -68,6 +69,8 @@ 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());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,7 +176,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
||||
await deferred.promise;
|
||||
}
|
||||
|
||||
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
||||
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
||||
switch (settingName) {
|
||||
// Special case URL previews
|
||||
case "urlPreviewsEnabled":
|
||||
@@ -199,7 +202,9 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 Travis Ralston
|
||||
|
||||
@@ -14,6 +14,7 @@ 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";
|
||||
@@ -56,6 +57,8 @@ 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());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,7 +111,7 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
||||
await deferred.promise;
|
||||
}
|
||||
|
||||
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
||||
public async setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
|
||||
switch (settingName) {
|
||||
// Special case URL previews
|
||||
case "urlPreviewsEnabled":
|
||||
@@ -117,7 +120,9 @@ 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user