= ({
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
@@ -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 =
diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index a033257871..dc363da304 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -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 {
};
getGroupAvatarUrl() {
- return MatrixClientPeg.get().mxcUrlToHttp(
- this.props.groupAvatarUrl,
+ return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
this.props.width,
this.props.height,
this.props.resizeMethod,
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js
index 8827f161f1..8cfd28986b 100644
--- a/src/components/views/dialogs/ConfirmUserActionDialog.js
+++ b/src/components/views/dialogs/ConfirmUserActionDialog.js
@@ -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 = ;
diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
index 504d563bd9..ee3696b427 100644
--- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
@@ -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;
if (!this.state.avatarPreview) {
if (this.state.currentAvatarUrl) {
- const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl);
+ const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
preview =
;
} else {
preview =
diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js
index d65ec7563f..f18b7a9d0c 100644
--- a/src/components/views/dialogs/IncomingSasDialog.js
+++ b/src/components/views/dialogs/IncomingSasDialog.js
@@ -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 =
-
-
{this.state.opponentProfile.displayname}
+ {oppProfile.displayname}
;
} else if (this.state.opponentProfileError) {
profile =
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index 4a216dbae4..4f5ee45a3c 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -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"));
}
diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js
index 75998cb721..73d5b91511 100644
--- a/src/components/views/elements/Flair.js
+++ b/src/components/views/elements/Flair.js
@@ -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;
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index b0d4fc7fa2..bf99ee6078 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -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 =
;
+ url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
}
pillClass = 'mx_GroupPill';
}
diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx
index 3a03252ebd..4e41db0ae7 100644
--- a/src/components/views/elements/SSOButtons.tsx
+++ b/src/components/views/elements/SSOButtons.tsx
@@ -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
{
idp: IIdentityProvider;
@@ -72,7 +73,7 @@ const SSOButton: React.FC = ({
brandClass = `mx_SSOButton_brand_${brandName}`;
icon =
;
} 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 =
;
}
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index 663acd6329..03d853babc 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -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}
diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js
index dc48c01acb..bc0bf966f9 100644
--- a/src/components/views/groups/GroupInviteTile.js
+++ b/src/components/views/groups/GroupInviteTile.js
@@ -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 = ;
diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js
index e8285803b0..a436f2403e 100644
--- a/src/components/views/groups/GroupMemberTile.js
+++ b/src/components/views/groups/GroupMemberTile.js
@@ -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 = (
-
- );
+ const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800);
+ avatarElement = 
;
}
const groupRoomName = this.state.groupRoom.displayname;
diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js
index 8b25437f71..7edfc1a376 100644
--- a/src/components/views/groups/GroupRoomTile.js
+++ b/src/components/views/groups/GroupRoomTile.js
@@ -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 = (
{ profile.shortDescription } :
;
- 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 = (
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index 498e2db12a..78ded9a514 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -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;
}
}
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index e9893f99b6..07d7beb793 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -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() {
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 59c5b4e66b..0a1f875935 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -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;
}
}
}
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index 89985dee7d..32b071ea24 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -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 {
}
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 {
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;
}
diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js
index ba860216f0..00aaf9bfda 100644
--- a/src/components/views/messages/RoomAvatarEvent.js
+++ b/src/components/views/messages/RoomAvatarEvent.js
@@ -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', {
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index eb47a56269..d415d19852 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -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,
diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index 563368384b..3dbe2b2b7f 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -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) {
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index 39c9f0bcf7..536abf57fc 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -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;
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index 8067046ffd..0b6739df64 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -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({
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 30dcdc3c47..971b868751 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -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) {
diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index 27abc6bc50..f42307c530 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -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} 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} Resolves to the server's response for chaining.
*/
- public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise {
+ public downloadThumbnail(width: number, height: number, mode: ResizeMode = "scale"): Promise {
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} Resolves to the server's response for chaining.
*/
- public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise {
+ public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMode = "scale"): Promise {
return fetch(this.getThumbnailOfSourceHttp(width, height, mode));
}
}
diff --git a/src/customisations/models/ResizeMode.ts b/src/customisations/models/ResizeMode.ts
new file mode 100644
index 0000000000..401b6723e5
--- /dev/null
+++ b/src/customisations/models/ResizeMode.ts
@@ -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";
diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts
index 8983380fec..5e722877e2 100644
--- a/src/stores/OwnProfileStore.ts
+++ b/src/stores/OwnProfileStore.ts
@@ -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 {
*/
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() {