Update URL Preview settings (#32992)

* Remove ability for url previews to be set per-room

* Add ability to enable E2EE URL Previews globally

* Remove old migration

* Cleanup

* Remove room account handler

* update snap

* screenshot updated

* Add a test
This commit is contained in:
Will Hunt
2026-04-09 12:32:50 +00:00
committed by GitHub
parent 253dcb44dd
commit b4d0c21abf
16 changed files with 231 additions and 719 deletions
@@ -1380,12 +1380,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
if (ev.getType() === "org.matrix.room.preview_urls") {
this.updatePreviewUrlVisibility(room);
this.updatePreviewUrlVisibility();
}
if (ev.getType() === "m.room.encryption") {
this.updateE2EStatus(room);
this.updatePreviewUrlVisibility(room);
this.updatePreviewUrlVisibility();
}
// ignore anything but real-time updates at the end of the room:
@@ -1541,15 +1541,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
}
private updatePreviewUrlVisibility(room: Room): void {
private updatePreviewUrlVisibility(): void {
this.setState(({ isRoomEncrypted }) => ({
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
showUrlPreview: this.getPreviewUrlVisibility(isRoomEncrypted),
}));
}
private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean {
const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
return SettingsStore.getValue(key, roomId);
private getPreviewUrlVisibility(isRoomEncrypted: boolean | null): boolean {
return SettingsStore.getValue(isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled");
}
private onRoom = (room: Room): void => {
@@ -1608,9 +1607,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
private onUrlPreviewsEnabledChange = (): void => {
if (this.state.room) {
this.updatePreviewUrlVisibility(this.state.room);
}
this.updatePreviewUrlVisibility();
};
private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => {
@@ -1638,7 +1635,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.setState({
isRoomEncrypted,
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
showUrlPreview: this.getPreviewUrlVisibility(isRoomEncrypted),
...(newE2EStatus && { e2eStatus: newE2EStatus }),
});
}
@@ -25,6 +25,7 @@ interface IProps {
label?: string;
isExplicit?: boolean;
hideIfCannotSet?: boolean;
requires?: BooleanSettingKey[];
onChange?(checked: boolean): void;
}
@@ -45,6 +46,12 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
public componentDidMount(): void {
defaultWatchManager.watchSetting(this.props.name, this.props.roomId ?? null, this.onSettingChange);
if (this.props.requires) {
// If we have any dependencies for this feature, also watch those features to ensure we catch the disabled state.
for (const flag of this.props.requires) {
defaultWatchManager.watchSetting(flag, this.props.roomId ?? null, this.onSettingChange);
}
}
}
public componentWillUnmount(): void {
@@ -1,152 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019 New Vector Ltd
Copyright 2017 Travis Ralston
Copyright 2016 OpenMarket 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 ReactNode, type JSX } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import { InlineSpinner } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsFieldset from "../settings/SettingsFieldset";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
import { useSettingValueAt } from "../../../hooks/useSettings.ts";
/**
* The URL preview settings for a room
*/
interface UrlPreviewSettingsProps {
/**
* The room.
*/
room: Room;
}
export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element {
const { roomId } = room;
const matrixClient = useMatrixClientContext();
const isEncrypted = useIsEncrypted(matrixClient, room);
const isLoading = isEncrypted === null;
return (
<SettingsFieldset
legend={_t("room_settings|general|url_previews_section")}
description={!isLoading && <Description isEncrypted={isEncrypted} />}
>
{isLoading ? (
<InlineSpinner />
) : (
<>
<PreviewsForRoom isEncrypted={isEncrypted} roomId={roomId} />
<SettingsFlag
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
level={SettingLevel.ROOM_DEVICE}
roomId={roomId}
/>
</>
)}
</SettingsFieldset>
);
}
/**
* Click handler for the user settings link
* @param e
*/
function onClickUserSettings(e: ButtonEvent): void {
e.preventDefault();
e.stopPropagation();
dis.fire(Action.ViewUserSettings);
}
/**
* The description for the URL preview settings
*/
interface DescriptionProps {
/**
* Whether the room is encrypted
*/
isEncrypted: boolean;
}
function Description({ isEncrypted }: DescriptionProps): JSX.Element {
const urlPreviewsEnabled = useSettingValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
let previewsForAccount: ReactNode | undefined;
if (isEncrypted) {
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
} else {
const button = {
a: (sub: string) => (
<AccessibleButton kind="link_inline" onClick={onClickUserSettings}>
{sub}
</AccessibleButton>
),
};
previewsForAccount = urlPreviewsEnabled
? _t("room_settings|general|user_url_previews_default_on", {}, button)
: _t("room_settings|general|user_url_previews_default_off", {}, button);
}
return (
<>
<p>{_t("room_settings|general|url_preview_explainer")}</p>
<p>{previewsForAccount}</p>
</>
);
}
/**
* The description for the URL preview settings
*/
interface PreviewsForRoomProps {
/**
* Whether the room is encrypted
*/
isEncrypted: boolean;
/**
* The room ID
*/
roomId: string;
}
function PreviewsForRoom({ isEncrypted, roomId }: PreviewsForRoomProps): JSX.Element | null {
const urlPreviewsEnabled = useSettingValueAt(
SettingLevel.ACCOUNT,
"urlPreviewsEnabled",
roomId,
/*explicit=*/ true,
);
if (isEncrypted) return null;
let previewsForRoom: ReactNode;
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
previewsForRoom = (
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.ROOM} roomId={roomId} isExplicit={true} />
);
} else {
previewsForRoom = (
<div>
{urlPreviewsEnabled
? _t("room_settings|general|default_url_previews_on")
: _t("room_settings|general|default_url_previews_off")}
</div>
);
}
return previewsForRoom;
}
@@ -15,14 +15,11 @@ import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
import AccessibleButton, { type ButtonEvent } from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature";
import AliasSettings from "../../../room_settings/AliasSettings";
import PosthogTrackers from "../../../../../PosthogTrackers";
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 {
@@ -62,10 +59,6 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", "") ?? undefined;
const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ? (
<UrlPreviewSettings room={room} />
) : null;
let leaveSection;
if (room.getMyMembership() === KnownMembership.Join) {
leaveSection = (
@@ -99,7 +92,6 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
</SettingsSection>
<SettingsSection heading={_t("room_settings|general|other_section")}>
{urlPreviewSettings}
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
<MediaPreviewAccountSettings roomId={room.roomId} />
</SettingsSubsection>
@@ -147,11 +147,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
"showCodeLineNumbers",
];
private static IMAGES_AND_VIDEOS_SETTINGS: BooleanSettingKey[] = [
"urlPreviewsEnabled",
"autoplayGifs",
"autoplayVideo",
];
private static IMAGES_AND_VIDEOS_SETTINGS: BooleanSettingKey[] = ["autoplayGifs", "autoplayVideo"];
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
"showTypingNotifications",
@@ -350,6 +346,19 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
</SettingsSubsection>
<SettingsSubsection
heading={_t("settings|preferences|link_previews_heading")}
description={_t("settings|preferences|link_previews_description")}
formWrap
>
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.DEVICE} />
<SettingsFlag
name="urlPreviewsEnabled_e2ee"
level={SettingLevel.DEVICE}
requires={["urlPreviewsEnabled"]}
/>
</SettingsSubsection>
<SettingsSubsection heading={_t("settings|preferences|media_heading")} formWrap>
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
</SettingsSubsection>