Merge branch 'develop' into i18n-bracket-fix

This commit is contained in:
Germain 2021-08-09 09:07:32 +01:00 committed by GitHub
commit 298318c0ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
363 changed files with 13183 additions and 6322 deletions

View file

@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { formatCallTime } from "../../../DateUtils";
interface IProps {
mxEvent: MatrixEvent;
@ -69,6 +70,18 @@ export default class CallEvent extends React.Component<IProps, IState> {
this.setState({ callState: newState });
};
private renderCallBackButton(text: string): JSX.Element {
return (
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> { text } </span>
</AccessibleButton>
);
}
private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) {
const silenceClass = classNames({
@ -103,17 +116,37 @@ export default class CallEvent extends React.Component<IProps, IState> {
}
if (state === CallState.Ended) {
const hangupReason = this.props.callEventGrouper.hangupReason;
const gotRejected = this.props.callEventGrouper.gotRejected;
if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
if (gotRejected) {
return (
<div className="mx_CallEvent_content">
{ _t("Call declined") }
{ this.renderCallBackButton(_t("Call back")) }
</div>
);
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore)
// Also, if we don't have a reason
const duration = this.props.callEventGrouper.duration;
let text = _t("Call ended");
if (duration) {
text += " • " + formatCallTime(duration);
}
return (
<div className="mx_CallEvent_content">
{ _t("This call has ended") }
{ text }
</div>
);
} else if (hangupReason === CallErrorCode.InviteTimeout) {
return (
<div className="mx_CallEvent_content">
{ _t("Missed call") }
{ this.renderCallBackButton(_t("Call back")) }
</div>
);
}
@ -133,8 +166,6 @@ export default class CallEvent extends React.Component<IProps, IState> {
// (as opposed to an error code they gave but we don't know about,
// in which case we show the error code)
reason = _t("An unknown error occurred");
} else if (hangupReason === CallErrorCode.InviteTimeout) {
reason = _t("No answer");
} else if (hangupReason === CallErrorCode.UserBusy) {
reason = _t("The user you called is busy.");
} else {
@ -148,7 +179,8 @@ export default class CallEvent extends React.Component<IProps, IState> {
className="mx_CallEvent_content_tooltip"
kind={InfoTooltipKind.Warning}
/>
{ _t("This call has failed") }
{ _t("Connection failed") }
{ this.renderCallBackButton(_t("Retry")) }
</div>
);
}
@ -162,14 +194,8 @@ export default class CallEvent extends React.Component<IProps, IState> {
if (state === CustomCallState.Missed) {
return (
<div className="mx_CallEvent_content">
{ _t("You missed this call") }
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> { _t("Call back") } </span>
</AccessibleButton>
{ _t("Missed call") }
{ this.renderCallBackButton(_t("Call back")) }
</div>
);
}
@ -186,11 +212,17 @@ export default class CallEvent extends React.Component<IProps, IState> {
const sender = event.sender ? event.sender.name : event.getSender();
const isVoice = this.props.callEventGrouper.isVoice;
const callType = isVoice ? _t("Voice call") : _t("Video call");
const content = this.renderContent(this.state.callState);
const callState = this.state.callState;
const hangupReason = this.props.callEventGrouper.hangupReason;
const content = this.renderContent(callState);
const className = classNames({
mx_CallEvent: true,
mx_CallEvent_voice: isVoice,
mx_CallEvent_video: !isVoice,
mx_CallEvent_missed: (
callState === CustomCallState.Missed ||
(callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout)
),
});
return (
@ -206,7 +238,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
{ sender }
</div>
<div className="mx_CallEvent_type">
<div className="mx_CallEvent_type_icon"></div>
<div className="mx_CallEvent_type_icon" />
{ callType }
</div>
</div>

View file

@ -16,12 +16,13 @@ limitations under the License.
import { MatrixEvent } from "matrix-js-sdk/src";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import React, { createRef } from "react";
import React from "react";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Spinner from "../elements/Spinner";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { FileDownloader } from "../../../utils/FileDownloader";
interface IProps {
mxEvent: MatrixEvent;
@ -39,7 +40,7 @@ interface IState {
@replaceableComponent("views.messages.DownloadActionButton")
export default class DownloadActionButton extends React.PureComponent<IProps, IState> {
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
private downloader = new FileDownloader();
public constructor(props: IProps) {
super(props);
@ -56,27 +57,21 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
if (this.state.blob) {
// Cheat and trigger a download, again.
return this.onFrameLoad();
return this.doDownload();
}
const blob = await this.props.mediaEventHelperGet().sourceBlob.value;
this.setState({ blob });
await this.doDownload();
};
private onFrameLoad = () => {
this.setState({ loading: false });
// we aren't showing the iframe, so we can send over the bare minimum styles and such.
this.iframe.current.contentWindow.postMessage({
imgSrc: "", // no image
imgStyle: null,
style: "",
private async doDownload() {
await this.downloader.download({
blob: this.state.blob,
download: this.props.mediaEventHelperGet().fileName,
textContent: "",
auto: true, // autodownload
}, '*');
};
name: this.props.mediaEventHelperGet().fileName,
});
this.setState({ loading: false });
}
public render() {
let spinner: JSX.Element;
@ -92,18 +87,11 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
return <RovingAccessibleTooltipButton
className={classes}
title={spinner ? _t("Downloading") : _t("Download")}
title={spinner ? _t("Decrypting") : _t("Download")}
onClick={this.onDownloadClick}
disabled={!!spinner}
>
{ spinner }
{ this.state.blob && <iframe
src="usercontent/" // XXX: Like MFileBody, this should come from the skin
ref={this.iframe}
onLoad={this.onFrameLoad}
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation"
style={{ display: "none" }}
/> }
</RovingAccessibleTooltipButton>;
}
}

View file

@ -16,13 +16,14 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Playback } from "../../../voice/Playback";
import { Playback } from "../../../audio/Playback";
import InlineSpinner from '../elements/InlineSpinner';
import { _t } from "../../../languageHandler";
import AudioPlayer from "../audio_messages/AudioPlayer";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import MFileBody from "./MFileBody";
import { IBodyProps } from "./IBodyProps";
import { PlaybackManager } from "../../../audio/PlaybackManager";
interface IState {
error?: Error;
@ -62,7 +63,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
// We should have a buffer to work with now: let's set it up
const playback = new Playback(buffer, waveform);
const playback = PlaybackManager.instance.createPlaybackInstance(buffer, waveform);
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
this.setState({ playback });
@ -75,7 +76,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
public render() {
if (this.state.error) {
// TODO: @@TR: Verify error state
return (
<span className="mx_MAudioBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -85,7 +85,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
}
if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return (
<span className="mx_MAudioBody">
<InlineSpinner />

View file

@ -23,9 +23,11 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
import { TileShape } from "../rooms/EventTile";
import { IContent } from "matrix-js-sdk/src";
import { presentableTextForFile } from "../../../utils/FileUtils";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip";
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
@ -93,35 +95,6 @@ export function computedStyle(element: HTMLElement) {
return cssText;
}
/**
* Extracts a human readable label for the file attachment to use as
* 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.
*/
export function presentableTextForFile(content: IContent, withSize = true): string {
let linkText = _t("Attachment");
if (content.body && content.body.length > 0) {
// The content body should be the name of the file including a
// file extension.
linkText = content.body;
}
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.
// The content.info also contains a MIME-type but we don't display
// it since it is "ugly", users generally aren't aware what it
// means and the type of the attachment can usually be inferrered
// from the file extension.
linkText += ' (' + filesize(content.info.size) + ')';
}
return linkText;
}
interface IProps extends IBodyProps {
/* whether or not to show the default placeholder for the file. Defaults to true. */
showGenericPlaceholder: boolean;
@ -140,6 +113,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
private userDidClick = false;
private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current);
public constructor(props: IProps) {
super(props);
@ -147,6 +121,32 @@ export default class MFileBody extends React.Component<IProps, IState> {
this.state = {};
}
private get content(): IMediaEventContent {
return this.props.mxEvent.getContent<IMediaEventContent>();
}
private get fileName(): string {
return this.content.body && this.content.body.length > 0 ? this.content.body : _t("Attachment");
}
private get linkText(): string {
return presentableTextForFile(this.content);
}
private downloadFile(fileName: string, text: string) {
this.fileDownloader.download({
blob: this.state.decryptedBlob,
name: fileName,
autoDownload: this.userDidClick,
opts: {
imgSrc: DOWNLOAD_ICON_URL,
imgStyle: null,
style: computedStyle(this.dummyLink.current),
textContent: _t("Download %(text)s", { text }),
},
});
}
private getContentUrl(): string {
const media = mediaFromContent(this.props.mxEvent.getContent());
return media.srcHttp;
@ -158,24 +158,56 @@ export default class MFileBody extends React.Component<IProps, IState> {
}
}
public render() {
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const text = presentableTextForFile(content);
const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
const contentUrl = this.getContentUrl();
const fileSize = content.info ? content.info.size : null;
const fileType = content.info ? content.info.mimetype : "application/octet-stream";
private decryptFile = async (): Promise<void> => {
if (this.state.decryptedBlob) {
return;
}
try {
this.userDidClick = true;
this.setState({
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
});
} catch (err) {
console.warn("Unable to decrypt attachment: ", err);
Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
title: _t("Error"),
description: _t("Error decrypting attachment"),
});
}
};
let placeholder = null;
private onPlaceholderClick = async () => {
const mediaHelper = this.props.mediaEventHelper;
if (mediaHelper.media.isEncrypted) {
await this.decryptFile();
this.downloadFile(this.fileName, this.linkText);
} else {
// As a button we're missing the `download` attribute for styling reasons, so
// download with the file downloader.
this.fileDownloader.download({
blob: await mediaHelper.sourceBlob.value,
name: this.fileName,
});
}
};
public render() {
const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
const contentUrl = this.getContentUrl();
const fileSize = this.content.info ? this.content.info.size : null;
const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream";
let placeholder: React.ReactNode = null;
if (this.props.showGenericPlaceholder) {
placeholder = (
<div className="mx_MFileBody_info">
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
<span className="mx_MFileBody_info_icon" />
<span className="mx_MFileBody_info_filename">
{ presentableTextForFile(content, false) }
</span>
</div>
<TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), true)}>
<span className="mx_MFileBody_info_filename">
{ presentableTextForFile(this.content, _t("Attachment"), true, true) }
</span>
</TextWithTooltip>
</AccessibleButton>
);
}
@ -186,20 +218,6 @@ export default class MFileBody extends React.Component<IProps, IState> {
// Need to decrypt the attachment
// Wait for the user to click on the link before downloading
// and decrypting the attachment.
const decrypt = async () => {
try {
this.userDidClick = true;
this.setState({
decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
});
} catch (err) {
console.warn("Unable to decrypt attachment: ", err);
Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
title: _t("Error"),
description: _t("Error decrypting attachment"),
});
}
};
// This button should actually Download because usercontent/ will try to click itself
// but it is not guaranteed between various browsers' settings.
@ -207,31 +225,14 @@ export default class MFileBody extends React.Component<IProps, IState> {
<span className="mx_MFileBody">
{ placeholder }
{ showDownloadLink && <div className="mx_MFileBody_download">
<AccessibleButton onClick={decrypt}>
{ _t("Decrypt %(text)s", { text: text }) }
<AccessibleButton onClick={this.decryptFile}>
{ _t("Decrypt %(text)s", { text: this.linkText }) }
</AccessibleButton>
</div> }
</span>
);
}
// When the iframe loads we tell it to render a download link
const onIframeLoad = (ev) => {
ev.target.contentWindow.postMessage({
imgSrc: DOWNLOAD_ICON_URL,
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
// will have the correct name when the user tries to download it.
// We can't provide a Content-Disposition header like we would for HTTP.
download: fileName,
textContent: _t("Download %(text)s", { text: text }),
// only auto-download if a user triggered this iframe explicitly
auto: this.userDidClick,
}, "*");
};
const url = "usercontent/"; // XXX: this path should probably be passed from the skin
// If the attachment is encrypted then put the link inside an iframe.
@ -247,9 +248,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
*/ }
<a ref={this.dummyLink} />
</div>
{ /*
TODO: Move iframe (and dummy link) into FileDownloader.
We currently have it set up this way because of styles applied to the iframe
itself which cannot be easily handled/overridden by the FileDownloader. In
future, the download link may disappear entirely at which point it could also
be suitable to just remove this bit of code.
*/ }
<iframe
src={url}
onLoad={onIframeLoad}
onLoad={() => this.downloadFile(this.fileName, this.linkText)}
ref={this.iframe}
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
</div> }
@ -288,7 +296,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
// We have to create an anchor to download the file
const tempAnchor = document.createElement('a');
tempAnchor.download = fileName;
tempAnchor.download = this.fileName;
tempAnchor.href = blobUrl;
document.body.appendChild(tempAnchor); // for firefox: https://stackoverflow.com/a/32226068
tempAnchor.click();
@ -297,7 +305,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
};
} else {
// Else we are hoping the browser will do the right thing
downloadProps["download"] = fileName;
downloadProps["download"] = this.fileName;
}
return (
@ -306,16 +314,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
{ showDownloadLink && <div className="mx_MFileBody_download">
<a {...downloadProps}>
<span className="mx_MFileBody_download_icon" />
{ _t("Download %(text)s", { text: text }) }
{ _t("Download %(text)s", { text: this.linkText }) }
</a>
{ this.props.tileShape === TileShape.FileGrid && <div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
{ this.content.info && this.content.info.size ? filesize(this.content.info.size) : "" }
</div> }
</div> }
</span>
);
} else {
const extra = text ? (': ' + text) : '';
const extra = this.linkText ? (': ' + this.linkText) : '';
return <span className="mx_MFileBody">
{ placeholder }
{ _t("Invalid file%(extra)s", { extra: extra }) }

View file

@ -306,7 +306,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
imageElement = <HiddenImagePlaceholder />;
} else {
imageElement = (
<img style={{ display: 'none' }} src={thumbUrl} ref={this.image}
<img
style={{ display: 'none' }}
src={thumbUrl}
ref={this.image}
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
@ -340,8 +343,11 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size
img = (
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref={this.image}
style={{ maxWidth: maxWidth + "px" }}
<img
className="mx_MImageBody_thumbnail"
src={thumbUrl}
ref={this.image}
style={{ maxWidth: `min(100%, ${maxWidth}px)` }}
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
@ -360,14 +366,15 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
const thumbnail = (
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px", maxWidth: maxWidth + "px" }} >
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
<div style={{ paddingBottom: forcedHeight ? (forcedHeight + "px") : ((100 * infoHeight / infoWidth) + '%') }} />
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight, maxWidth: maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}>
{ showPlaceholder &&
<div className="mx_MImageBody_thumbnail" style={{
// Constrain width here so that spinner appears central to the loaded thumbnail
maxWidth: infoWidth + "px",
}}>
<div
className="mx_MImageBody_thumbnail"
style={{
// Constrain width here so that spinner appears central to the loaded thumbnail
maxWidth: `min(100%, ${infoWidth}px)`,
}}
>
{ placeholder }
</div>
}
@ -406,7 +413,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
// Overidden by MStickerBody
protected getFileBody(): JSX.Element {
protected getFileBody(): string | JSX.Element {
// We only ever need the download bar if we're appearing outside of the timeline
if (this.props.tileShape) {
return <MFileBody {...this.props} showGenericPlaceholder={false} />;
@ -418,10 +425,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
if (this.state.error !== null) {
return (
<span className="mx_MImageBody">
<div className="mx_MImageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting image") }
</span>
</div>
);
}
@ -436,10 +443,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
const fileBody = this.getFileBody();
return <span className="mx_MImageBody">
return <div className="mx_MImageBody">
{ thumbnail }
{ fileBody }
</span>;
</div>;
}
}
@ -454,7 +461,7 @@ export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProp
let className = 'mx_HiddenImagePlaceholder';
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
return (
<div className={className} style={{ maxWidth: maxWidth }}>
<div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
<div className='mx_HiddenImagePlaceholder_button'>
<span className='mx_HiddenImagePlaceholder_eye' />
<span>{ _t("Show image") }</span>

View file

@ -16,9 +16,11 @@ limitations under the License.
import React from "react";
import MImageBody from "./MImageBody";
import { presentableTextForFile } from "./MFileBody";
import { presentableTextForFile } from "../../../utils/FileUtils";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import SenderProfile from "./SenderProfile";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from "../../../languageHandler";
const FORCED_IMAGE_HEIGHT = 44;
@ -32,8 +34,9 @@ export default class MImageReplyBody extends MImageBody {
}
// Don't show "Download this_file.png ..."
public getFileBody(): JSX.Element {
return <>{ presentableTextForFile(this.props.mxEvent.getContent()) }</>;
public getFileBody(): string {
const sticker = this.props.mxEvent.getType() === EventType.Sticker;
return presentableTextForFile(this.props.mxEvent.getContent(), sticker ? _t("Sticker") : _t("Image"), !sticker);
}
render() {

View file

@ -23,16 +23,16 @@ import { BLURHASH_FIELD } from "../../../ContentMessages";
@replaceableComponent("views.messages.MStickerBody")
export default class MStickerBody extends MImageBody {
// Mostly empty to prevent default behaviour of MImageBody
onClick(ev) {
protected onClick = (ev: React.MouseEvent) => {
ev.preventDefault();
if (!this.state.showImage) {
this.showImage();
}
}
};
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
// which is added by mx_MStickerBody_wrapper
wrapImage(contentUrl, children) {
protected wrapImage(contentUrl: string, children: React.ReactNode): JSX.Element {
let onClick = null;
if (!this.state.showImage) {
onClick = this.onClick;
@ -42,13 +42,13 @@ export default class MStickerBody extends MImageBody {
// Placeholder to show in place of the sticker image if
// img onLoad hasn't fired yet.
getPlaceholder(width, height) {
protected getPlaceholder(width: number, height: number): JSX.Element {
if (this.props.mxEvent.getContent().info[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
return <img src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
}
// Tooltip to show on mouse over
getTooltip() {
protected getTooltip(): JSX.Element {
const content = this.props.mxEvent && this.props.mxEvent.getContent();
if (!content || !content.body || !content.info || !content.info.w) return null;
@ -60,7 +60,7 @@ export default class MStickerBody extends MImageBody {
}
// Don't show "Download this_file.png ..."
getFileBody() {
protected getFileBody() {
return null;
}
}

View file

@ -267,8 +267,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
width={width}
poster={poster}
onPlay={this.videoOnPlay}
>
</video>
/>
{ this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
</span>
);

View file

@ -27,7 +27,6 @@ export default class MVoiceMessageBody extends MAudioBody {
// A voice message is an audio file but rendered in a special way.
public render() {
if (this.state.error) {
// TODO: @@TR: Verify error state
return (
<span className="mx_MVoiceMessageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -37,7 +36,6 @@ export default class MVoiceMessageBody extends MAudioBody {
}
if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return (
<span className="mx_MVoiceMessageBody">
<InlineSpinner />

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import MAudioBody from "./MAudioBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import MVoiceMessageBody from "./MVoiceMessageBody";
import { IBodyProps } from "./IBodyProps";
@ -27,8 +26,7 @@ export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
// MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
|| !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
if (isVoiceMessage && voiceMessagesEnabled) {
if (isVoiceMessage) {
return <MVoiceMessageBody {...this.props} />;
} else {
return <MAudioBody {...this.props} />;

View file

@ -78,8 +78,11 @@ export default class RoomAvatarEvent extends React.Component {
{ senderDisplayName: senderDisplayName },
{
'img': () =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick}>
<AccessibleButton
key="avatar"
className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick}
>
<RoomAvatar width={14} height={14} oobData={oobData} />
</AccessibleButton>,
})

View file

@ -136,7 +136,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private addCodeExpansionButton(div: HTMLDivElement, pre: HTMLPreElement): void {
// Calculate how many percent does the pre element take up.
// If it's less than 30% we don't add the expansion button.
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
// We also round the number as it sometimes can be 29.99...
const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
if (percentageOfViewport < 30) return;
const button = document.createElement("span");
@ -514,7 +515,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
switch (content.msgtype) {
case MsgType.Emote:
return (
<span className="mx_MEmoteBody mx_EventTile_content">
<div className="mx_MEmoteBody mx_EventTile_content">
*&nbsp;
<span
className="mx_MEmoteBody_sender"
@ -525,21 +526,21 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
&nbsp;
{ body }
{ widgets }
</span>
</div>
);
case MsgType.Notice:
return (
<span className="mx_MNoticeBody mx_EventTile_content">
<div className="mx_MNoticeBody mx_EventTile_content">
{ body }
{ widgets }
</span>
</div>
);
default: // including "m.text"
return (
<span className="mx_MTextBody mx_EventTile_content">
<div className="mx_MTextBody mx_EventTile_content">
{ body }
{ widgets }
</span>
</div>
);
}
}

View file

@ -15,18 +15,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixEvent } from 'matrix-js-sdk/src';
import classNames from 'classnames';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};
interface IProps {
mxEvent: MatrixEvent;
}
interface IState {
expanded: boolean;
}
@replaceableComponent("views.messages.ViewSourceEvent")
export default class ViewSourceEvent extends React.PureComponent<IProps, IState> {
constructor(props) {
super(props);
@ -35,7 +38,7 @@ export default class ViewSourceEvent extends React.PureComponent {
};
}
componentDidMount() {
public componentDidMount(): void {
const { mxEvent } = this.props;
const client = MatrixClientPeg.get();
@ -46,15 +49,15 @@ export default class ViewSourceEvent extends React.PureComponent {
}
}
onToggle = (ev) => {
private onToggle = (ev: React.MouseEvent) => {
ev.preventDefault();
const { expanded } = this.state;
this.setState({
expanded: !expanded,
});
}
};
render() {
public render(): React.ReactNode {
const { mxEvent } = this.props;
const { expanded } = this.state;