Convert cases of mxcUrlToHttp to new media customisation

This commit is contained in:
Travis Ralston 2021-03-03 19:06:46 -07:00
parent 53935782bc
commit 1ac12479ca
33 changed files with 178 additions and 121 deletions

View file

@ -36,6 +36,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
import ReplyThread from "./components/views/elements/ReplyThread";
import {mediaFromMxc} from "./customisations/Media";
linkifyMatrix(linkify);
@ -181,11 +182,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
return { tagName, attribs: {}};
}
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
attribs.src,
attribs.width || 800,
attribs.height || 600,
);
const width = Number(attribs.width) || 800;
const height = Number(attribs.height) || 600;
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
return { tagName, attribs };
},
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {

View file

@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel";
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
import RoomViewStore from "./stores/RoomViewStore";
import UserActivity from "./UserActivity";
import {mediaFromMxc} from "./customisations/Media";
/*
* Dispatches:
@ -150,7 +151,7 @@ export const Notifier = {
// Ideally in here we could use MSC1310 to detect the type of file, and reject it.
return {
url: MatrixClientPeg.get().mxcUrlToHttp(content.url),
url: mediaFromMxc(content.url).srcHttp,
name: content.name,
type: content.type,
size: content.size,

View file

@ -27,6 +27,7 @@ import {sortBy} from "lodash";
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter";
import FlairStore from "../stores/FlairStore";
import {mediaFromMxc} from "../customisations/Media";
const COMMUNITY_REGEX = /\B\+\S*/g;
@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider {
name={name || groupId}
width={24}
height={24}
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} />
</PillCompletion>
),
range,

View file

@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar";
import {mediaFromMxc} from "../../customisations/Media";
import {replaceableComponent} from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td(
@ -368,8 +369,7 @@ class FeaturedUser extends React.Component {
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
const httpUrl = MatrixClientPeg.get()
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64);
const deleteButton = this.props.editing ?
<img
@ -981,10 +981,9 @@ export default class GroupView extends React.Component {
<Spinner />
</div>;
}
const httpInviterAvatar = this.state.inviterProfile ?
this._matrixClient.mxcUrlToHttp(
this.state.inviterProfile.avatarUrl, 36, 36,
) : null;
const httpInviterAvatar = this.state.inviterProfile
? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
: null;
const inviter = group.inviter || {};
let inviterName = inviter.userId;

View file

@ -41,6 +41,7 @@ import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media";
interface IProps {
isMinimized: boolean;
@ -121,7 +122,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
if (settingBgMxc) {
avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize);
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
}
const avatarUrlProp = `url(${avatarUrl})`;

View file

@ -34,6 +34,7 @@ import {EnhancedMap} from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar";
import {mediaFromMxc} from "../../customisations/Media";
interface IProps {
space: Room;
@ -158,12 +159,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
let url: string;
if (space.avatar_url) {
url = MatrixClientPeg.get().mxcUrlToHttp(
space.avatar_url,
Math.floor(24 * window.devicePixelRatio),
Math.floor(24 * window.devicePixelRatio),
"crop",
);
url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio));
}
return <div className="mx_SpaceRoomDirectory_subspace">
@ -265,12 +261,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
let url: string;
if (room.avatar_url) {
url = cli.mxcUrlToHttp(
room.avatar_url,
Math.floor(32 * window.devicePixelRatio),
Math.floor(32 * window.devicePixelRatio),
"crop",
);
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio));
}
const content = <React.Fragment>

View file

@ -1,5 +1,5 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,6 +18,8 @@ import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import BaseAvatar from './BaseAvatar';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import {ResizeMode} from "../../../customisations/models/ResizeMode";
export interface IProps {
groupId?: string;
@ -25,7 +27,7 @@ export interface IProps {
groupAvatarUrl?: string;
width?: number;
height?: number;
resizeMethod?: string;
resizeMethod?: ResizeMode;
onClick?: React.MouseEventHandler;
}
@ -38,8 +40,7 @@ export default class GroupAvatar extends React.Component<IProps> {
};
getGroupAvatarUrl() {
return MatrixClientPeg.get().mxcUrlToHttp(
this.props.groupAvatarUrl,
return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
this.props.width,
this.props.height,
this.props.resizeMethod,

View file

@ -21,6 +21,7 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
/*
* A dialog for confirming an operation on another user.
@ -108,8 +109,9 @@ export default class ConfirmUserActionDialog extends React.Component {
name = this.props.member.name;
userId = this.props.member.userId;
} else {
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
const httpAvatarUrl = this.props.groupMember.avatarUrl
? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
: null;
name = this.props.groupMember.displayname || this.props.groupMember.userId;
userId = this.props.groupMember.userId;
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;

View file

@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import FlairStore from "../../../stores/FlairStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
interface IProps extends IDialogProps {
communityId: string;
@ -118,7 +119,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
if (this.state.currentAvatarUrl) {
const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl);
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
} else {
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />

View file

@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
const PHASE_START = 0;
const PHASE_SHOW_SAS = 1;
@ -123,22 +124,21 @@ export default class IncomingSasDialog extends React.Component {
const Spinner = sdk.getComponent("views.elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId();
const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
let profile;
if (this.state.opponentProfile) {
const oppProfile = this.state.opponentProfile;
if (oppProfile) {
const url = oppProfile.avatar_url
? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio))
: null;
profile = <div className="mx_IncomingSasDialog_opponentProfile">
<BaseAvatar name={this.state.opponentProfile.displayname}
<BaseAvatar name={oppProfile.displayname}
idName={this.props.verifier.userId}
url={MatrixClientPeg.get().mxcUrlToHttp(
this.state.opponentProfile.avatar_url,
Math.floor(48 * window.devicePixelRatio),
Math.floor(48 * window.devicePixelRatio),
'crop',
)}
url={url}
width={48} height={48} resizeMethod='crop'
/>
<h2>{this.state.opponentProfile.displayname}</h2>
<h2>{oppProfile.displayname}</h2>
</div>;
} else if (this.state.opponentProfileError) {
profile = <div>

View file

@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import { UserAddressType } from '../../../UserAddress.js';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.elements.AddressTile")
export default class AddressTile extends React.Component {
@ -47,9 +48,7 @@ export default class AddressTile extends React.Component {
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
if (isMatrixAddress && address.avatarMxc) {
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
address.avatarMxc, 25, 25, 'crop',
));
imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25));
} else if (address.addressType === 'email') {
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
}

View file

@ -20,6 +20,7 @@ import FlairStore from '../../../stores/FlairStore';
import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
class FlairAvatar extends React.Component {
@ -39,8 +40,7 @@ class FlairAvatar extends React.Component {
}
render() {
const httpUrl = this.context.mxcUrlToHttp(
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16);
const tooltip = this.props.groupProfile.name ?
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
this.props.groupProfile.groupId;

View file

@ -26,6 +26,7 @@ import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import {mediaFromMxc} from "../../../customisations/Media";
import Tooltip from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@ -259,7 +260,7 @@ class Pill extends React.Component {
linkText = groupId;
if (this.props.shouldShowPillAvatar) {
avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
}
pillClass = 'mx_GroupPill';
}

View file

@ -24,6 +24,7 @@ import AccessibleButton from "./AccessibleButton";
import {_t} from "../../../languageHandler";
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import {mediaFromMxc} from "../../../customisations/Media";
interface ISSOButtonProps extends Omit<IProps, "flow"> {
idp: IIdentityProvider;
@ -72,7 +73,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
brandClass = `mx_SSOButton_brand_${brandName}`;
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
const src = mediaFromMxc(idp.icon).getSquareThumbnailHttp(24);
icon = <img src={src} height="24" width="24" alt={idp.name} />;
}

View file

@ -30,6 +30,7 @@ import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
import {mediaFromMxc} from "../../../customisations/Media";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
@ -130,11 +131,11 @@ export default class TagTile extends React.Component {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 32;
const avatarSize = 32;
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
const httpUrl = profile.avatarUrl
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarSize)
: null;
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
const className = classNames({
@ -180,8 +181,8 @@ export default class TagTile extends React.Component {
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarHeight}
height={avatarHeight}
width={avatarSize}
height={avatarSize}
/>
{contextButton}
{badgeElement}

View file

@ -27,6 +27,7 @@ import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/Contex
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
// XXX this class copies a lot from RoomTile.js
@replaceableComponent("views.groups.GroupInviteTile")
@ -117,8 +118,9 @@ export default class GroupInviteTile extends React.Component {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl ?
this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
const httpAvatarUrl = this.props.group.avatarUrl
? mediaFromMxc(this.props.group.avatarUrl).getSquareThumbnailHttp(24)
: null;
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;

View file

@ -23,6 +23,7 @@ import dis from '../../../dispatcher/dispatcher';
import { GroupMemberType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.groups.GroupMemberTile")
export default class GroupMemberTile extends React.Component {
@ -46,10 +47,9 @@ export default class GroupMemberTile extends React.Component {
const EntityTile = sdk.getComponent('rooms.EntityTile');
const name = this.props.member.displayname || this.props.member.userId;
const avatarUrl = this.context.mxcUrlToHttp(
this.props.member.avatarUrl,
36, 36, 'crop',
);
const avatarUrl = this.props.member.avatarUrl
? mediaFromMxc(this.props.member.avatarUrl).getSquareThumbnailHttp(36)
: null;
const av = (
<BaseAvatar

View file

@ -25,6 +25,7 @@ import GroupStore from '../../../stores/GroupStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.groups.GroupRoomInfo")
export default class GroupRoomInfo extends React.Component {
@ -204,10 +205,8 @@ export default class GroupRoomInfo extends React.Component {
const avatarUrl = this.state.groupRoom.avatarUrl;
let avatarElement;
if (avatarUrl) {
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = (<div className="mx_MemberInfo_avatar">
<img src={httpUrl} />
</div>);
const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800);
avatarElement = <div className="mx_MemberInfo_avatar"><img src={httpUrl} /></div>;
}
const groupRoomName = this.state.groupRoom.displayname;

View file

@ -21,6 +21,7 @@ import dis from '../../../dispatcher/dispatcher';
import { GroupRoomType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.groups.GroupRoomTile")
class GroupRoomTile extends React.Component {
@ -42,10 +43,9 @@ class GroupRoomTile extends React.Component {
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const avatarUrl = this.context.mxcUrlToHttp(
this.props.groupRoom.avatarUrl,
36, 36, 'crop',
);
const avatarUrl = this.props.groupRoom.avatarUrl
? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36)
: null;
const av = (
<BaseAvatar name={this.props.groupRoom.displayname}

View file

@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
function nop() {}
@ -73,8 +74,9 @@ class GroupTile extends React.Component {
const descElement = this.props.showDescription ?
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
<div />;
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
const httpUrl = profile.avatarUrl
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
: null;
let avatarElement = (
<div className="mx_GroupTile_avatar">

View file

@ -22,6 +22,7 @@ import { decryptFile } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
import InlineSpinner from '../elements/InlineSpinner';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromContent} from "../../../customisations/Media";
@replaceableComponent("views.messages.MAudioBody")
export default class MAudioBody extends React.Component {
@ -41,11 +42,11 @@ export default class MAudioBody extends React.Component {
}
_getContentUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
return media.srcHttp;
}
}

View file

@ -27,6 +27,7 @@ import request from 'browser-request';
import Modal from '../../../Modal';
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromContent} from "../../../customisations/Media";
// A cached tinted copy of require("../../../../res/img/download.svg")
@ -178,8 +179,8 @@ export default class MFileBody extends React.Component {
}
_getContentUrl() {
const content = this.props.mxEvent.getContent();
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
const media = mediaFromContent(this.props.mxEvent.getContent());
return media.srcHttp;
}
componentDidMount() {

View file

@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import InlineSpinner from '../elements/InlineSpinner';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromContent} from "../../../customisations/Media";
@replaceableComponent("views.messages.MImageBody")
export default class MImageBody extends React.Component {
@ -167,16 +168,16 @@ export default class MImageBody extends React.Component {
}
_getContentUrl() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) {
return this.state.decryptedUrl;
} else {
return this.context.mxcUrlToHttp(content.url);
return media.srcHttp;
}
}
_getThumbUrl() {
// FIXME: the dharma skin lets images grow as wide as you like, rather than capped to 800x600.
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
// thumbnail resolution will be unnecessarily reduced.
// custom timeline widths seems preferable.
@ -185,21 +186,19 @@ export default class MImageBody extends React.Component {
const thumbHeight = Math.round(600 * pixelRatio);
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
const media = mediaFromContent(content);
if (media.isEncrypted) {
// Don't use the thumbnail for clients wishing to autoplay gifs.
if (this.state.decryptedThumbnailUrl) {
return this.state.decryptedThumbnailUrl;
}
return this.state.decryptedUrl;
} else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
} else if (content.info && content.info.mimetype === "image/svg+xml" && media.hasThumbnail) {
// special case to return clientside sender-generated thumbnails for SVGs, if any,
// given we deliberately don't thumbnail them serverside to prevent
// billion lol attacks and similar
return this.context.mxcUrlToHttp(
content.info.thumbnail_url,
thumbWidth,
thumbHeight,
);
return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale');
} else {
// we try to download the correct resolution
// for hi-res images (like retina screenshots).
@ -218,7 +217,7 @@ export default class MImageBody extends React.Component {
pixelRatio === 1.0 ||
(!info || !info.w || !info.h || !info.size)
) {
return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
} else {
// we should only request thumbnails if the image is bigger than 800x600
// (or 1600x1200 on retina) otherwise the image in the timeline will just
@ -233,24 +232,17 @@ export default class MImageBody extends React.Component {
info.w > thumbWidth ||
info.h > thumbHeight
);
const isLargeFileSize = info.size > 1*1024*1024;
const isLargeFileSize = info.size > 1*1024*1024; // 1mb
if (isLargeFileSize && isLargerThanThumbnail) {
// image is too large physically and bytewise to clutter our timeline so
// we ask for a thumbnail, despite knowing that it will be max 800x600
// despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
return this.context.mxcUrlToHttp(
content.url,
thumbWidth,
thumbHeight,
);
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
} else {
// download the original image otherwise, so we can scale it client side
// to take pixelRatio into account.
// ( no width/height means we want the original image)
return this.context.mxcUrlToHttp(
content.url,
);
return media.srcHttp;
}
}
}

View file

@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import InlineSpinner from '../elements/InlineSpinner';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromContent} from "../../../customisations/Media";
interface IProps {
/* the MatrixEvent to show */
@ -76,11 +77,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
}
private getContentUrl(): string|null {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
return media.srcHttp;
}
}
@ -91,10 +92,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
private getThumbUrl(): string|null {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
const media = mediaFromContent(content);
if (media.isEncrypted) {
return this.state.decryptedThumbnailUrl;
} else if (content.info && content.info.thumbnail_url) {
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
} else if (media.hasThumbnail) {
return media.thumbnailHttp;
} else {
return null;
}

View file

@ -24,6 +24,7 @@ import * as sdk from '../../../index';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.messages.RoomAvatarEvent")
export default class RoomAvatarEvent extends React.Component {
@ -35,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component {
onAvatarClick = () => {
const cli = MatrixClientPeg.get();
const ev = this.props.mxEvent;
const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {

View file

@ -63,6 +63,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import {mediaFromMxc} from "../../../customisations/Media";
interface IDevice {
deviceId: string;
@ -1408,7 +1409,7 @@ const UserInfoHeader: React.FC<{
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
if (!avatarUrl) return;
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
const params = {
src: httpUrl,
name: member.name,

View file

@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import * as sdk from "../../../index";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
// TODO: Merge with ProfileSettings?
@replaceableComponent("views.room_settings.RoomProfileSettings")
@ -38,7 +39,7 @@ export default class RoomProfileSettings extends React.Component {
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96);
const topicEvent = room.currentState.getStateEvents("m.room.topic", "");
const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : '';
@ -112,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
if (this.state.avatarFile) {
const uri = await client.uploadContent(this.state.avatarFile);
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, '');
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
newState.originalAvatarUrl = newState.avatarUrl;
newState.avatarFile = null;
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {

View file

@ -26,6 +26,7 @@ import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils";
import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.rooms.LinkPreviewWidget")
export default class LinkPreviewWidget extends React.Component {
@ -83,7 +84,7 @@ export default class LinkPreviewWidget extends React.Component {
let src = p["og:image"];
if (src && src.startsWith("mxc://")) {
src = MatrixClientPeg.get().mxcUrlToHttp(src);
src = mediaFromMxc(src).srcHttp;
}
const params = {
@ -109,9 +110,11 @@ export default class LinkPreviewWidget extends React.Component {
if (!SettingsStore.getValue("showImages")) {
image = null; // Don't render a button to show the image, just hide it outright
}
const imageMaxWidth = 100; const imageMaxHeight = 100;
const imageMaxWidth = 100;
const imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
// We deliberately don't want a square here, so use the source HTTP thumbnail function
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale');
}
let thumbHeight = imageMaxHeight;

View file

@ -21,6 +21,7 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Spinner from '../elements/Spinner';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.settings.ChangeAvatar")
export default class ChangeAvatar extends React.Component {
@ -117,7 +118,7 @@ export default class ChangeAvatar extends React.Component {
httpPromise.then(function() {
self.setState({
phase: ChangeAvatar.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),
avatarUrl: mediaFromMxc(newUrl).srcHttp,
});
}, function(error) {
self.setState({

View file

@ -24,6 +24,7 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
@replaceableComponent("views.settings.ProfileSettings")
export default class ProfileSettings extends React.Component {
@ -32,7 +33,7 @@ export default class ProfileSettings extends React.Component {
const client = MatrixClientPeg.get();
let avatarUrl = OwnProfileStore.instance.avatarMxc;
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96);
this.state = {
userId: client.getUserId(),
originalDisplayName: OwnProfileStore.instance.displayName,
@ -97,7 +98,7 @@ export default class ProfileSettings extends React.Component {
` (${this.state.avatarFile.size}) bytes`);
const uri = await client.uploadContent(this.state.avatarFile);
await client.setAvatarUrl(uri);
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
newState.originalAvatarUrl = newState.avatarUrl;
newState.avatarFile = null;
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {

View file

@ -16,6 +16,7 @@
import {MatrixClientPeg} from "../MatrixClientPeg";
import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
import {ResizeMode} from "./models/ResizeMode";
// Populate this class with the details of your customisations when copying it.
@ -33,6 +34,13 @@ export class Media {
constructor(private prepared: IPreparedMedia) {
}
/**
* True if the media appears to be encrypted. Actual file contents may vary.
*/
public get isEncrypted(): boolean {
return !!this.prepared.file;
}
/**
* The MXC URI of the source media.
*/
@ -62,6 +70,15 @@ export class Media {
return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc);
}
/**
* The HTTP URL for the thumbnail media (without any specified width, height, etc). Null/undefined
* if no thumbnail media recorded.
*/
public get thumbnailHttp(): string | undefined | null {
if (!this.hasThumbnail) return null;
return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc);
}
/**
* Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail
* is recorded for this media. Returns null/undefined otherwise.
@ -70,7 +87,7 @@ export class Media {
* @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
* @returns {string} The HTTP URL which points to the thumbnail.
*/
public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined {
public getThumbnailHttp(width: number, height: number, mode: ResizeMode = "scale"): string | null | undefined {
if (!this.hasThumbnail) return null;
return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode);
}
@ -82,10 +99,23 @@ export class Media {
* @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
* @returns {string} The HTTP URL which points to the thumbnail.
*/
public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string {
public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMode = "scale"): string {
return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode);
}
/**
* Creates a square thumbnail of the media. If the media has a thumbnail recorded, that MXC will
* be used, otherwise the source media will be used.
* @param {number} dim The desired width and height.
* @returns {string} An HTTP URL for the thumbnail.
*/
public getSquareThumbnailHttp(dim: number): string {
if (this.hasThumbnail) {
return this.getThumbnailHttp(dim, dim, 'crop');
}
return this.getThumbnailOfSourceHttp(dim, dim, 'crop');
}
/**
* Downloads the source media.
* @returns {Promise<Response>} Resolves to the server's response for chaining.
@ -102,7 +132,7 @@ export class Media {
* @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
* @returns {Promise<Response>} Resolves to the server's response for chaining.
*/
public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise<Response> {
public downloadThumbnail(width: number, height: number, mode: ResizeMode = "scale"): Promise<Response> {
if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail");
return fetch(this.getThumbnailHttp(width, height, mode));
}
@ -114,7 +144,7 @@ export class Media {
* @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
* @returns {Promise<Response>} Resolves to the server's response for chaining.
*/
public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise<Response> {
public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMode = "scale"): Promise<Response> {
return fetch(this.getThumbnailOfSourceHttp(width, height, mode));
}
}

View file

@ -0,0 +1,17 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type ResizeMode = "scale" | "crop";

View file

@ -22,6 +22,7 @@ import { User } from "matrix-js-sdk/src/models/user";
import { throttle } from "lodash";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { _t } from "../languageHandler";
import {mediaFromMxc} from "../customisations/Media";
interface IState {
displayName?: string;
@ -72,8 +73,12 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
*/
public getHttpAvatarUrl(size = 0): string {
if (!this.avatarMxc) return null;
const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
const media = mediaFromMxc(this.avatarMxc);
if (!size || size <= 0) {
return media.srcHttp;
} else {
return media.getSquareThumbnailHttp(size);
}
}
protected async onNotReady() {