Update controls.
This commit is contained in:
9
src/@types/matrix-js-sdk.d.ts
vendored
9
src/@types/matrix-js-sdk.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 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,8 +14,12 @@ 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 { 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" {
|
||||
export interface FileInfo {
|
||||
/**
|
||||
@@ -59,7 +63,6 @@ declare module "matrix-js-sdk/src/types" {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AccountDataEvents {
|
||||
// Analytics account data event
|
||||
"im.vector.analytics": {
|
||||
@@ -87,6 +90,8 @@ declare module "matrix-js-sdk/src/types" {
|
||||
"m.accepted_terms": {
|
||||
accepted: string[];
|
||||
};
|
||||
|
||||
"io.element.msc4278.media_preview_config": MediaPreviewConfig,
|
||||
}
|
||||
|
||||
export interface AudioContent {
|
||||
|
||||
10
src/@types/media_preview.ts
Normal file
10
src/@types/media_preview.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum MediaPreviewValue {
|
||||
On = "on",
|
||||
Private = "private",
|
||||
Off = "off"
|
||||
}
|
||||
|
||||
export interface MediaPreviewConfig extends Record<string, unknown> {
|
||||
media_previews: MediaPreviewValue,
|
||||
invite_avatars: MediaPreviewValue,
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import SettingsStore from "./settings/SettingsStore";
|
||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
||||
import { MediaPreviewValue } from "./@types/media_preview";
|
||||
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
|
||||
@@ -50,7 +51,8 @@ export const transformTags: NonNullable<IOptions["transformTags"]> = {
|
||||
// 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")) {
|
||||
// TODO: Is this a private room?
|
||||
if (!src || SettingsStore.getValue("mediaPreviewConfig").media_previews !== MediaPreviewValue.On ) {
|
||||
return { tagName, attribs: {} };
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { type IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import { LocalRoom } from "../../../models/LocalRoom";
|
||||
import { filterBoolean } from "../../../utils/arrays";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MediaPreviewValue } from "../../../@types/media_preview";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
@@ -96,7 +97,8 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||
private static getImageUrls(props: IProps): string[] {
|
||||
const myMembership = props.room?.getMyMembership();
|
||||
if (myMembership === KnownMembership.Invite || !myMembership) {
|
||||
if (SettingsStore.getValue("showAvatarsOnInvites") === false) {
|
||||
|
||||
if (SettingsStore.getValue("mediaPreviewConfig", props.room?.roomId).invite_avatars !== MediaPreviewValue.On) {
|
||||
// The user has opted out of showing avatars, so return no urls here.
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import ImageView from "../elements/ImageView";
|
||||
import LinkWithTooltip from "../elements/LinkWithTooltip";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import { MediaPreviewValue } from "../../../@types/media_preview";
|
||||
|
||||
interface IProps {
|
||||
link: string;
|
||||
@@ -69,7 +70,8 @@ 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")) {
|
||||
// HSTODO: Private rooms?
|
||||
if (!SettingsStore.getValue("mediaPreviewConfig", this.props.mxEvent.getRoomId()).media_previews !== MediaPreviewValue.On) {
|
||||
image = null; // Don't render a button to show the image, just hide it outright
|
||||
}
|
||||
const imageMaxWidth = 100;
|
||||
|
||||
@@ -116,7 +116,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 +146,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||
"urlPreviewsEnabled",
|
||||
"autoplayGifs",
|
||||
"autoplayVideo",
|
||||
"showImages",
|
||||
];
|
||||
|
||||
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
|
||||
|
||||
@@ -5,13 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import { useSettingValue } from "./useSettings";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
||||
import { JoinRule, MediaPreviewConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||
import { MediaPreviewValue } from "../@types/media_preview";
|
||||
|
||||
const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRule.Restricted];
|
||||
|
||||
@@ -22,7 +23,6 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul
|
||||
*/
|
||||
export function useMediaVisible(eventId: string, roomId: string): [boolean, (visible: boolean) => void] {
|
||||
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId);
|
||||
// const defaultShowImages = useSettingValue("showImages");
|
||||
const client = useMatrixClientContext();
|
||||
const eventVisibility = useSettingValue("showMediaEventIds");
|
||||
const setMediaVisible = useCallback(
|
||||
@@ -38,10 +38,10 @@ export function useMediaVisible(eventId: string, roomId: string): [boolean, (vis
|
||||
// Always prefer the explicit per-event user preference here.
|
||||
if (eventVisibility[eventId]) {
|
||||
return [true, setMediaVisible];
|
||||
} else if (mediaPreviewSetting === MediaPreviewConfig.Off) {
|
||||
return [false, setMediaVisible];
|
||||
} else if (mediaPreviewSetting === MediaPreviewConfig.On) {
|
||||
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Off) {
|
||||
return [false, setMediaVisible];
|
||||
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.On) {
|
||||
return [true, setMediaVisible];
|
||||
}
|
||||
const joinRule = client.getRoom(roomId)?.getJoinRule();
|
||||
if (PRIVATE_JOIN_RULES.includes(joinRule as JoinRule)) {
|
||||
@@ -51,5 +51,4 @@ export function useMediaVisible(eventId: string, roomId: string): [boolean, (vis
|
||||
console.log("Room is probably public");
|
||||
return [false, setMediaVisible];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from "react";
|
||||
import { MediaPreviewConfig, UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
|
||||
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
|
||||
import { MediaPreviewConfig } from "../@types/media_preview.ts";
|
||||
|
||||
import { _t, _td, type TranslationKey } from "../languageHandler";
|
||||
import DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts";
|
||||
@@ -313,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>;
|
||||
@@ -432,7 +431,7 @@ export const SETTINGS: Settings = {
|
||||
"mediaPreviewConfig": {
|
||||
controller: new MediaPreviewConfigController(),
|
||||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||
default: MediaPreviewConfig.Private,
|
||||
default: MediaPreviewConfigController.default,
|
||||
},
|
||||
"feature_report_to_moderators": {
|
||||
isFeature: true,
|
||||
@@ -1130,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
if (showImages !== null || showAvatarsOnInvites !== null) {
|
||||
this.setValue("mediaPreviewConfig", null, SettingLevel.ACCOUNT, {
|
||||
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;
|
||||
|
||||
@@ -5,33 +5,41 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ClientEvent, MatrixEvent, MediaPreviewConfig, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent, MatrixEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { AccountDataEvents } from "matrix-js-sdk/src/types";
|
||||
import { MediaPreviewConfig, MediaPreviewValue } from "../../@types/media_preview.ts";
|
||||
|
||||
import { SettingLevel } from "../SettingLevel.ts";
|
||||
import MatrixClientBackedController from "./MatrixClientBackedController.ts";
|
||||
|
||||
|
||||
const CLIENT_KEY = "m.media_preview_config";
|
||||
const CLIENT_KEY = "io.element.msc4278.media_preview_config";
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
export default class MediaPreviewConfigController extends MatrixClientBackedController {
|
||||
private globalSetting: MediaPreviewConfig = MediaPreviewConfig.Private;
|
||||
|
||||
public static readonly default: AccountDataEvents["io.element.msc4278.media_preview_config"] = {
|
||||
media_previews: MediaPreviewValue.On,
|
||||
invite_avatars: MediaPreviewValue.On
|
||||
}
|
||||
|
||||
private globalSetting: MediaPreviewConfig = MediaPreviewConfigController.default;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private getRoomValue = (roomId: string): MediaPreviewConfig|null => {
|
||||
return this.client?.getRoom(roomId)?.getAccountData(CLIENT_KEY)?.getContent().value ?? null;
|
||||
return this.client?.getRoom(roomId)?.getAccountData(CLIENT_KEY)?.getContent<MediaPreviewConfig>() ?? null;
|
||||
}
|
||||
|
||||
private onAccountData = (event: MatrixEvent): void => {
|
||||
// TODO: Validate.
|
||||
const roomId = event.getRoomId();
|
||||
if (!roomId) {
|
||||
this.globalSetting = event.getContent().value;
|
||||
this.globalSetting = event.getContent();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,13 +64,11 @@ export default class MediaPreviewConfigController extends MatrixClientBackedCont
|
||||
|
||||
public onChange(_level: SettingLevel, roomId: string | null, newValue: MediaPreviewConfig): void {
|
||||
if (roomId) {
|
||||
this.client?.setRoomAccountData(roomId, "m.media_preview_config", {
|
||||
this.client?.setRoomAccountData(roomId, CLIENT_KEY, {
|
||||
value: newValue
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.client?.setAccountDataRaw( "m.media_preview_config", {
|
||||
value: newValue
|
||||
});
|
||||
this.client?.setAccountDataRaw(CLIENT_KEY, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user