Merge branch 'develop' into improve-image-view
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
b27a8e3c3a
489 changed files with 13551 additions and 3187 deletions
|
@ -19,6 +19,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {formatFullDateNoTime} from '../../../DateUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
function getdaysArray() {
|
||||
return [
|
||||
|
@ -32,6 +33,7 @@ function getdaysArray() {
|
|||
];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.DateSeparator")
|
||||
export default class DateSeparator extends React.Component {
|
||||
static propTypes = {
|
||||
ts: PropTypes.number.isRequired,
|
||||
|
|
|
@ -27,12 +27,14 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import Modal from '../../../Modal';
|
||||
import classNames from 'classnames';
|
||||
import RedactedBody from "./RedactedBody";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
function getReplacedContent(event) {
|
||||
const originalContent = event.getOriginalContent();
|
||||
return originalContent["m.new_content"] || originalContent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.EditHistoryMessage")
|
||||
export default class EditHistoryMessage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// the message event being edited
|
||||
|
@ -74,9 +76,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
_onViewSourceClick = () => {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
content: this.props.mxEvent.event,
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
};
|
||||
|
||||
|
@ -158,6 +158,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
|||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
|
||||
const classes = classNames({
|
||||
"mx_EventTile": true,
|
||||
// Note: we keep the `sending` state class for tests, not for our styles
|
||||
"mx_EventTile_sending": isSending,
|
||||
"mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
|
||||
});
|
||||
|
|
|
@ -14,16 +14,16 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MFileBody from './MFileBody';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.messages.MAudioBody")
|
||||
export default class MAudioBody extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -41,11 +41,11 @@ export default class MAudioBody extends React.Component {
|
|||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,7 +105,7 @@ export default class MAudioBody extends React.Component {
|
|||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<audio src={contentUrl} controls />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2015, 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,51 +17,24 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import filesize from 'filesize';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {decryptFile} from '../../../utils/DecryptFile';
|
||||
import Tinter from '../../../Tinter';
|
||||
import request from 'browser-request';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
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")
|
||||
let tintedDownloadImageURL;
|
||||
// Track a list of mounted MFileBody instances so that we can update
|
||||
// the require("../../../../res/img/download.svg") when the tint changes.
|
||||
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();
|
||||
});
|
||||
});
|
||||
async function cacheDownloadIcon() {
|
||||
if (downloadIconUrl) return; // cached already
|
||||
const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text());
|
||||
downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||
}
|
||||
|
||||
Tinter.registerTintable(updateTintedDownloadImage);
|
||||
// Cache the asset immediately
|
||||
cacheDownloadIcon();
|
||||
|
||||
// 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
|
||||
|
@ -105,6 +77,7 @@ function computedStyle(element) {
|
|||
}
|
||||
const style = window.getComputedStyle(element, null);
|
||||
let cssText = style.cssText;
|
||||
// noinspection EqualityComparisonWithCoercionJS
|
||||
if (cssText == "") {
|
||||
// Firefox doesn't implement ".cssText" for computed styles.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||
|
@ -116,6 +89,7 @@ function computedStyle(element) {
|
|||
return cssText;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MFileBody")
|
||||
export default class MFileBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -126,6 +100,12 @@ export default class MFileBody extends React.Component {
|
|||
onHeightChanged: PropTypes.func,
|
||||
/* the shape of the tile, used */
|
||||
tileShape: PropTypes.string,
|
||||
/* whether or not to show the default placeholder for the file. Defaults to true. */
|
||||
showGenericPlaceholder: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showGenericPlaceholder: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -137,7 +117,6 @@ export default class MFileBody extends React.Component {
|
|||
|
||||
this._iframe = createRef();
|
||||
this._dummyLink = createRef();
|
||||
this._downloadImage = createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,9 +124,10 @@ export default class MFileBody extends React.Component {
|
|||
* link text.
|
||||
*
|
||||
* @param {Object} content The "content" key of the matrix event.
|
||||
* @param {boolean} withSize Whether to include size information. Default true.
|
||||
* @return {string} the human readable link text for the attachment.
|
||||
*/
|
||||
presentableTextForFile(content) {
|
||||
presentableTextForFile(content, withSize = true) {
|
||||
let linkText = _t("Attachment");
|
||||
if (content.body && content.body.length > 0) {
|
||||
// The content body should be the name of the file including a
|
||||
|
@ -155,7 +135,7 @@ export default class MFileBody extends React.Component {
|
|||
linkText = content.body;
|
||||
}
|
||||
|
||||
if (content.info && content.info.size) {
|
||||
if (content.info && content.info.size && withSize) {
|
||||
// If we know the size of the file then add it as human readable
|
||||
// string to the end of the link text so that the user knows how
|
||||
// big a file they are downloading.
|
||||
|
@ -169,16 +149,8 @@ export default class MFileBody extends React.Component {
|
|||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
}
|
||||
|
||||
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();
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -187,37 +159,25 @@ 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() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const text = this.presentableTextForFile(content);
|
||||
const isEncrypted = content.file !== undefined;
|
||||
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
|
||||
const contentUrl = this._getContentUrl();
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const fileSize = content.info ? content.info.size : null;
|
||||
const fileType = content.info ? content.info.mimetype : "application/octet-stream";
|
||||
|
||||
let placeholder = null;
|
||||
if (this.props.showGenericPlaceholder) {
|
||||
placeholder = (
|
||||
<div className="mx_MFileBody_info">
|
||||
<span className="mx_MFileBody_info_icon" />
|
||||
<span className="mx_MFileBody_info_filename">{this.presentableTextForFile(content, false)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEncrypted) {
|
||||
if (this.state.decryptedBlob === null) {
|
||||
// Need to decrypt the attachment
|
||||
|
@ -248,6 +208,7 @@ export default class MFileBody extends React.Component {
|
|||
// but it is not guaranteed between various browsers' settings.
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
{placeholder}
|
||||
<div className="mx_MFileBody_download">
|
||||
<AccessibleButton onClick={decrypt}>
|
||||
{ _t("Decrypt %(text)s", { text: text }) }
|
||||
|
@ -260,7 +221,8 @@ export default class MFileBody extends React.Component {
|
|||
// When the iframe loads we tell it to render a download link
|
||||
const onIframeLoad = (ev) => {
|
||||
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),
|
||||
blob: this.state.decryptedBlob,
|
||||
// Set a download attribute for encrypted files so that the file
|
||||
|
@ -278,6 +240,7 @@ export default class MFileBody extends React.Component {
|
|||
// If the attachment is encrypted then put the link inside an iframe.
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
{placeholder}
|
||||
<div className="mx_MFileBody_download">
|
||||
<div style={{display: "none"}}>
|
||||
{ /*
|
||||
|
@ -346,6 +309,7 @@ export default class MFileBody extends React.Component {
|
|||
if (this.props.tileShape === "file_grid") {
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
{placeholder}
|
||||
<div className="mx_MFileBody_download">
|
||||
<a className="mx_MFileBody_downloadLink" {...downloadProps}>
|
||||
{ fileName }
|
||||
|
@ -359,9 +323,10 @@ export default class MFileBody extends React.Component {
|
|||
} else {
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
{placeholder}
|
||||
<div className="mx_MFileBody_download">
|
||||
<a {...downloadProps}>
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
|
||||
<span className="mx_MFileBody_download_icon" />
|
||||
{ _t("Download %(text)s", { text: text }) }
|
||||
</a>
|
||||
</div>
|
||||
|
@ -371,6 +336,7 @@ export default class MFileBody extends React.Component {
|
|||
} else {
|
||||
const extra = text ? (': ' + text) : '';
|
||||
return <span className="mx_MFileBody">
|
||||
{placeholder}
|
||||
{ _t("Invalid file%(extra)s", { extra: extra }) }
|
||||
</span>;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,10 @@ import { _t } from '../../../languageHandler';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.messages.MImageBody")
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -71,7 +74,7 @@ export default class MImageBody extends React.Component {
|
|||
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) {
|
||||
if (this.unmounted) return;
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
|
@ -169,16 +172,16 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return this.context.mxcUrlToHttp(content.url);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
_getThumbUrl() {
|
||||
// FIXME: the dharma skin lets images grow as wide as you like, rather than capped to 800x600.
|
||||
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
||||
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||
// thumbnail resolution will be unnecessarily reduced.
|
||||
// custom timeline widths seems preferable.
|
||||
|
@ -187,21 +190,19 @@ export default class MImageBody extends React.Component {
|
|||
const thumbHeight = Math.round(600 * pixelRatio);
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(content);
|
||||
|
||||
if (media.isEncrypted) {
|
||||
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||
if (this.state.decryptedThumbnailUrl) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
}
|
||||
return this.state.decryptedUrl;
|
||||
} else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
|
||||
} else if (content.info && content.info.mimetype === "image/svg+xml" && media.hasThumbnail) {
|
||||
// special case to return clientside sender-generated thumbnails for SVGs, if any,
|
||||
// given we deliberately don't thumbnail them serverside to prevent
|
||||
// billion lol attacks and similar
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.info.thumbnail_url,
|
||||
thumbWidth,
|
||||
thumbHeight,
|
||||
);
|
||||
return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale');
|
||||
} else {
|
||||
// we try to download the correct resolution
|
||||
// for hi-res images (like retina screenshots).
|
||||
|
@ -220,7 +221,7 @@ export default class MImageBody extends React.Component {
|
|||
pixelRatio === 1.0 ||
|
||||
(!info || !info.w || !info.h || !info.size)
|
||||
) {
|
||||
return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
|
||||
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
|
||||
} else {
|
||||
// we should only request thumbnails if the image is bigger than 800x600
|
||||
// (or 1600x1200 on retina) otherwise the image in the timeline will just
|
||||
|
@ -235,24 +236,17 @@ export default class MImageBody extends React.Component {
|
|||
info.w > thumbWidth ||
|
||||
info.h > thumbHeight
|
||||
);
|
||||
const isLargeFileSize = info.size > 1*1024*1024;
|
||||
const isLargeFileSize = info.size > 1*1024*1024; // 1mb
|
||||
|
||||
if (isLargeFileSize && isLargerThanThumbnail) {
|
||||
// image is too large physically and bytewise to clutter our timeline so
|
||||
// we ask for a thumbnail, despite knowing that it will be max 800x600
|
||||
// despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.url,
|
||||
thumbWidth,
|
||||
thumbHeight,
|
||||
);
|
||||
return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
|
||||
} else {
|
||||
// download the original image otherwise, so we can scale it client side
|
||||
// to take pixelRatio into account.
|
||||
// ( no width/height means we want the original image)
|
||||
return this.context.mxcUrlToHttp(
|
||||
content.url,
|
||||
);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -366,7 +360,7 @@ export default class MImageBody extends React.Component {
|
|||
}
|
||||
|
||||
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 240, infoHeight);
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||
// The maximum width of the thumbnail, as dictated by its natural
|
||||
// maximum height.
|
||||
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||
|
@ -456,7 +450,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
// Overidden by MStickerBody
|
||||
getFileBody() {
|
||||
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />;
|
||||
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -21,11 +21,13 @@ import WidgetStore from "../../../stores/WidgetStore";
|
|||
import EventTileBubble from "./EventTileBubble";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MJitsiWidgetEvent")
|
||||
export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -22,7 +22,9 @@ import { _t } from '../../../languageHandler';
|
|||
import {getNameForEventRoom, userLabelForEventRoom}
|
||||
from '../../../utils/KeyVerificationStateObserver';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationConclusion")
|
||||
export default class MKeyVerificationConclusion extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -25,7 +25,9 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.MKeyVerificationRequest")
|
||||
export default class MKeyVerificationRequest extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -17,7 +17,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import MImageBody from './MImageBody';
|
||||
import * as sdk from '../../../index';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.MStickerBody")
|
||||
export default class MStickerBody extends MImageBody {
|
||||
// Mostly empty to prevent default behaviour of MImageBody
|
||||
onClick(ev) {
|
||||
|
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import MFileBody from './MFileBody';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import InlineSpinner from '../elements/InlineSpinner';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromContent} from "../../../customisations/Media";
|
||||
|
||||
interface IProps {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -38,6 +39,7 @@ interface IState {
|
|||
fetchingData: boolean,
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.MVideoBody")
|
||||
export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||
private videoRef = React.createRef<HTMLVideoElement>();
|
||||
|
||||
|
@ -74,11 +76,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private getContentUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
return media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,10 +91,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private getThumbUrl(): string|null {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
const media = mediaFromContent(content);
|
||||
if (media.isEncrypted) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
} else if (content.info && content.info.thumbnail_url) {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
||||
} else if (media.hasThumbnail) {
|
||||
return media.thumbnailHttp;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -243,7 +246,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
|||
onPlay={this.videoOnPlay}
|
||||
>
|
||||
</video>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
|
|||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -101,6 +102,7 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
|
|||
</React.Fragment>;
|
||||
};
|
||||
|
||||
@replaceableComponent("views.messages.MessageActionBar")
|
||||
export default class MessageActionBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
|
|
@ -21,7 +21,9 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import {Mjolnir} from "../../../mjolnir/Mjolnir";
|
||||
import RedactedBody from "./RedactedBody";
|
||||
import UnknownBody from "./UnknownBody";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.MessageEvent")
|
||||
export default class MessageEvent extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
|
|
@ -18,7 +18,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {formatFullDate, formatTime} from '../../../DateUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.MessageTimestamp")
|
||||
export default class MessageTimestamp extends React.Component {
|
||||
static propTypes = {
|
||||
ts: PropTypes.number.isRequired,
|
||||
|
|
|
@ -17,7 +17,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.MjolnirBody")
|
||||
export default class MjolnirBody extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
|
|
@ -21,10 +21,12 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
// The maximum number of reactions to initially show on a message.
|
||||
const MAX_ITEMS_WHEN_LIMITED = 8;
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRow")
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
|
|
|
@ -23,7 +23,9 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButton")
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
|
|
|
@ -22,7 +22,9 @@ import * as sdk from '../../../index';
|
|||
import { unicodeToShortcode } from '../../../HtmlUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
|
||||
export default class ReactionsRowButtonTooltip extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
|
|
|
@ -23,7 +23,10 @@ import { _t } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.messages.RoomAvatarEvent")
|
||||
export default class RoomAvatarEvent extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -33,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component {
|
|||
onAvatarClick = () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ev = this.props.mxEvent;
|
||||
const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
|
||||
const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
|
||||
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||
|
|
|
@ -23,7 +23,9 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import EventTileBubble from "./EventTileBubble";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.RoomCreate")
|
||||
export default class RoomCreate extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
|
|
@ -18,14 +18,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.SenderProfile")
|
||||
export default class SenderProfile extends React.Component {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
|
||||
text: PropTypes.string, // Text to show. Defaults to sender name
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
|
@ -118,17 +118,10 @@ export default class SenderProfile extends React.Component {
|
|||
{ flair }
|
||||
</span>;
|
||||
|
||||
const content = this.props.text ?
|
||||
<span>
|
||||
<span className="mx_SenderProfile_aux">
|
||||
{ _t(this.props.text, { senderName: () => nameElem }) }
|
||||
</span>
|
||||
</span> : nameFlair;
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
||||
<div className="mx_SenderProfile_hover">
|
||||
{ content }
|
||||
{ nameFlair }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -35,7 +35,9 @@ import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
|||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.TextualBody")
|
||||
export default class TextualBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
@ -99,6 +101,10 @@ export default class TextualBody extends React.Component {
|
|||
// If there already is a div wrapping the codeblock we want to skip this.
|
||||
// This happens after the codeblock was edited.
|
||||
if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
|
||||
// Add code element if it's missing since we depend on it
|
||||
if (pres[i].getElementsByTagName("code").length == 0) {
|
||||
this._addCodeElement(pres[i]);
|
||||
}
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = this._wrapInDiv(pres[i]);
|
||||
|
@ -128,6 +134,12 @@ export default class TextualBody extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_addCodeElement(pre) {
|
||||
const code = document.createElement("code");
|
||||
code.append(...pre.childNodes);
|
||||
pre.appendChild(code);
|
||||
}
|
||||
|
||||
_addCodeExpansionButton(div, pre) {
|
||||
// Calculate how many percent does the pre element take up.
|
||||
// If it's less than 30% we don't add the expansion button.
|
||||
|
|
|
@ -18,7 +18,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.TextualEvent")
|
||||
export default class TextualEvent extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
|
|
@ -20,7 +20,9 @@ import { _t } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.TileErrorBoundary")
|
||||
export default class TileErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -17,7 +17,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.messages.ViewSourceEvent")
|
||||
export default class ViewSourceEvent extends React.PureComponent {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue