mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-17 05:35:04 +08:00
Merge pull request #5714 from matrix-org/travis/media-customization
Support a media handling customisation endpoint
This commit is contained in:
commit
d3541b78eb
19
docs/media-handling.md
Normal file
19
docs/media-handling.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Media handling
|
||||||
|
|
||||||
|
Surely media should be as easy as just putting a URL into an `img` and calling it good, right?
|
||||||
|
Not quite. Matrix uses something called a Matrix Content URI (better known as MXC URI) to identify
|
||||||
|
content, which is then converted to a regular HTTPS URL on the homeserver. However, sometimes that
|
||||||
|
URL can change depending on deployment considerations.
|
||||||
|
|
||||||
|
The react-sdk features a [customisation endpoint](https://github.com/vector-im/element-web/blob/develop/docs/customisations.md)
|
||||||
|
for media handling where all conversions from MXC URI to HTTPS URL happen. This is to ensure that
|
||||||
|
those obscure deployments can route all their media to the right place.
|
||||||
|
|
||||||
|
For development, there are currently two functions available: `mediaFromMxc` and `mediaFromContent`.
|
||||||
|
The `mediaFromMxc` function should be self-explanatory. `mediaFromContent` takes an event content as
|
||||||
|
a parameter and will automatically parse out the source media and thumbnail. Both functions return
|
||||||
|
a `Media` object with a number of options on it, such as getting various common HTTPS URLs for the
|
||||||
|
media.
|
||||||
|
|
||||||
|
**It is extremely important that all media calls are put through this customisation endpoint.** So
|
||||||
|
much so it's a lint rule to avoid accidental use of the wrong functions.
|
@ -157,6 +157,7 @@
|
|||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.0",
|
||||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||||
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"matrix-mock-request": "^1.2.3",
|
"matrix-mock-request": "^1.2.3",
|
||||||
"matrix-react-test-utils": "^0.2.2",
|
"matrix-react-test-utils": "^0.2.2",
|
||||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -16,6 +16,19 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_MFileBody_download {
|
.mx_MFileBody_download {
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
|
|
||||||
|
.mx_MFileBody_download_icon {
|
||||||
|
// 12px instead of 14px to better match surrounding font size
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
mask-size: 12px;
|
||||||
|
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url("$(res)/img/download.svg");
|
||||||
|
background-color: $accent-color;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MFileBody_download a {
|
.mx_MFileBody_download a {
|
||||||
|
@ -14,27 +14,23 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||||
import {User} from "matrix-js-sdk/src/models/user";
|
import {User} from "matrix-js-sdk/src/models/user";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
|
import {mediaFromMxc} from "./customisations/Media";
|
||||||
|
|
||||||
export type ResizeMethod = "crop" | "scale";
|
export type ResizeMethod = "crop" | "scale";
|
||||||
|
|
||||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||||
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
||||||
let url: string;
|
let url: string;
|
||||||
if (member && member.getAvatarUrl) {
|
if (member?.getMxcAvatarUrl()) {
|
||||||
url = member.getAvatarUrl(
|
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
|
||||||
Math.floor(width * window.devicePixelRatio),
|
Math.floor(width * window.devicePixelRatio),
|
||||||
Math.floor(height * window.devicePixelRatio),
|
Math.floor(height * window.devicePixelRatio),
|
||||||
resizeMethod,
|
resizeMethod,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@ -47,16 +43,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||||
const url = getHttpUriForMxc(
|
if (!user.avatarUrl) return null;
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(
|
||||||
Math.floor(width * window.devicePixelRatio),
|
Math.floor(width * window.devicePixelRatio),
|
||||||
Math.floor(height * window.devicePixelRatio),
|
Math.floor(height * window.devicePixelRatio),
|
||||||
resizeMethod,
|
resizeMethod,
|
||||||
);
|
);
|
||||||
if (!url || url.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidHexColor(color: string): boolean {
|
function isValidHexColor(color: string): boolean {
|
||||||
@ -154,15 +146,8 @@ export function getInitialLetter(name: string): string {
|
|||||||
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||||
if (!room) return null; // null-guard
|
if (!room) return null; // null-guard
|
||||||
|
|
||||||
const explicitRoomAvatar = room.getAvatarUrl(
|
if (room.getMxcAvatarUrl()) {
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
width,
|
|
||||||
height,
|
|
||||||
resizeMethod,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
if (explicitRoomAvatar) {
|
|
||||||
return explicitRoomAvatar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// space rooms cannot be DMs so skip the rest
|
// space rooms cannot be DMs so skip the rest
|
||||||
@ -177,14 +162,8 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
|||||||
// then still try to show any avatar (pref. other member)
|
// then still try to show any avatar (pref. other member)
|
||||||
otherMember = room.getAvatarFallbackMember();
|
otherMember = room.getAvatarFallbackMember();
|
||||||
}
|
}
|
||||||
if (otherMember) {
|
if (otherMember?.getMxcAvatarUrl()) {
|
||||||
return otherMember.getAvatarUrl(
|
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
resizeMethod,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -32,10 +32,10 @@ import { AllHtmlEntities } from 'html-entities';
|
|||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import cheerio from 'cheerio';
|
import cheerio from 'cheerio';
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
|
||||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
|
import {mediaFromMxc} from "./customisations/Media";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
@ -181,11 +181,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
|||||||
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
||||||
return { tagName, attribs: {}};
|
return { tagName, attribs: {}};
|
||||||
}
|
}
|
||||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
const width = Number(attribs.width) || 800;
|
||||||
attribs.src,
|
const height = Number(attribs.height) || 600;
|
||||||
attribs.width || 800,
|
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
|
||||||
attribs.height || 600,
|
|
||||||
);
|
|
||||||
return { tagName, attribs };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||||
|
@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel";
|
|||||||
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
|
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
import UserActivity from "./UserActivity";
|
import UserActivity from "./UserActivity";
|
||||||
|
import {mediaFromMxc} from "./customisations/Media";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
@ -150,7 +151,7 @@ export const Notifier = {
|
|||||||
// Ideally in here we could use MSC1310 to detect the type of file, and reject it.
|
// Ideally in here we could use MSC1310 to detect the type of file, and reject it.
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: MatrixClientPeg.get().mxcUrlToHttp(content.url),
|
url: mediaFromMxc(content.url).srcHttp,
|
||||||
name: content.name,
|
name: content.name,
|
||||||
type: content.type,
|
type: content.type,
|
||||||
size: content.size,
|
size: content.size,
|
||||||
|
@ -27,6 +27,7 @@ import {sortBy} from "lodash";
|
|||||||
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
import FlairStore from "../stores/FlairStore";
|
import FlairStore from "../stores/FlairStore";
|
||||||
|
import {mediaFromMxc} from "../customisations/Media";
|
||||||
|
|
||||||
const COMMUNITY_REGEX = /\B\+\S*/g;
|
const COMMUNITY_REGEX = /\B\+\S*/g;
|
||||||
|
|
||||||
@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
|||||||
name={name || groupId}
|
name={name || groupId}
|
||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
|
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} />
|
||||||
</PillCompletion>
|
</PillCompletion>
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
|
@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk";
|
|||||||
import {allSettled, sleep} from "../../utils/promise";
|
import {allSettled, sleep} from "../../utils/promise";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
const LONG_DESC_PLACEHOLDER = _td(
|
const LONG_DESC_PLACEHOLDER = _td(
|
||||||
@ -368,8 +369,7 @@ class FeaturedUser extends React.Component {
|
|||||||
|
|
||||||
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
|
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
|
||||||
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||||
const httpUrl = MatrixClientPeg.get()
|
const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64);
|
||||||
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
|
||||||
|
|
||||||
const deleteButton = this.props.editing ?
|
const deleteButton = this.props.editing ?
|
||||||
<img
|
<img
|
||||||
@ -981,10 +981,9 @@ export default class GroupView extends React.Component {
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const httpInviterAvatar = this.state.inviterProfile ?
|
const httpInviterAvatar = this.state.inviterProfile
|
||||||
this._matrixClient.mxcUrlToHttp(
|
? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
|
||||||
this.state.inviterProfile.avatarUrl, 36, 36,
|
: null;
|
||||||
) : null;
|
|
||||||
|
|
||||||
const inviter = group.inviter || {};
|
const inviter = group.inviter || {};
|
||||||
let inviterName = inviter.userId;
|
let inviterName = inviter.userId;
|
||||||
|
@ -36,11 +36,11 @@ import {Key} from "../../Keyboard";
|
|||||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|
||||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||||
import LeftPanelWidget from "./LeftPanelWidget";
|
import LeftPanelWidget from "./LeftPanelWidget";
|
||||||
import SpacePanel from "../views/spaces/SpacePanel";
|
import SpacePanel from "../views/spaces/SpacePanel";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
@ -121,7 +121,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||||||
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
||||||
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
||||||
if (settingBgMxc) {
|
if (settingBgMxc) {
|
||||||
avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize);
|
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarUrlProp = `url(${avatarUrl})`;
|
const avatarUrlProp = `url(${avatarUrl})`;
|
||||||
|
@ -27,7 +27,6 @@ import { _t } from '../../languageHandler';
|
|||||||
import SdkConfig from '../../SdkConfig';
|
import SdkConfig from '../../SdkConfig';
|
||||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||||
import Analytics from '../../Analytics';
|
import Analytics from '../../Analytics';
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||||
@ -35,6 +34,7 @@ import GroupStore from "../../stores/GroupStore";
|
|||||||
import FlairStore from "../../stores/FlairStore";
|
import FlairStore from "../../stores/FlairStore";
|
||||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
|
|
||||||
const MAX_NAME_LENGTH = 80;
|
const MAX_NAME_LENGTH = 80;
|
||||||
const MAX_TOPIC_LENGTH = 800;
|
const MAX_TOPIC_LENGTH = 800;
|
||||||
@ -521,10 +521,9 @@ export default class RoomDirectory extends React.Component {
|
|||||||
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
|
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
|
||||||
}
|
}
|
||||||
topic = linkifyAndSanitizeHtml(topic);
|
topic = linkifyAndSanitizeHtml(topic);
|
||||||
const avatarUrl = getHttpUriForMxc(
|
let avatarUrl = null;
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
||||||
room.avatar_url, 32, 32, "crop",
|
|
||||||
);
|
|
||||||
return [
|
return [
|
||||||
<div key={ `${room.room_id}_avatar` }
|
<div key={ `${room.room_id}_avatar` }
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||||
|
@ -34,6 +34,7 @@ import {EnhancedMap} from "../../utils/maps";
|
|||||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||||
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
@ -158,12 +159,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
|
|||||||
|
|
||||||
let url: string;
|
let url: string;
|
||||||
if (space.avatar_url) {
|
if (space.avatar_url) {
|
||||||
url = MatrixClientPeg.get().mxcUrlToHttp(
|
url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio));
|
||||||
space.avatar_url,
|
|
||||||
Math.floor(24 * window.devicePixelRatio),
|
|
||||||
Math.floor(24 * window.devicePixelRatio),
|
|
||||||
"crop",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_SpaceRoomDirectory_subspace">
|
return <div className="mx_SpaceRoomDirectory_subspace">
|
||||||
@ -265,12 +261,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
|
|||||||
|
|
||||||
let url: string;
|
let url: string;
|
||||||
if (room.avatar_url) {
|
if (room.avatar_url) {
|
||||||
url = cli.mxcUrlToHttp(
|
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio));
|
||||||
room.avatar_url,
|
|
||||||
Math.floor(32 * window.devicePixelRatio),
|
|
||||||
Math.floor(32 * window.devicePixelRatio),
|
|
||||||
"crop",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = <React.Fragment>
|
const content = <React.Fragment>
|
||||||
|
@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
|
|||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import {toPx} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
|
import {ResizeMethod} from "../../../Avatar";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
name: string; // The name (first initial used as default)
|
name: string; // The name (first initial used as default)
|
||||||
@ -35,7 +36,7 @@ interface IProps {
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
// XXX: resizeMethod not actually used.
|
// XXX: resizeMethod not actually used.
|
||||||
resizeMethod?: string;
|
resizeMethod?: ResizeMethod;
|
||||||
defaultToInitialLetter?: boolean; // true to add default url
|
defaultToInitialLetter?: boolean; // true to add default url
|
||||||
onClick?: React.MouseEventHandler;
|
onClick?: React.MouseEventHandler;
|
||||||
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
||||||
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -15,9 +15,10 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|
||||||
import BaseAvatar from './BaseAvatar';
|
import BaseAvatar from './BaseAvatar';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
import {ResizeMethod} from "../../../Avatar";
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
@ -25,7 +26,7 @@ export interface IProps {
|
|||||||
groupAvatarUrl?: string;
|
groupAvatarUrl?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
resizeMethod?: string;
|
resizeMethod?: ResizeMethod;
|
||||||
onClick?: React.MouseEventHandler;
|
onClick?: React.MouseEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,8 +39,8 @@ export default class GroupAvatar extends React.Component<IProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getGroupAvatarUrl() {
|
getGroupAvatarUrl() {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(
|
if (!this.props.groupAvatarUrl) return null;
|
||||||
this.props.groupAvatarUrl,
|
return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
|
||||||
this.props.width,
|
this.props.width,
|
||||||
this.props.height,
|
this.props.height,
|
||||||
this.props.resizeMethod,
|
this.props.resizeMethod,
|
||||||
|
@ -20,16 +20,17 @@ import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
|||||||
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import BaseAvatar from "./BaseAvatar";
|
import BaseAvatar from "./BaseAvatar";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
import {ResizeMethod} from "../../../Avatar";
|
||||||
|
|
||||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
fallbackUserId?: string;
|
fallbackUserId?: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
resizeMethod?: string;
|
resizeMethod?: ResizeMethod;
|
||||||
// The onClick to give the avatar
|
// The onClick to give the avatar
|
||||||
onClick?: React.MouseEventHandler;
|
onClick?: React.MouseEventHandler;
|
||||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||||
@ -63,18 +64,19 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static getState(props: IProps): IState {
|
private static getState(props: IProps): IState {
|
||||||
if (props.member && props.member.name) {
|
if (props.member?.name) {
|
||||||
return {
|
let imageUrl = null;
|
||||||
name: props.member.name,
|
if (props.member.getMxcAvatarUrl()) {
|
||||||
title: props.title || props.member.userId,
|
imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
|
||||||
imageUrl: props.member.getAvatarUrl(
|
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
false,
|
);
|
||||||
false,
|
}
|
||||||
),
|
return {
|
||||||
|
name: props.member.name,
|
||||||
|
title: props.title || props.member.userId,
|
||||||
|
imageUrl: imageUrl,
|
||||||
};
|
};
|
||||||
} else if (props.fallbackUserId) {
|
} else if (props.fallbackUserId) {
|
||||||
return {
|
return {
|
||||||
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
import React, {ComponentProps} from 'react';
|
import React, {ComponentProps} from 'react';
|
||||||
import Room from 'matrix-js-sdk/src/models/room';
|
import Room from 'matrix-js-sdk/src/models/room';
|
||||||
import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
|
|
||||||
|
|
||||||
import BaseAvatar from './BaseAvatar';
|
import BaseAvatar from './BaseAvatar';
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
@ -24,6 +23,7 @@ import Modal from '../../../Modal';
|
|||||||
import * as Avatar from '../../../Avatar';
|
import * as Avatar from '../../../Avatar';
|
||||||
import {ResizeMethod} from "../../../Avatar";
|
import {ResizeMethod} from "../../../Avatar";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
@ -90,16 +90,16 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static getImageUrls(props: IProps): string[] {
|
private static getImageUrls(props: IProps): string[] {
|
||||||
return [
|
let oobAvatar = null;
|
||||||
getHttpUriForMxc(
|
if (props.oobData.avatarUrl) {
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
|
||||||
// Default props don't play nicely with getDerivedStateFromProps
|
|
||||||
//props.oobData !== undefined ? props.oobData.avatarUrl : {},
|
|
||||||
props.oobData.avatarUrl,
|
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
), // highest priority
|
);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
oobAvatar, // highest priority
|
||||||
RoomAvatar.getRoomAvatarUrl(props),
|
RoomAvatar.getRoomAvatarUrl(props),
|
||||||
].filter(function(url) {
|
].filter(function(url) {
|
||||||
return (url !== null && url !== "");
|
return (url !== null && url !== "");
|
||||||
|
@ -14,21 +14,18 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {ComponentProps, useContext} from 'react';
|
import React, {ComponentProps} from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|
||||||
import {IApp} from "../../../stores/WidgetStore";
|
import {IApp} from "../../../stores/WidgetStore";
|
||||||
import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
|
import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
|
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
|
||||||
app: IApp;
|
app: IApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
|
const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
|
||||||
|
|
||||||
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
|
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
|
||||||
// heuristics for some better icons until Widgets support their own icons
|
// heuristics for some better icons until Widgets support their own icons
|
||||||
if (app.type.includes("jitsi")) {
|
if (app.type.includes("jitsi")) {
|
||||||
@ -47,7 +44,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
|
|||||||
name={app.id}
|
name={app.id}
|
||||||
className={classNames("mx_WidgetAvatar", className)}
|
className={classNames("mx_WidgetAvatar", className)}
|
||||||
// MSC2765
|
// MSC2765
|
||||||
url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined}
|
url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined}
|
||||||
urls={iconUrls}
|
urls={iconUrls}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
|
@ -26,12 +26,12 @@ import SdkConfig from "../../../SdkConfig";
|
|||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import InviteDialog from "./InviteDialog";
|
import InviteDialog from "./InviteDialog";
|
||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
|
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "./ErrorDialog";
|
import ErrorDialog from "./ErrorDialog";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@ -142,12 +142,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
|||||||
|
|
||||||
private renderPerson(person: IPerson, key: any) {
|
private renderPerson(person: IPerson, key: any) {
|
||||||
const avatarSize = 36;
|
const avatarSize = 36;
|
||||||
|
let avatarUrl = null;
|
||||||
|
if (person.user.getMxcAvatarUrl()) {
|
||||||
|
avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
|
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
url={getHttpUriForMxc(
|
url={avatarUrl}
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
|
|
||||||
avatarSize, avatarSize, "crop")}
|
|
||||||
name={person.user.name}
|
name={person.user.name}
|
||||||
idName={person.user.userId}
|
idName={person.user.userId}
|
||||||
width={avatarSize}
|
width={avatarSize}
|
||||||
|
@ -21,6 +21,7 @@ import * as sdk from '../../../index';
|
|||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { GroupMemberType } from '../../../groups';
|
import { GroupMemberType } from '../../../groups';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for confirming an operation on another user.
|
* 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;
|
name = this.props.member.name;
|
||||||
userId = this.props.member.userId;
|
userId = this.props.member.userId;
|
||||||
} else {
|
} else {
|
||||||
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
|
const httpAvatarUrl = this.props.groupMember.avatarUrl
|
||||||
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
|
? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
|
||||||
|
: null;
|
||||||
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||||
userId = this.props.groupMember.userId;
|
userId = this.props.groupMember.userId;
|
||||||
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||||
|
@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|||||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import FlairStore from "../../../stores/FlairStore";
|
import FlairStore from "../../../stores/FlairStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
communityId: string;
|
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" />;
|
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||||
if (!this.state.avatarPreview) {
|
if (!this.state.avatarPreview) {
|
||||||
if (this.state.currentAvatarUrl) {
|
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" />;
|
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||||
} else {
|
} else {
|
||||||
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />
|
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />
|
||||||
|
@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
const PHASE_START = 0;
|
const PHASE_START = 0;
|
||||||
const PHASE_SHOW_SAS = 1;
|
const PHASE_SHOW_SAS = 1;
|
||||||
@ -123,22 +124,21 @@ export default class IncomingSasDialog extends React.Component {
|
|||||||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
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;
|
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">
|
profile = <div className="mx_IncomingSasDialog_opponentProfile">
|
||||||
<BaseAvatar name={this.state.opponentProfile.displayname}
|
<BaseAvatar name={oppProfile.displayname}
|
||||||
idName={this.props.verifier.userId}
|
idName={this.props.verifier.userId}
|
||||||
url={MatrixClientPeg.get().mxcUrlToHttp(
|
url={url}
|
||||||
this.state.opponentProfile.avatar_url,
|
|
||||||
Math.floor(48 * window.devicePixelRatio),
|
|
||||||
Math.floor(48 * window.devicePixelRatio),
|
|
||||||
'crop',
|
|
||||||
)}
|
|
||||||
width={48} height={48} resizeMethod='crop'
|
width={48} height={48} resizeMethod='crop'
|
||||||
/>
|
/>
|
||||||
<h2>{this.state.opponentProfile.displayname}</h2>
|
<h2>{oppProfile.displayname}</h2>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.opponentProfileError) {
|
} else if (this.state.opponentProfileError) {
|
||||||
profile = <div>
|
profile = <div>
|
||||||
|
@ -22,7 +22,6 @@ import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Pe
|
|||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
import * as Email from "../../../email";
|
import * as Email from "../../../email";
|
||||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||||
@ -43,6 +42,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
|
|||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
@ -160,9 +160,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|||||||
width={avatarSize} height={avatarSize} />
|
width={avatarSize} height={avatarSize} />
|
||||||
: <BaseAvatar
|
: <BaseAvatar
|
||||||
className='mx_InviteDialog_userTile_avatar'
|
className='mx_InviteDialog_userTile_avatar'
|
||||||
url={getHttpUriForMxc(
|
url={this.props.member.getMxcAvatarUrl()
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
||||||
avatarSize, avatarSize, "crop")}
|
: null}
|
||||||
name={this.props.member.name}
|
name={this.props.member.name}
|
||||||
idName={this.props.member.userId}
|
idName={this.props.member.userId}
|
||||||
width={avatarSize}
|
width={avatarSize}
|
||||||
@ -262,9 +262,9 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|||||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
width={avatarSize} height={avatarSize} />
|
width={avatarSize} height={avatarSize} />
|
||||||
: <BaseAvatar
|
: <BaseAvatar
|
||||||
url={getHttpUriForMxc(
|
url={this.props.member.getMxcAvatarUrl()
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
||||||
avatarSize, avatarSize, "crop")}
|
: null}
|
||||||
name={this.props.member.name}
|
name={this.props.member.name}
|
||||||
idName={this.props.member.userId}
|
idName={this.props.member.userId}
|
||||||
width={avatarSize}
|
width={avatarSize}
|
||||||
|
@ -19,10 +19,10 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { UserAddressType } from '../../../UserAddress.js';
|
import { UserAddressType } from '../../../UserAddress.js';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.AddressTile")
|
@replaceableComponent("views.elements.AddressTile")
|
||||||
export default class AddressTile extends React.Component {
|
export default class AddressTile extends React.Component {
|
||||||
@ -47,9 +47,7 @@ export default class AddressTile extends React.Component {
|
|||||||
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
|
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
|
||||||
|
|
||||||
if (isMatrixAddress && address.avatarMxc) {
|
if (isMatrixAddress && address.avatarMxc) {
|
||||||
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25));
|
||||||
address.avatarMxc, 25, 25, 'crop',
|
|
||||||
));
|
|
||||||
} else if (address.addressType === 'email') {
|
} else if (address.addressType === 'email') {
|
||||||
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
|
imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
|
||||||
}
|
}
|
||||||
|
@ -70,9 +70,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const userId = client.getUserId();
|
const userId = client.getUserId();
|
||||||
const profileInfo = await client.getProfileInfo(userId);
|
const profileInfo = await client.getProfileInfo(userId);
|
||||||
const avatarUrl = Avatar.avatarUrlForUser(
|
const avatarUrl = profileInfo.avatar_url;
|
||||||
{avatarUrl: profileInfo.avatar_url},
|
|
||||||
AVATAR_SIZE, AVATAR_SIZE, "crop");
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
userId,
|
userId,
|
||||||
@ -113,8 +111,9 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||||||
name: displayname,
|
name: displayname,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
getAvatarUrl: (..._) => {
|
getAvatarUrl: (..._) => {
|
||||||
return avatarUrl;
|
return Avatar.avatarUrlForUser({avatarUrl}, AVATAR_SIZE, AVATAR_SIZE, "crop");
|
||||||
},
|
},
|
||||||
|
getMxcAvatarUrl: () => avatarUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
|
@ -20,6 +20,7 @@ import FlairStore from '../../../stores/FlairStore';
|
|||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
|
|
||||||
class FlairAvatar extends React.Component {
|
class FlairAvatar extends React.Component {
|
||||||
@ -39,8 +40,7 @@ class FlairAvatar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const httpUrl = this.context.mxcUrlToHttp(
|
const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16);
|
||||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
|
||||||
const tooltip = this.props.groupProfile.name ?
|
const tooltip = this.props.groupProfile.name ?
|
||||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||||
this.props.groupProfile.groupId;
|
this.props.groupProfile.groupId;
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 - 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -26,6 +24,7 @@ import FlairStore from "../../../stores/FlairStore";
|
|||||||
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
|
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@ -254,12 +253,12 @@ class Pill extends React.Component {
|
|||||||
case Pill.TYPE_GROUP_MENTION: {
|
case Pill.TYPE_GROUP_MENTION: {
|
||||||
if (this.state.group) {
|
if (this.state.group) {
|
||||||
const {avatarUrl, groupId, name} = this.state.group;
|
const {avatarUrl, groupId, name} = this.state.group;
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
linkText = groupId;
|
linkText = groupId;
|
||||||
if (this.props.shouldShowPillAvatar) {
|
if (this.props.shouldShowPillAvatar) {
|
||||||
avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
|
avatar = <BaseAvatar
|
||||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
name={name || groupId} width={16} height={16} aria-hidden="true"
|
||||||
|
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
|
||||||
}
|
}
|
||||||
pillClass = 'mx_GroupPill';
|
pillClass = 'mx_GroupPill';
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import AccessibleButton from "./AccessibleButton";
|
|||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
|
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
|
||||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
interface ISSOButtonProps extends Omit<IProps, "flow"> {
|
||||||
idp: IIdentityProvider;
|
idp: IIdentityProvider;
|
||||||
@ -72,7 +73,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
|
|||||||
brandClass = `mx_SSOButton_brand_${brandName}`;
|
brandClass = `mx_SSOButton_brand_${brandName}`;
|
||||||
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
|
||||||
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
|
} 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} />;
|
icon = <img src={src} height="24" width="24" alt={idp.name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
|
|||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
|
// 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 BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const profile = this.state.profile || {};
|
const profile = this.state.profile || {};
|
||||||
const name = profile.name || this.props.tag;
|
const name = profile.name || this.props.tag;
|
||||||
const avatarHeight = 32;
|
const avatarSize = 32;
|
||||||
|
|
||||||
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
const httpUrl = profile.avatarUrl
|
||||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarSize)
|
||||||
) : null;
|
: null;
|
||||||
|
|
||||||
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
|
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
|
||||||
const className = classNames({
|
const className = classNames({
|
||||||
@ -180,8 +181,8 @@ export default class TagTile extends React.Component {
|
|||||||
name={name}
|
name={name}
|
||||||
idName={this.props.tag}
|
idName={this.props.tag}
|
||||||
url={httpUrl}
|
url={httpUrl}
|
||||||
width={avatarHeight}
|
width={avatarSize}
|
||||||
height={avatarHeight}
|
height={avatarSize}
|
||||||
/>
|
/>
|
||||||
{contextButton}
|
{contextButton}
|
||||||
{badgeElement}
|
{badgeElement}
|
||||||
|
@ -27,6 +27,7 @@ import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/Contex
|
|||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
// XXX this class copies a lot from RoomTile.js
|
// XXX this class copies a lot from RoomTile.js
|
||||||
@replaceableComponent("views.groups.GroupInviteTile")
|
@replaceableComponent("views.groups.GroupInviteTile")
|
||||||
@ -117,8 +118,9 @@ export default class GroupInviteTile extends React.Component {
|
|||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
|
||||||
const groupName = this.props.group.name || this.props.group.groupId;
|
const groupName = this.props.group.name || this.props.group.groupId;
|
||||||
const httpAvatarUrl = this.props.group.avatarUrl ?
|
const httpAvatarUrl = this.props.group.avatarUrl
|
||||||
this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
|
? mediaFromMxc(this.props.group.avatarUrl).getSquareThumbnailHttp(24)
|
||||||
|
: null;
|
||||||
|
|
||||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||||||
import { GroupMemberType } from '../../../groups';
|
import { GroupMemberType } from '../../../groups';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.groups.GroupMemberTile")
|
@replaceableComponent("views.groups.GroupMemberTile")
|
||||||
export default class GroupMemberTile extends React.Component {
|
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 EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||||
|
|
||||||
const name = this.props.member.displayname || this.props.member.userId;
|
const name = this.props.member.displayname || this.props.member.userId;
|
||||||
const avatarUrl = this.context.mxcUrlToHttp(
|
const avatarUrl = this.props.member.avatarUrl
|
||||||
this.props.member.avatarUrl,
|
? mediaFromMxc(this.props.member.avatarUrl).getSquareThumbnailHttp(36)
|
||||||
36, 36, 'crop',
|
: null;
|
||||||
);
|
|
||||||
|
|
||||||
const av = (
|
const av = (
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
|
@ -25,6 +25,7 @@ import GroupStore from '../../../stores/GroupStore';
|
|||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.groups.GroupRoomInfo")
|
@replaceableComponent("views.groups.GroupRoomInfo")
|
||||||
export default class GroupRoomInfo extends React.Component {
|
export default class GroupRoomInfo extends React.Component {
|
||||||
@ -204,10 +205,8 @@ export default class GroupRoomInfo extends React.Component {
|
|||||||
const avatarUrl = this.state.groupRoom.avatarUrl;
|
const avatarUrl = this.state.groupRoom.avatarUrl;
|
||||||
let avatarElement;
|
let avatarElement;
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
|
const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800);
|
||||||
avatarElement = (<div className="mx_MemberInfo_avatar">
|
avatarElement = <div className="mx_MemberInfo_avatar"><img src={httpUrl} /></div>;
|
||||||
<img src={httpUrl} />
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupRoomName = this.state.groupRoom.displayname;
|
const groupRoomName = this.state.groupRoom.displayname;
|
||||||
|
@ -21,6 +21,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||||||
import { GroupRoomType } from '../../../groups';
|
import { GroupRoomType } from '../../../groups';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.groups.GroupRoomTile")
|
@replaceableComponent("views.groups.GroupRoomTile")
|
||||||
class GroupRoomTile extends React.Component {
|
class GroupRoomTile extends React.Component {
|
||||||
@ -42,10 +43,9 @@ class GroupRoomTile extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const avatarUrl = this.context.mxcUrlToHttp(
|
const avatarUrl = this.props.groupRoom.avatarUrl
|
||||||
this.props.groupRoom.avatarUrl,
|
? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36)
|
||||||
36, 36, 'crop',
|
: null;
|
||||||
);
|
|
||||||
|
|
||||||
const av = (
|
const av = (
|
||||||
<BaseAvatar name={this.props.groupRoom.displayname}
|
<BaseAvatar name={this.props.groupRoom.displayname}
|
||||||
|
@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
|
|||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
function nop() {}
|
function nop() {}
|
||||||
|
|
||||||
@ -73,8 +74,9 @@ class GroupTile extends React.Component {
|
|||||||
const descElement = this.props.showDescription ?
|
const descElement = this.props.showDescription ?
|
||||||
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
||||||
<div />;
|
<div />;
|
||||||
const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
|
const httpUrl = profile.avatarUrl
|
||||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
|
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
|
||||||
|
: null;
|
||||||
|
|
||||||
let avatarElement = (
|
let avatarElement = (
|
||||||
<div className="mx_GroupTile_avatar">
|
<div className="mx_GroupTile_avatar">
|
||||||
|
@ -17,11 +17,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|
||||||
import { decryptFile } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromContent} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MAudioBody")
|
@replaceableComponent("views.messages.MAudioBody")
|
||||||
export default class MAudioBody extends React.Component {
|
export default class MAudioBody extends React.Component {
|
||||||
@ -41,11 +41,11 @@ export default class MAudioBody extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getContentUrl() {
|
_getContentUrl() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||||
if (content.file !== undefined) {
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
return media.srcHttp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -18,52 +17,24 @@ limitations under the License.
|
|||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import filesize from 'filesize';
|
import filesize from 'filesize';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {decryptFile} from '../../../utils/DecryptFile';
|
import {decryptFile} from '../../../utils/DecryptFile';
|
||||||
import Tinter from '../../../Tinter';
|
|
||||||
import request from 'browser-request';
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromContent} from "../../../customisations/Media";
|
||||||
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
|
||||||
|
let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||||
|
|
||||||
// A cached tinted copy of require("../../../../res/img/download.svg")
|
async function cacheDownloadIcon() {
|
||||||
let tintedDownloadImageURL;
|
if (downloadIconUrl) return; // cached already
|
||||||
// Track a list of mounted MFileBody instances so that we can update
|
const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text());
|
||||||
// the require("../../../../res/img/download.svg") when the tint changes.
|
downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||||
let nextMountId = 0;
|
|
||||||
const mounts = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the tinted copy of require("../../../../res/img/download.svg") when the tint changes.
|
|
||||||
*/
|
|
||||||
function updateTintedDownloadImage() {
|
|
||||||
// Download the svg as an XML document.
|
|
||||||
// We could cache the XML response here, but since the tint rarely changes
|
|
||||||
// it's probably not worth it.
|
|
||||||
// Also note that we can't use fetch here because fetch doesn't support
|
|
||||||
// file URLs, which the download image will be if we're running from
|
|
||||||
// the filesystem (like in an Electron wrapper).
|
|
||||||
request({uri: require("../../../../res/img/download.svg")}, (err, response, body) => {
|
|
||||||
if (err) return;
|
|
||||||
|
|
||||||
const svg = new DOMParser().parseFromString(body, "image/svg+xml");
|
|
||||||
// Apply the fixups to the XML.
|
|
||||||
const fixups = Tinter.calcSvgFixups([{contentDocument: svg}]);
|
|
||||||
Tinter.applySvgFixups(fixups);
|
|
||||||
// Encoded the fixed up SVG as a data URL.
|
|
||||||
const svgString = new XMLSerializer().serializeToString(svg);
|
|
||||||
tintedDownloadImageURL = "data:image/svg+xml;base64," + window.btoa(svgString);
|
|
||||||
// Notify each mounted MFileBody that the URL has changed.
|
|
||||||
Object.keys(mounts).forEach(function(id) {
|
|
||||||
mounts[id].tint();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Tinter.registerTintable(updateTintedDownloadImage);
|
// Cache the asset immediately
|
||||||
|
cacheDownloadIcon();
|
||||||
|
|
||||||
// User supplied content can contain scripts, we have to be careful that
|
// User supplied content can contain scripts, we have to be careful that
|
||||||
// we don't accidentally run those script within the same origin as the
|
// we don't accidentally run those script within the same origin as the
|
||||||
@ -106,6 +77,7 @@ function computedStyle(element) {
|
|||||||
}
|
}
|
||||||
const style = window.getComputedStyle(element, null);
|
const style = window.getComputedStyle(element, null);
|
||||||
let cssText = style.cssText;
|
let cssText = style.cssText;
|
||||||
|
// noinspection EqualityComparisonWithCoercionJS
|
||||||
if (cssText == "") {
|
if (cssText == "") {
|
||||||
// Firefox doesn't implement ".cssText" for computed styles.
|
// Firefox doesn't implement ".cssText" for computed styles.
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||||
@ -145,7 +117,6 @@ export default class MFileBody extends React.Component {
|
|||||||
|
|
||||||
this._iframe = createRef();
|
this._iframe = createRef();
|
||||||
this._dummyLink = createRef();
|
this._dummyLink = createRef();
|
||||||
this._downloadImage = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,16 +149,8 @@ export default class MFileBody extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getContentUrl() {
|
_getContentUrl() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
return media.srcHttp;
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// Add this to the list of mounted components to receive notifications
|
|
||||||
// when the tint changes.
|
|
||||||
this.id = nextMountId++;
|
|
||||||
mounts[this.id] = this;
|
|
||||||
this.tint();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
@ -196,34 +159,12 @@ export default class MFileBody extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// Remove this from the list of mounted components
|
|
||||||
delete mounts[this.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
tint = () => {
|
|
||||||
// Update our tinted copy of require("../../../../res/img/download.svg")
|
|
||||||
if (this._downloadImage.current) {
|
|
||||||
this._downloadImage.current.src = tintedDownloadImageURL;
|
|
||||||
}
|
|
||||||
if (this._iframe.current) {
|
|
||||||
// If the attachment is encrypted then the download image
|
|
||||||
// will be inside the iframe so we wont be able to update
|
|
||||||
// it directly.
|
|
||||||
this._iframe.current.contentWindow.postMessage({
|
|
||||||
imgSrc: tintedDownloadImageURL,
|
|
||||||
style: computedStyle(this._dummyLink.current),
|
|
||||||
}, "*");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
const text = this.presentableTextForFile(content);
|
const text = this.presentableTextForFile(content);
|
||||||
const isEncrypted = content.file !== undefined;
|
const isEncrypted = content.file !== undefined;
|
||||||
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
|
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const fileSize = content.info ? content.info.size : null;
|
const fileSize = content.info ? content.info.size : null;
|
||||||
const fileType = content.info ? content.info.mimetype : "application/octet-stream";
|
const fileType = content.info ? content.info.mimetype : "application/octet-stream";
|
||||||
|
|
||||||
@ -280,7 +221,8 @@ export default class MFileBody extends React.Component {
|
|||||||
// When the iframe loads we tell it to render a download link
|
// When the iframe loads we tell it to render a download link
|
||||||
const onIframeLoad = (ev) => {
|
const onIframeLoad = (ev) => {
|
||||||
ev.target.contentWindow.postMessage({
|
ev.target.contentWindow.postMessage({
|
||||||
imgSrc: tintedDownloadImageURL,
|
imgSrc: downloadIconUrl,
|
||||||
|
imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon.
|
||||||
style: computedStyle(this._dummyLink.current),
|
style: computedStyle(this._dummyLink.current),
|
||||||
blob: this.state.decryptedBlob,
|
blob: this.state.decryptedBlob,
|
||||||
// Set a download attribute for encrypted files so that the file
|
// Set a download attribute for encrypted files so that the file
|
||||||
@ -384,7 +326,7 @@ export default class MFileBody extends React.Component {
|
|||||||
{placeholder}
|
{placeholder}
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<a {...downloadProps}>
|
<a {...downloadProps}>
|
||||||
<img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
|
<span className="mx_MFileBody_download_icon" />
|
||||||
{ _t("Download %(text)s", { text: text }) }
|
{ _t("Download %(text)s", { text: text }) }
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromContent} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MImageBody")
|
@replaceableComponent("views.messages.MImageBody")
|
||||||
export default class MImageBody extends React.Component {
|
export default class MImageBody extends React.Component {
|
||||||
@ -70,7 +71,7 @@ export default class MImageBody extends React.Component {
|
|||||||
this._image = createRef();
|
this._image = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
|
// FIXME: factor this out and apply it to MVideoBody and MAudioBody too!
|
||||||
onClientSync(syncState, prevState) {
|
onClientSync(syncState, prevState) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
// Consider the client reconnected if there is no error with syncing.
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
@ -167,16 +168,16 @@ export default class MImageBody extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getContentUrl() {
|
_getContentUrl() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||||
if (content.file !== undefined) {
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return this.context.mxcUrlToHttp(content.url);
|
return media.srcHttp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getThumbUrl() {
|
_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
|
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||||
// thumbnail resolution will be unnecessarily reduced.
|
// thumbnail resolution will be unnecessarily reduced.
|
||||||
// custom timeline widths seems preferable.
|
// custom timeline widths seems preferable.
|
||||||
@ -185,21 +186,19 @@ export default class MImageBody extends React.Component {
|
|||||||
const thumbHeight = Math.round(600 * pixelRatio);
|
const thumbHeight = Math.round(600 * pixelRatio);
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent();
|
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.
|
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||||
if (this.state.decryptedThumbnailUrl) {
|
if (this.state.decryptedThumbnailUrl) {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
}
|
}
|
||||||
return this.state.decryptedUrl;
|
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,
|
// special case to return clientside sender-generated thumbnails for SVGs, if any,
|
||||||
// given we deliberately don't thumbnail them serverside to prevent
|
// given we deliberately don't thumbnail them serverside to prevent
|
||||||
// billion lol attacks and similar
|
// billion lol attacks and similar
|
||||||
return this.context.mxcUrlToHttp(
|
return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale');
|
||||||
content.info.thumbnail_url,
|
|
||||||
thumbWidth,
|
|
||||||
thumbHeight,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// we try to download the correct resolution
|
// we try to download the correct resolution
|
||||||
// for hi-res images (like retina screenshots).
|
// for hi-res images (like retina screenshots).
|
||||||
@ -218,7 +217,7 @@ export default class MImageBody extends React.Component {
|
|||||||
pixelRatio === 1.0 ||
|
pixelRatio === 1.0 ||
|
||||||
(!info || !info.w || !info.h || !info.size)
|
(!info || !info.w || !info.h || !info.size)
|
||||||
) {
|
) {
|
||||||
return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
|
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
|
||||||
} else {
|
} else {
|
||||||
// we should only request thumbnails if the image is bigger than 800x600
|
// we should only request thumbnails if the image is bigger than 800x600
|
||||||
// (or 1600x1200 on retina) otherwise the image in the timeline will just
|
// (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.w > thumbWidth ||
|
||||||
info.h > thumbHeight
|
info.h > thumbHeight
|
||||||
);
|
);
|
||||||
const isLargeFileSize = info.size > 1*1024*1024;
|
const isLargeFileSize = info.size > 1*1024*1024; // 1mb
|
||||||
|
|
||||||
if (isLargeFileSize && isLargerThanThumbnail) {
|
if (isLargeFileSize && isLargerThanThumbnail) {
|
||||||
// image is too large physically and bytewise to clutter our timeline so
|
// 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
|
// 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).
|
// despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
|
||||||
return this.context.mxcUrlToHttp(
|
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
|
||||||
content.url,
|
|
||||||
thumbWidth,
|
|
||||||
thumbHeight,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// download the original image otherwise, so we can scale it client side
|
// download the original image otherwise, so we can scale it client side
|
||||||
// to take pixelRatio into account.
|
// to take pixelRatio into account.
|
||||||
// ( no width/height means we want the original image)
|
return media.srcHttp;
|
||||||
return this.context.mxcUrlToHttp(
|
|
||||||
content.url,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,12 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|
||||||
import { decryptFile } from '../../../utils/DecryptFile';
|
import { decryptFile } from '../../../utils/DecryptFile';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromContent} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
@ -76,11 +76,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getContentUrl(): string|null {
|
private getContentUrl(): string|null {
|
||||||
const content = this.props.mxEvent.getContent();
|
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||||
if (content.file !== undefined) {
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
return media.srcHttp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,10 +91,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||||||
|
|
||||||
private getThumbUrl(): string|null {
|
private getThumbUrl(): string|null {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
const media = mediaFromContent(content);
|
||||||
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
} else if (content.info && content.info.thumbnail_url) {
|
} else if (media.hasThumbnail) {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
return media.thumbnailHttp;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import * as sdk from '../../../index';
|
|||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.RoomAvatarEvent")
|
@replaceableComponent("views.messages.RoomAvatarEvent")
|
||||||
export default class RoomAvatarEvent extends React.Component {
|
export default class RoomAvatarEvent extends React.Component {
|
||||||
@ -35,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component {
|
|||||||
onAvatarClick = () => {
|
onAvatarClick = () => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const ev = this.props.mxEvent;
|
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 room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||||
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||||
|
@ -64,6 +64,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface IDevice {
|
interface IDevice {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
@ -1424,14 +1425,14 @@ const UserInfoHeader: React.FC<{
|
|||||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
||||||
if (!avatarUrl) return;
|
if (!avatarUrl) return;
|
||||||
|
|
||||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
|
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
||||||
const params = {
|
const params = {
|
||||||
src: httpUrl,
|
src: httpUrl,
|
||||||
name: member.name,
|
name: member.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||||
}, [cli, member]);
|
}, [member]);
|
||||||
|
|
||||||
const avatarElement = (
|
const avatarElement = (
|
||||||
<div className="mx_UserInfo_avatar">
|
<div className="mx_UserInfo_avatar">
|
||||||
|
@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
// TODO: Merge with ProfileSettings?
|
// TODO: Merge with ProfileSettings?
|
||||||
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
||||||
@ -38,7 +39,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||||||
|
|
||||||
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
||||||
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
|
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 topicEvent = room.currentState.getStateEvents("m.room.topic", "");
|
||||||
const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : '';
|
const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : '';
|
||||||
@ -112,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
|
|||||||
if (this.state.avatarFile) {
|
if (this.state.avatarFile) {
|
||||||
const uri = await client.uploadContent(this.state.avatarFile);
|
const uri = await client.uploadContent(this.state.avatarFile);
|
||||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, '');
|
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.originalAvatarUrl = newState.avatarUrl;
|
||||||
newState.avatarFile = null;
|
newState.avatarFile = null;
|
||||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||||
|
@ -26,6 +26,7 @@ import Modal from "../../../Modal";
|
|||||||
import * as ImageUtils from "../../../ImageUtils";
|
import * as ImageUtils from "../../../ImageUtils";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
||||||
export default class LinkPreviewWidget extends React.Component {
|
export default class LinkPreviewWidget extends React.Component {
|
||||||
@ -83,7 +84,7 @@ export default class LinkPreviewWidget extends React.Component {
|
|||||||
|
|
||||||
let src = p["og:image"];
|
let src = p["og:image"];
|
||||||
if (src && src.startsWith("mxc://")) {
|
if (src && src.startsWith("mxc://")) {
|
||||||
src = MatrixClientPeg.get().mxcUrlToHttp(src);
|
src = mediaFromMxc(src).srcHttp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
@ -109,9 +110,11 @@ export default class LinkPreviewWidget extends React.Component {
|
|||||||
if (!SettingsStore.getValue("showImages")) {
|
if (!SettingsStore.getValue("showImages")) {
|
||||||
image = null; // Don't render a button to show the image, just hide it outright
|
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://")) {
|
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;
|
let thumbHeight = imageMaxHeight;
|
||||||
|
@ -18,10 +18,9 @@ import * as sdk from '../../../index';
|
|||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { linkifyElement } from '../../../HtmlUtils';
|
import { linkifyElement } from '../../../HtmlUtils';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
export function getDisplayAliasForRoom(room) {
|
export function getDisplayAliasForRoom(room) {
|
||||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||||
@ -100,13 +99,14 @@ export default class RoomDetailRow extends React.Component {
|
|||||||
{ guestJoin }
|
{ guestJoin }
|
||||||
</div>) : <div />;
|
</div>) : <div />;
|
||||||
|
|
||||||
|
let avatarUrl = null;
|
||||||
|
if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24);
|
||||||
|
|
||||||
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
|
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
|
||||||
<td className="mx_RoomDirectory_roomAvatar">
|
<td className="mx_RoomDirectory_roomAvatar">
|
||||||
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
||||||
name={name} idName={name}
|
name={name} idName={name}
|
||||||
url={getHttpUriForMxc(
|
url={avatarUrl} />
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
|
||||||
room.avatarUrl, 24, 24, "crop")} />
|
|
||||||
</td>
|
</td>
|
||||||
<td className="mx_RoomDirectory_roomDescription">
|
<td className="mx_RoomDirectory_roomDescription">
|
||||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||||
|
@ -16,9 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import Pill from "../elements/Pill";
|
import Pill from "../elements/Pill";
|
||||||
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
@ -27,6 +25,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
|||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
ev: MatrixEvent;
|
ev: MatrixEvent;
|
||||||
@ -114,10 +113,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
|
|||||||
let networkIcon;
|
let networkIcon;
|
||||||
|
|
||||||
if (protocol.avatar_url) {
|
if (protocol.avatar_url) {
|
||||||
const avatarUrl = getHttpUriForMxc(
|
const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64);
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
|
||||||
protocol.avatar_url, 64, 64, "crop",
|
|
||||||
);
|
|
||||||
|
|
||||||
networkIcon = <BaseAvatar className="protocol-icon"
|
networkIcon = <BaseAvatar className="protocol-icon"
|
||||||
width={48}
|
width={48}
|
||||||
|
@ -21,6 +21,7 @@ import * as sdk from '../../../index';
|
|||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Spinner from '../elements/Spinner';
|
import Spinner from '../elements/Spinner';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.settings.ChangeAvatar")
|
@replaceableComponent("views.settings.ChangeAvatar")
|
||||||
export default class ChangeAvatar extends React.Component {
|
export default class ChangeAvatar extends React.Component {
|
||||||
@ -117,7 +118,7 @@ export default class ChangeAvatar extends React.Component {
|
|||||||
httpPromise.then(function() {
|
httpPromise.then(function() {
|
||||||
self.setState({
|
self.setState({
|
||||||
phase: ChangeAvatar.Phases.Display,
|
phase: ChangeAvatar.Phases.Display,
|
||||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),
|
avatarUrl: mediaFromMxc(newUrl).srcHttp,
|
||||||
});
|
});
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
self.setState({
|
self.setState({
|
||||||
|
@ -24,6 +24,7 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
|||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.settings.ProfileSettings")
|
@replaceableComponent("views.settings.ProfileSettings")
|
||||||
export default class ProfileSettings extends React.Component {
|
export default class ProfileSettings extends React.Component {
|
||||||
@ -32,7 +33,7 @@ export default class ProfileSettings extends React.Component {
|
|||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
let avatarUrl = OwnProfileStore.instance.avatarMxc;
|
let avatarUrl = OwnProfileStore.instance.avatarMxc;
|
||||||
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
|
if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96);
|
||||||
this.state = {
|
this.state = {
|
||||||
userId: client.getUserId(),
|
userId: client.getUserId(),
|
||||||
originalDisplayName: OwnProfileStore.instance.displayName,
|
originalDisplayName: OwnProfileStore.instance.displayName,
|
||||||
@ -97,7 +98,7 @@ export default class ProfileSettings extends React.Component {
|
|||||||
` (${this.state.avatarFile.size}) bytes`);
|
` (${this.state.avatarFile.size}) bytes`);
|
||||||
const uri = await client.uploadContent(this.state.avatarFile);
|
const uri = await client.uploadContent(this.state.avatarFile);
|
||||||
await client.setAvatarUrl(uri);
|
await client.setAvatarUrl(uri);
|
||||||
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
|
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||||
newState.originalAvatarUrl = newState.avatarUrl;
|
newState.originalAvatarUrl = newState.avatarUrl;
|
||||||
newState.avatarFile = null;
|
newState.avatarFile = null;
|
||||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||||
|
144
src/customisations/Media.ts
Normal file
144
src/customisations/Media.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||||
|
import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
|
||||||
|
import {ResizeMethod} from "../Avatar";
|
||||||
|
|
||||||
|
// Populate this class with the details of your customisations when copying it.
|
||||||
|
|
||||||
|
// Implementation note: The Media class must complete the contract as shown here, though
|
||||||
|
// the constructor can be whatever is relevant to your implementation. The mediaForX
|
||||||
|
// functions below create an instance of the Media class and are used throughout the
|
||||||
|
// project.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A media object is a representation of a "source media" and an optional
|
||||||
|
* "thumbnail media", derived from event contents or external sources.
|
||||||
|
*/
|
||||||
|
export class Media {
|
||||||
|
// Per above, this constructor signature can be whatever is helpful for you.
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public get srcMxc(): string {
|
||||||
|
return this.prepared.mxc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MXC URI of the thumbnail media, if a thumbnail is recorded. Null/undefined
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public get thumbnailMxc(): string | undefined | null {
|
||||||
|
return this.prepared.thumbnail?.mxc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not a thumbnail is recorded for this media.
|
||||||
|
*/
|
||||||
|
public get hasThumbnail(): boolean {
|
||||||
|
return !!this.thumbnailMxc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP URL for the source media.
|
||||||
|
*/
|
||||||
|
public get srcHttp(): string {
|
||||||
|
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.
|
||||||
|
* @param {number} width The desired width of the thumbnail.
|
||||||
|
* @param {number} height The desired height of the thumbnail.
|
||||||
|
* @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: ResizeMethod = "scale"): string | null | undefined {
|
||||||
|
if (!this.hasThumbnail) return null;
|
||||||
|
return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the HTTP URL for a thumbnail of the source media with the requested characteristics.
|
||||||
|
* @param {number} width The desired width of the thumbnail.
|
||||||
|
* @param {number} height The desired height of the thumbnail.
|
||||||
|
* @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: ResizeMethod = "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.
|
||||||
|
*/
|
||||||
|
public downloadSource(): Promise<Response> {
|
||||||
|
return fetch(this.srcHttp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a media object from event content.
|
||||||
|
* @param {IMediaEventContent} content The event content.
|
||||||
|
* @returns {Media} The media object.
|
||||||
|
*/
|
||||||
|
export function mediaFromContent(content: IMediaEventContent): Media {
|
||||||
|
return new Media(prepEventContentAsMedia(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a media object from an MXC URI.
|
||||||
|
* @param {string} mxc The MXC URI.
|
||||||
|
* @returns {Media} The media object.
|
||||||
|
*/
|
||||||
|
export function mediaFromMxc(mxc: string): Media {
|
||||||
|
return mediaFromContent({url: mxc});
|
||||||
|
}
|
88
src/customisations/models/IMediaEventContent.ts
Normal file
88
src/customisations/models/IMediaEventContent.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: These types should be elsewhere.
|
||||||
|
|
||||||
|
export interface IEncryptedFile {
|
||||||
|
url: string;
|
||||||
|
mimetype?: string;
|
||||||
|
key: {
|
||||||
|
alg: string;
|
||||||
|
key_ops: string[]; // eslint-disable-line camelcase
|
||||||
|
kty: string;
|
||||||
|
k: string;
|
||||||
|
ext: boolean;
|
||||||
|
};
|
||||||
|
iv: string;
|
||||||
|
hashes: {[alg: string]: string};
|
||||||
|
v: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMediaEventContent {
|
||||||
|
url?: string; // required on unencrypted media
|
||||||
|
file?: IEncryptedFile; // required for *encrypted* media
|
||||||
|
info?: {
|
||||||
|
thumbnail_url?: string; // eslint-disable-line camelcase
|
||||||
|
thumbnail_file?: IEncryptedFile; // eslint-disable-line camelcase
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPreparedMedia extends IMediaObject {
|
||||||
|
thumbnail?: IMediaObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMediaObject {
|
||||||
|
mxc: string;
|
||||||
|
file?: IEncryptedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an event content body into a prepared media object. This prepared media object
|
||||||
|
* can be used with other functions to manipulate the media.
|
||||||
|
* @param {IMediaEventContent} content Unredacted media event content. See interface.
|
||||||
|
* @returns {IPreparedMedia} A prepared media object.
|
||||||
|
* @throws Throws if the given content cannot be packaged into a prepared media object.
|
||||||
|
*/
|
||||||
|
export function prepEventContentAsMedia(content: IMediaEventContent): IPreparedMedia {
|
||||||
|
let thumbnail: IMediaObject = null;
|
||||||
|
if (content?.info?.thumbnail_url) {
|
||||||
|
thumbnail = {
|
||||||
|
mxc: content.info.thumbnail_url,
|
||||||
|
file: content.info.thumbnail_file,
|
||||||
|
};
|
||||||
|
} else if (content?.info?.thumbnail_file?.url) {
|
||||||
|
thumbnail = {
|
||||||
|
mxc: content.info.thumbnail_file.url,
|
||||||
|
file: content.info.thumbnail_file,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content?.url) {
|
||||||
|
return {
|
||||||
|
thumbnail,
|
||||||
|
mxc: content.url,
|
||||||
|
file: content.file,
|
||||||
|
};
|
||||||
|
} else if (content?.file?.url) {
|
||||||
|
return {
|
||||||
|
thumbnail,
|
||||||
|
mxc: content.file.url,
|
||||||
|
file: content.file,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid file provided: cannot determine MXC URI. Has it been redacted?");
|
||||||
|
}
|
@ -22,6 +22,7 @@ import { User } from "matrix-js-sdk/src/models/user";
|
|||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
|
import {mediaFromMxc} from "../customisations/Media";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
@ -72,8 +73,12 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
|||||||
*/
|
*/
|
||||||
public getHttpAvatarUrl(size = 0): string {
|
public getHttpAvatarUrl(size = 0): string {
|
||||||
if (!this.avatarMxc) return null;
|
if (!this.avatarMxc) return null;
|
||||||
const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
|
const media = mediaFromMxc(this.avatarMxc);
|
||||||
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
|
if (!size || size <= 0) {
|
||||||
|
return media.srcHttp;
|
||||||
|
} else {
|
||||||
|
return media.getSquareThumbnailHttp(size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onNotReady() {
|
protected async onNotReady() {
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
function remoteRender(event) {
|
function remoteRender(event) {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
|
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("span"); // we'll mask it as an image
|
||||||
img.id = "img";
|
img.id = "img";
|
||||||
img.src = data.imgSrc;
|
|
||||||
img.style = data.imgStyle;
|
|
||||||
|
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.id = "a";
|
a.id = "a";
|
||||||
@ -16,6 +14,23 @@ function remoteRender(event) {
|
|||||||
a.appendChild(img);
|
a.appendChild(img);
|
||||||
a.appendChild(document.createTextNode(data.textContent));
|
a.appendChild(document.createTextNode(data.textContent));
|
||||||
|
|
||||||
|
// Apply image style after so we can steal the anchor's colour.
|
||||||
|
// Style copied from a rendered version of mx_MFileBody_download_icon
|
||||||
|
img.style = (data.imgStyle || "" +
|
||||||
|
"width: 12px; height: 12px;" +
|
||||||
|
"-webkit-mask-size: 12px;" +
|
||||||
|
"mask-size: 12px;" +
|
||||||
|
"-webkit-mask-position: center;" +
|
||||||
|
"mask-position: center;" +
|
||||||
|
"-webkit-mask-repeat: no-repeat;" +
|
||||||
|
"mask-repeat: no-repeat;" +
|
||||||
|
"display: inline-block;") + "" +
|
||||||
|
|
||||||
|
// Always add these styles
|
||||||
|
`-webkit-mask-image: url('${data.imgSrc}');` +
|
||||||
|
`mask-image: url('${data.imgSrc}');` +
|
||||||
|
`background-color: ${a.style.color};`;
|
||||||
|
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
// Don't display scrollbars if the link takes more than one line to display.
|
// Don't display scrollbars if the link takes more than one line to display.
|
||||||
body.style = "margin: 0px; overflow: hidden";
|
body.style = "margin: 0px; overflow: hidden";
|
||||||
@ -26,20 +41,8 @@ function remoteRender(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteSetTint(event) {
|
|
||||||
const data = event.data;
|
|
||||||
|
|
||||||
const img = document.getElementById("img");
|
|
||||||
img.src = data.imgSrc;
|
|
||||||
img.style = data.imgStyle;
|
|
||||||
|
|
||||||
const a = document.getElementById("a");
|
|
||||||
a.style = data.style;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onmessage = function(e) {
|
window.onmessage = function(e) {
|
||||||
if (e.origin === window.location.origin) {
|
if (e.origin === window.location.origin) {
|
||||||
if (e.data.blob) remoteRender(e);
|
if (e.data.blob) remoteRender(e);
|
||||||
else remoteSetTint(e);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -17,8 +16,8 @@ limitations under the License.
|
|||||||
|
|
||||||
// Pull in the encryption lib so that we can decrypt attachments.
|
// Pull in the encryption lib so that we can decrypt attachments.
|
||||||
import encrypt from 'browser-encrypt-attachment';
|
import encrypt from 'browser-encrypt-attachment';
|
||||||
// Grab the client so that we can turn mxc:// URLs into https:// URLS.
|
import {mediaFromContent} from "../customisations/Media";
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {IEncryptedFile} from "../customisations/models/IMediaEventContent";
|
||||||
|
|
||||||
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
||||||
// as for performance reasons these are now rendered via URL.createObjectURL()
|
// as for performance reasons these are now rendered via URL.createObjectURL()
|
||||||
@ -54,48 +53,46 @@ import {MatrixClientPeg} from '../MatrixClientPeg';
|
|||||||
// For the record, mime-types which must NEVER enter this list below include:
|
// For the record, mime-types which must NEVER enter this list below include:
|
||||||
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||||
|
|
||||||
const ALLOWED_BLOB_MIMETYPES = {
|
const ALLOWED_BLOB_MIMETYPES = [
|
||||||
'image/jpeg': true,
|
'image/jpeg',
|
||||||
'image/gif': true,
|
'image/gif',
|
||||||
'image/png': true,
|
'image/png',
|
||||||
|
|
||||||
'video/mp4': true,
|
'video/mp4',
|
||||||
'video/webm': true,
|
'video/webm',
|
||||||
'video/ogg': true,
|
'video/ogg',
|
||||||
|
|
||||||
'audio/mp4': true,
|
'audio/mp4',
|
||||||
'audio/webm': true,
|
'audio/webm',
|
||||||
'audio/aac': true,
|
'audio/aac',
|
||||||
'audio/mpeg': true,
|
'audio/mpeg',
|
||||||
'audio/ogg': true,
|
'audio/ogg',
|
||||||
'audio/wave': true,
|
'audio/wave',
|
||||||
'audio/wav': true,
|
'audio/wav',
|
||||||
'audio/x-wav': true,
|
'audio/x-wav',
|
||||||
'audio/x-pn-wav': true,
|
'audio/x-pn-wav',
|
||||||
'audio/flac': true,
|
'audio/flac',
|
||||||
'audio/x-flac': true,
|
'audio/x-flac',
|
||||||
};
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt a file attached to a matrix event.
|
* Decrypt a file attached to a matrix event.
|
||||||
* @param {Object} file The json taken from the matrix event.
|
* @param {IEncryptedFile} file The json taken from the matrix event.
|
||||||
* This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments}
|
* This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments}
|
||||||
* as the encryption info object, so will also have the those keys in addition to
|
* as the encryption info object, so will also have the those keys in addition to
|
||||||
* the keys below.
|
* the keys below.
|
||||||
* @param {string} file.url An mxc:// URL for the encrypted file.
|
* @returns {Promise<Blob>} Resolves to a Blob of the file.
|
||||||
* @param {string} file.mimetype The MIME-type of the plaintext file.
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
*/
|
||||||
export function decryptFile(file) {
|
export function decryptFile(file: IEncryptedFile): Promise<Blob> {
|
||||||
const url = MatrixClientPeg.get().mxcUrlToHttp(file.url);
|
const media = mediaFromContent({file});
|
||||||
// Download the encrypted file as an array buffer.
|
// Download the encrypted file as an array buffer.
|
||||||
return Promise.resolve(fetch(url)).then(function(response) {
|
return media.downloadSource().then((response) => {
|
||||||
return response.arrayBuffer();
|
return response.arrayBuffer();
|
||||||
}).then(function(responseData) {
|
}).then((responseData) => {
|
||||||
// Decrypt the array buffer using the information taken from
|
// Decrypt the array buffer using the information taken from
|
||||||
// the event content.
|
// the event content.
|
||||||
return encrypt.decryptAttachment(responseData, file);
|
return encrypt.decryptAttachment(responseData, file);
|
||||||
}).then(function(dataArray) {
|
}).then((dataArray) => {
|
||||||
// Turn the array into a Blob and give it the correct MIME-type.
|
// Turn the array into a Blob and give it the correct MIME-type.
|
||||||
|
|
||||||
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
|
// IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
|
||||||
@ -103,11 +100,10 @@ export function decryptFile(file) {
|
|||||||
// browser (e.g. by copying the URI into a new tab or window.)
|
// browser (e.g. by copying the URI into a new tab or window.)
|
||||||
// See warning at top of file.
|
// See warning at top of file.
|
||||||
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
||||||
if (!ALLOWED_BLOB_MIMETYPES[mimetype]) {
|
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||||
mimetype = 'application/octet-stream';
|
mimetype = 'application/octet-stream';
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new Blob([dataArray], {type: mimetype});
|
return new Blob([dataArray], {type: mimetype});
|
||||||
return blob;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -262,7 +262,8 @@ describe('GroupView', function() {
|
|||||||
expect(longDescElement.innerHTML).toContain('<ul>');
|
expect(longDescElement.innerHTML).toContain('<ul>');
|
||||||
expect(longDescElement.innerHTML).toContain('<li>And lists!</li>');
|
expect(longDescElement.innerHTML).toContain('<li>And lists!</li>');
|
||||||
|
|
||||||
const imgSrc = "https://my.home.server/_matrix/media/r0/thumbnail/someimageurl?width=800&height=600";
|
const imgSrc = "https://my.home.server/_matrix/media/r0/thumbnail/someimageurl" +
|
||||||
|
"?width=800&height=600&method=scale";
|
||||||
expect(longDescElement.innerHTML).toContain('<img src="' + imgSrc + '">');
|
expect(longDescElement.innerHTML).toContain('<img src="' + imgSrc + '">');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -116,6 +116,7 @@ describe('MessagePanel', function() {
|
|||||||
getAvatarUrl: () => {
|
getAvatarUrl: () => {
|
||||||
return "avatar.jpeg";
|
return "avatar.jpeg";
|
||||||
},
|
},
|
||||||
|
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
},
|
},
|
||||||
ts: ts0 + i*1000,
|
ts: ts0 + i*1000,
|
||||||
mship: 'join',
|
mship: 'join',
|
||||||
@ -148,6 +149,7 @@ describe('MessagePanel', function() {
|
|||||||
getAvatarUrl: () => {
|
getAvatarUrl: () => {
|
||||||
return "avatar.jpeg";
|
return "avatar.jpeg";
|
||||||
},
|
},
|
||||||
|
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
},
|
},
|
||||||
ts: ts0 + i*1000,
|
ts: ts0 + i*1000,
|
||||||
mship: 'join',
|
mship: 'join',
|
||||||
@ -193,6 +195,7 @@ describe('MessagePanel', function() {
|
|||||||
getAvatarUrl: () => {
|
getAvatarUrl: () => {
|
||||||
return "avatar.jpeg";
|
return "avatar.jpeg";
|
||||||
},
|
},
|
||||||
|
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
},
|
},
|
||||||
ts: ts0 + 1,
|
ts: ts0 + 1,
|
||||||
mship: 'join',
|
mship: 'join',
|
||||||
@ -239,6 +242,7 @@ describe('MessagePanel', function() {
|
|||||||
getAvatarUrl: () => {
|
getAvatarUrl: () => {
|
||||||
return "avatar.jpeg";
|
return "avatar.jpeg";
|
||||||
},
|
},
|
||||||
|
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
},
|
},
|
||||||
ts: ts0 + 5,
|
ts: ts0 + 5,
|
||||||
mship: 'invite',
|
mship: 'invite',
|
||||||
|
@ -50,6 +50,7 @@ describe('MemberEventListSummary', function() {
|
|||||||
getAvatarUrl: () => {
|
getAvatarUrl: () => {
|
||||||
return "avatar.jpeg";
|
return "avatar.jpeg";
|
||||||
},
|
},
|
||||||
|
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Override random event ID to allow for equality tests against tiles from
|
// Override random event ID to allow for equality tests against tiles from
|
||||||
|
@ -37,6 +37,7 @@ describe("<TextualBody />", () => {
|
|||||||
getRoom: () => mkStubRoom("room_id"),
|
getRoom: () => mkStubRoom("room_id"),
|
||||||
getAccountData: () => undefined,
|
getAccountData: () => undefined,
|
||||||
isGuest: () => false,
|
isGuest: () => false,
|
||||||
|
mxcUrlToHttp: (s) => s,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ev = mkEvent({
|
const ev = mkEvent({
|
||||||
@ -61,6 +62,7 @@ describe("<TextualBody />", () => {
|
|||||||
getRoom: () => mkStubRoom("room_id"),
|
getRoom: () => mkStubRoom("room_id"),
|
||||||
getAccountData: () => undefined,
|
getAccountData: () => undefined,
|
||||||
isGuest: () => false,
|
isGuest: () => false,
|
||||||
|
mxcUrlToHttp: (s) => s,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ev = mkEvent({
|
const ev = mkEvent({
|
||||||
@ -86,6 +88,7 @@ describe("<TextualBody />", () => {
|
|||||||
getRoom: () => mkStubRoom("room_id"),
|
getRoom: () => mkStubRoom("room_id"),
|
||||||
getAccountData: () => undefined,
|
getAccountData: () => undefined,
|
||||||
isGuest: () => false,
|
isGuest: () => false,
|
||||||
|
mxcUrlToHttp: (s) => s,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,6 +142,7 @@ describe("<TextualBody />", () => {
|
|||||||
on: () => undefined,
|
on: () => undefined,
|
||||||
removeListener: () => undefined,
|
removeListener: () => undefined,
|
||||||
isGuest: () => false,
|
isGuest: () => false,
|
||||||
|
mxcUrlToHttp: (s) => s,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -284,6 +288,7 @@ describe("<TextualBody />", () => {
|
|||||||
getAccountData: () => undefined,
|
getAccountData: () => undefined,
|
||||||
getUrlPreview: (url) => new Promise(() => {}),
|
getUrlPreview: (url) => new Promise(() => {}),
|
||||||
isGuest: () => false,
|
isGuest: () => false,
|
||||||
|
mxcUrlToHttp: (s) => s,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ev = mkEvent({
|
const ev = mkEvent({
|
||||||
|
@ -2,3 +2,5 @@ import * as languageHandler from "../src/languageHandler";
|
|||||||
|
|
||||||
languageHandler.setLanguage('en');
|
languageHandler.setLanguage('en');
|
||||||
languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);
|
languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);
|
||||||
|
|
||||||
|
require('jest-fetch-mock').enableMocks();
|
||||||
|
@ -213,6 +213,7 @@ export function mkStubRoom(roomId = null) {
|
|||||||
rawDisplayName: 'Member',
|
rawDisplayName: 'Member',
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
getAvatarUrl: () => 'mxc://avatar.url/image.png',
|
getAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
|
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
}),
|
}),
|
||||||
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
||||||
getJoinedMembers: jest.fn().mockReturnValue([]),
|
getJoinedMembers: jest.fn().mockReturnValue([]),
|
||||||
@ -242,6 +243,7 @@ export function mkStubRoom(roomId = null) {
|
|||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
getDMInviter: jest.fn(),
|
getDMInviter: jest.fn(),
|
||||||
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||||
|
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
25
yarn.lock
25
yarn.lock
@ -2589,6 +2589,13 @@ crc-32@^0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-0.3.0.tgz#6a3d3687f5baec41f7e9b99fe1953a2e5d19775e"
|
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-0.3.0.tgz#6a3d3687f5baec41f7e9b99fe1953a2e5d19775e"
|
||||||
integrity sha1-aj02h/W67EH36bmf4ZU6Ll0Zd14=
|
integrity sha1-aj02h/W67EH36bmf4ZU6Ll0Zd14=
|
||||||
|
|
||||||
|
cross-fetch@^3.0.4:
|
||||||
|
version "3.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c"
|
||||||
|
integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==
|
||||||
|
dependencies:
|
||||||
|
node-fetch "2.6.1"
|
||||||
|
|
||||||
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||||
version "6.0.5"
|
version "6.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||||
@ -4918,6 +4925,14 @@ jest-environment-node@^26.6.2:
|
|||||||
jest-mock "^26.6.2"
|
jest-mock "^26.6.2"
|
||||||
jest-util "^26.6.2"
|
jest-util "^26.6.2"
|
||||||
|
|
||||||
|
jest-fetch-mock@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
|
||||||
|
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
|
||||||
|
dependencies:
|
||||||
|
cross-fetch "^3.0.4"
|
||||||
|
promise-polyfill "^8.1.3"
|
||||||
|
|
||||||
jest-get-type@^26.3.0:
|
jest-get-type@^26.3.0:
|
||||||
version "26.3.0"
|
version "26.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
|
||||||
@ -5835,6 +5850,11 @@ nice-try@^1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||||
|
|
||||||
|
node-fetch@2.6.1:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||||
|
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||||
|
|
||||||
node-fetch@^1.0.1:
|
node-fetch@^1.0.1:
|
||||||
version "1.7.3"
|
version "1.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||||
@ -6448,6 +6468,11 @@ progress@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||||
|
|
||||||
|
promise-polyfill@^8.1.3:
|
||||||
|
version "8.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0"
|
||||||
|
integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==
|
||||||
|
|
||||||
promise@^7.0.3, promise@^7.1.1:
|
promise@^7.0.3, promise@^7.1.1:
|
||||||
version "7.3.1"
|
version "7.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
||||||
|
Loading…
Reference in New Issue
Block a user