From 1ac12479ca7b132b9d9d4eddc64ed6aa9b7fbde0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 3 Mar 2021 19:06:46 -0700 Subject: [PATCH] Convert cases of mxcUrlToHttp to new media customisation --- src/HtmlUtils.tsx | 9 ++--- src/Notifier.ts | 3 +- src/autocomplete/CommunityProvider.tsx | 3 +- src/components/structures/GroupView.js | 11 +++--- src/components/structures/LeftPanel.tsx | 3 +- .../structures/SpaceRoomDirectory.tsx | 15 ++------ src/components/views/avatars/GroupAvatar.tsx | 9 +++-- .../views/dialogs/ConfirmUserActionDialog.js | 6 ++- .../dialogs/EditCommunityPrototypeDialog.tsx | 3 +- .../views/dialogs/IncomingSasDialog.js | 20 +++++----- src/components/views/elements/AddressTile.js | 5 +-- src/components/views/elements/Flair.js | 4 +- src/components/views/elements/Pill.js | 3 +- src/components/views/elements/SSOButtons.tsx | 3 +- src/components/views/elements/TagTile.js | 13 ++++--- .../views/groups/GroupInviteTile.js | 6 ++- .../views/groups/GroupMemberTile.js | 8 ++-- src/components/views/groups/GroupRoomInfo.js | 7 ++-- src/components/views/groups/GroupRoomTile.js | 8 ++-- src/components/views/groups/GroupTile.js | 6 ++- src/components/views/messages/MAudioBody.js | 7 ++-- src/components/views/messages/MFileBody.js | 5 ++- src/components/views/messages/MImageBody.js | 36 +++++++----------- src/components/views/messages/MVideoBody.tsx | 14 ++++--- .../views/messages/RoomAvatarEvent.js | 3 +- src/components/views/right_panel/UserInfo.tsx | 3 +- .../room_settings/RoomProfileSettings.js | 5 ++- .../views/rooms/LinkPreviewWidget.js | 9 +++-- src/components/views/settings/ChangeAvatar.js | 3 +- .../views/settings/ProfileSettings.js | 5 ++- src/customisations/Media.ts | 38 +++++++++++++++++-- src/customisations/models/ResizeMode.ts | 17 +++++++++ src/stores/OwnProfileStore.ts | 9 ++++- 33 files changed, 178 insertions(+), 121 deletions(-) create mode 100644 src/customisations/models/ResizeMode.ts diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 7d6b049914..12752eb20f 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -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) { diff --git a/src/Notifier.ts b/src/Notifier.ts index 6460be20ad..f68bfabc18 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -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, diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index ebf5d536ec..b7a4e0960e 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -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} /> ), range, diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index b4b871a0b4..f05d8d0758 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -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 = { name }; - 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 ? ; } - 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; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 88c7a71b35..f7865d094a 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -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 { 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})`; diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 72e52678b6..9ee16558d3 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -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 = ({ 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 =
); + 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() {