Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering

This commit is contained in:
Tulir Asokan 2021-07-02 12:39:51 +03:00
commit 5240209529
782 changed files with 11943 additions and 10094 deletions

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 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.
@ -16,12 +16,12 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {formatFullDateNoTime} from '../../../DateUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent";
function getdaysArray() {
import { _t } from '../../../languageHandler';
import { formatFullDateNoTime } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
function getDaysArray(): string[] {
return [
_t('Sunday'),
_t('Monday'),
@ -33,17 +33,17 @@ function getdaysArray() {
];
}
@replaceableComponent("views.messages.DateSeparator")
export default class DateSeparator extends React.Component {
static propTypes = {
ts: PropTypes.number.isRequired,
};
interface IProps {
ts: number;
}
getLabel() {
@replaceableComponent("views.messages.DateSeparator")
export default class DateSeparator extends React.Component<IProps> {
private getLabel() {
const date = new Date(this.props.ts);
const today = new Date();
const yesterday = new Date();
const days = getdaysArray();
const days = getDaysArray();
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {

View file

@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
import {formatTime} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import { formatTime } from '../../../DateUtils';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import classNames from 'classnames';
import RedactedBody from "./RedactedBody";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
function getReplacedContent(event) {
const originalContent = event.getOriginalContent();
@ -46,21 +46,21 @@ export default class EditHistoryMessage extends React.PureComponent {
constructor(props) {
super(props);
const cli = MatrixClientPeg.get();
const {userId} = cli.credentials;
const { userId } = cli.credentials;
const event = this.props.mxEvent;
const room = cli.getRoom(event.getRoomId());
if (event.localRedactionEvent()) {
event.localRedactionEvent().on("status", this._onAssociatedStatusChanged);
}
const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
this._content = createRef();
this._pills = [];
}
_onAssociatedStatusChanged = () => {
this.setState({sendStatus: this.props.mxEvent.getAssociatedStatus()});
this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() });
};
_onRedactClick = async () => {
@ -129,7 +129,7 @@ export default class EditHistoryMessage extends React.PureComponent {
}
render() {
const {mxEvent} = this.props;
const { mxEvent } = this.props;
const content = getReplacedContent(mxEvent);
let contentContainer;
if (mxEvent.isRedacted()) {
@ -139,7 +139,7 @@ export default class EditHistoryMessage extends React.PureComponent {
if (this.props.previousEdit) {
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
} else {
contentElements = HtmlUtils.bodyToHtml(content, null, {stripReplyFallback: true});
contentElements = HtmlUtils.bodyToHtml(content, null, { stripReplyFallback: true });
}
if (mxEvent.getContent().msgtype === "m.emote") {
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {forwardRef, useContext} from 'react';
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import React, { forwardRef, useContext } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -27,7 +27,7 @@ interface IProps {
mxEvent: MatrixEvent;
}
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({mxEvent}, ref) => {
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent }, ref) => {
const cli = useContext(MatrixClientContext);
const roomId = mxEvent.getRoomId();
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {forwardRef, ReactNode, ReactChildren} from "react";
import React, { forwardRef, ReactNode, ReactChildren } from "react";
import classNames from "classnames";
interface IProps {

View file

@ -1,112 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
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 React from 'react';
import MFileBody from './MFileBody';
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);
this.state = {
playing: false,
decryptedUrl: null,
decryptedBlob: null,
error: null,
};
}
onPlayToggle() {
this.setState({
playing: !this.state.playing,
});
}
_getContentUrl() {
const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) {
return this.state.decryptedUrl;
} else {
return media.srcHttp;
}
}
componentDidMount() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let decryptedBlob;
decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return URL.createObjectURL(decryptedBlob);
}).then((url) => {
this.setState({
decryptedUrl: url,
decryptedBlob: decryptedBlob,
});
}, (err) => {
console.warn("Unable to decrypt attachment: ", err);
this.setState({
error: err,
});
});
}
}
componentWillUnmount() {
if (this.state.decryptedUrl) {
URL.revokeObjectURL(this.state.decryptedUrl);
}
}
render() {
const content = this.props.mxEvent.getContent();
if (this.state.error !== null) {
return (
<span className="mx_MAudioBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
{ _t("Error decrypting audio") }
</span>
);
}
if (content.file !== undefined && this.state.decryptedUrl === null) {
// Need to decrypt the attachment
// The attachment is decrypted in componentDidMount.
// For now add an img tag with a 16x16 spinner.
// Not sure how tall the audio player is so not sure how tall it should actually be.
return (
<span className="mx_MAudioBody">
<InlineSpinner />
</span>
);
}
const contentUrl = this._getContentUrl();
return (
<span className="mx_MAudioBody">
<audio src={contentUrl} controls />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
</span>
);
}
}

View file

@ -0,0 +1,110 @@
/*
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 React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Playback } from "../../../voice/Playback";
import MFileBody from "./MFileBody";
import InlineSpinner from '../elements/InlineSpinner';
import { _t } from "../../../languageHandler";
import { mediaFromContent } from "../../../customisations/Media";
import { decryptFile } from "../../../utils/DecryptFile";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import AudioPlayer from "../audio_messages/AudioPlayer";
interface IProps {
mxEvent: MatrixEvent;
}
interface IState {
error?: Error;
playback?: Playback;
decryptedBlob?: Blob;
}
@replaceableComponent("views.messages.MAudioBody")
export default class MAudioBody extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
public async componentDidMount() {
let buffer: ArrayBuffer;
const content: IMediaEventContent = this.props.mxEvent.getContent();
const media = mediaFromContent(content);
if (media.isEncrypted) {
try {
const blob = await decryptFile(content.file);
buffer = await blob.arrayBuffer();
this.setState({ decryptedBlob: blob });
} catch (e) {
this.setState({ error: e });
console.warn("Unable to decrypt audio message", e);
return; // stop processing the audio file
}
} else {
try {
buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
} catch (e) {
this.setState({ error: e });
console.warn("Unable to download audio message", e);
return; // stop processing the audio file
}
}
// We should have a buffer to work with now: let's set it up
const playback = new Playback(buffer);
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
this.setState({ playback });
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
}
public componentWillUnmount() {
this.state.playback?.destroy();
}
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" />
{ _t("Error processing audio message") }
</span>
);
}
if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return (
<span className="mx_MAudioBody">
<InlineSpinner />
</span>
);
}
// At this point we should have a playable state
return (
<span className="mx_MAudioBody">
<AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
</span>
);
}
}

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import filesize from 'filesize';
import { _t } from '../../../languageHandler';
import {decryptFile} from '../../../utils/DecryptFile';
import { decryptFile } from '../../../utils/DecryptFile';
import Modal from '../../../Modal';
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromContent} from "../../../customisations/Media";
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
@ -242,7 +242,7 @@ export default class MFileBody extends React.Component {
<span className="mx_MFileBody">
{placeholder}
<div className="mx_MFileBody_download">
<div style={{display: "none"}}>
<div style={{ display: "none" }}>
{ /*
* Add dummy copy of the "a" tag
* We'll use it to learn how the download link

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import MFileBody from './MFileBody';
@ -27,8 +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";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
import { BLURHASH_FIELD } from "../../../ContentMessages";
@replaceableComponent("views.messages.MImageBody")
export default class MImageBody extends React.Component {
@ -90,7 +92,7 @@ export default class MImageBody extends React.Component {
showImage() {
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
this.setState({showImage: true});
this.setState({ showImage: true });
this._downloadImage();
}
@ -296,7 +298,7 @@ export default class MImageBody extends React.Component {
if (showImage) {
// Don't download anything becaue we don't want to display anything.
this._downloadImage();
this.setState({showImage: true});
this.setState({ showImage: true });
}
this._afterComponentDidMount();
@ -333,7 +335,8 @@ export default class MImageBody extends React.Component {
infoWidth = content.info.w;
infoHeight = content.info.h;
} else {
// Whilst the image loads, display nothing.
// Whilst the image loads, display nothing. We also don't display a blurhash image
// because we don't really know what size of image we'll end up with.
//
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
//
@ -345,7 +348,7 @@ export default class MImageBody extends React.Component {
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}
@ -368,12 +371,8 @@ export default class MImageBody extends React.Component {
let placeholder = null;
let gifLabel = null;
// e2e image hasn't been decrypted yet
if (content.file !== undefined && this.state.decryptedUrl === null) {
placeholder = <InlineSpinner w={32} h={32} />;
} else if (!this.state.imgLoaded) {
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
placeholder = this.getPlaceholder();
if (!this.state.imgLoaded) {
placeholder = this.getPlaceholder(maxWidth, maxHeight);
}
let showPlaceholder = Boolean(placeholder);
@ -395,7 +394,7 @@ export default class MImageBody extends React.Component {
if (!this.state.showImage) {
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon.
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
}
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
@ -411,13 +410,11 @@ export default class MImageBody extends React.Component {
// Constrain width here so that spinner appears central to the loaded thumbnail
maxWidth: infoWidth + "px",
}}>
<div className="mx_MImageBody_thumbnail_spinner">
{ placeholder }
</div>
{ placeholder }
</div>
}
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
<div style={{ display: !showPlaceholder ? undefined : 'none' }}>
{ img }
{ gifLabel }
</div>
@ -437,9 +434,12 @@ export default class MImageBody extends React.Component {
}
// Overidden by MStickerBody
getPlaceholder() {
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
return null;
getPlaceholder(width, height) {
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
if (blurhash) return <BlurhashPlaceholder blurhash={blurhash} width={width} height={height} />;
return <div className="mx_MImageBody_thumbnail_spinner">
<InlineSpinner w={32} h={32} />
</div>;
}
// Overidden by MStickerBody

View file

@ -21,7 +21,7 @@ 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";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
mxEvent: MatrixEvent;
@ -52,20 +52,20 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
// removed
return <EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t('Video conference ended by %(senderName)s', {senderName})}
title={_t('Video conference ended by %(senderName)s', { senderName })}
/>;
} else if (prevUrl) {
// modified
return <EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t('Video conference updated by %(senderName)s', {senderName})}
title={_t('Video conference updated by %(senderName)s', { senderName })}
subtitle={joinCopy}
/>;
} else {
// assume added
return <EventTileBubble
className="mx_MJitsiWidgetEvent"
title={_t("Video conference started by %(senderName)s", {senderName})}
title={_t("Video conference started by %(senderName)s", { senderName })}
subtitle={joinCopy}
/>;
}

View file

@ -17,12 +17,12 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import {getNameForEventRoom, userLabelForEventRoom}
import { getNameForEventRoom, userLabelForEventRoom }
from '../../../utils/KeyVerificationStateObserver';
import EventTileBubble from "./EventTileBubble";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.messages.MKeyVerificationConclusion")
export default class MKeyVerificationConclusion extends React.Component {
@ -90,7 +90,7 @@ export default class MKeyVerificationConclusion extends React.Component {
}
render() {
const {mxEvent} = this.props;
const { mxEvent } = this.props;
const request = mxEvent.verificationRequest;
if (!this._shouldRender(mxEvent, request)) {
@ -103,15 +103,15 @@ export default class MKeyVerificationConclusion extends React.Component {
let title;
if (request.done) {
title = _t("You verified %(name)s", {name: getNameForEventRoom(request.otherUserId, mxEvent)});
title = _t("You verified %(name)s", { name: getNameForEventRoom(request.otherUserId, mxEvent) });
} else if (request.cancelled) {
const userId = request.cancellingUserId;
if (userId === myUserId) {
title = _t("You cancelled verifying %(name)s",
{name: getNameForEventRoom(request.otherUserId, mxEvent)});
{ name: getNameForEventRoom(request.otherUserId, mxEvent) });
} else {
title = _t("%(name)s cancelled verifying",
{name: getNameForEventRoom(userId, mxEvent)});
{ name: getNameForEventRoom(userId, mxEvent) });
}
}

View file

@ -15,58 +15,57 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixEvent } from 'matrix-js-sdk/src';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {getNameForEventRoom, userLabelForEventRoom}
import { getNameForEventRoom, userLabelForEventRoom }
from '../../../utils/KeyVerificationStateObserver';
import dis from "../../../dispatcher/dispatcher";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import { Action } from "../../../dispatcher/actions";
import EventTileBubble from "./EventTileBubble";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.MKeyVerificationRequest")
export default class MKeyVerificationRequest extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
export default class MKeyVerificationRequest extends React.Component<IProps> {
public componentDidMount() {
const request = this.props.mxEvent.verificationRequest;
if (request) {
request.on("change", this._onRequestChanged);
request.on("change", this.onRequestChanged);
}
}
componentWillUnmount() {
public componentWillUnmount() {
const request = this.props.mxEvent.verificationRequest;
if (request) {
request.off("change", this._onRequestChanged);
request.off("change", this.onRequestChanged);
}
}
_openRequest = () => {
const {verificationRequest} = this.props.mxEvent;
private openRequest = () => {
const { verificationRequest } = this.props.mxEvent;
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
dis.dispatch({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {verificationRequest, member},
refireParams: { verificationRequest, member },
});
};
_onRequestChanged = () => {
private onRequestChanged = () => {
this.forceUpdate();
};
_onAcceptClicked = async () => {
private onAcceptClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
this._openRequest();
this.openRequest();
await request.accept();
} catch (err) {
console.error(err.message);
@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
};
_onRejectClicked = async () => {
private onRejectClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@ -85,20 +84,20 @@ export default class MKeyVerificationRequest extends React.Component {
}
};
_acceptedLabel(userId) {
private acceptedLabel(userId: string) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
if (userId === myUserId) {
return _t("You accepted");
} else {
return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
return _t("%(name)s accepted", { name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId()) });
}
}
_cancelledLabel(userId) {
private cancelledLabel(userId: string) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
const {cancellationCode} = this.props.mxEvent.verificationRequest;
const { cancellationCode } = this.props.mxEvent.verificationRequest;
const declined = cancellationCode === "m.user";
if (userId === myUserId) {
if (declined) {
@ -108,18 +107,17 @@ export default class MKeyVerificationRequest extends React.Component {
}
} else {
if (declined) {
return _t("%(name)s declined", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
return _t("%(name)s declined", { name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId()) });
} else {
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
return _t("%(name)s cancelled", { name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId()) });
}
}
}
render() {
public render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const FormButton = sdk.getComponent("elements.FormButton");
const {mxEvent} = this.props;
const { mxEvent } = this.props;
const request = mxEvent.verificationRequest;
if (!request || request.invalid) {
@ -134,11 +132,11 @@ export default class MKeyVerificationRequest extends React.Component {
let stateLabel;
const accepted = request.ready || request.started || request.done;
if (accepted) {
stateLabel = (<AccessibleButton onClick={this._openRequest}>
{this._acceptedLabel(request.receivingUserId)}
stateLabel = (<AccessibleButton onClick={this.openRequest}>
{this.acceptedLabel(request.receivingUserId)}
</AccessibleButton>);
} else if (request.cancelled) {
stateLabel = this._cancelledLabel(request.cancellingUserId);
stateLabel = this.cancelledLabel(request.cancellingUserId);
} else if (request.accepting) {
stateLabel = _t("Accepting …");
} else if (request.declining) {
@ -149,12 +147,16 @@ export default class MKeyVerificationRequest extends React.Component {
if (!request.initiatedByMe) {
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
title = _t("%(name)s wants to verify", {name});
title = _t("%(name)s wants to verify", { name });
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
if (request.canAccept) {
stateNode = (<div className="mx_cryptoEvent_buttons">
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
<AccessibleButton kind="danger" onClick={this.onRejectClicked}>
{_t("Decline")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onAcceptClicked}>
{_t("Accept")}
</AccessibleButton>
</div>);
}
} else { // request sent by us
@ -174,8 +176,3 @@ export default class MKeyVerificationRequest extends React.Component {
return null;
}
}
MKeyVerificationRequest.propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};

View file

@ -17,7 +17,8 @@ limitations under the License.
import React from 'react';
import MImageBody from './MImageBody';
import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { BLURHASH_FIELD } from "../../../ContentMessages";
@replaceableComponent("views.messages.MStickerBody")
export default class MStickerBody extends MImageBody {
@ -41,9 +42,9 @@ export default class MStickerBody extends MImageBody {
// Placeholder to show in place of the sticker image if
// img onLoad hasn't fired yet.
getPlaceholder() {
const TintableSVG = sdk.getComponent('elements.TintableSvg');
return <TintableSVG src={require("../../../../res/img/icons-show-stickers.svg")} width="75" height="75" />;
getPlaceholder(width, height) {
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
@ -53,7 +54,7 @@ export default class MStickerBody extends MImageBody {
if (!content || !content.body || !content.info || !content.info.w) return null;
const Tooltip = sdk.getComponent('elements.Tooltip');
return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
return <div style={{ left: content.info.w + 'px' }} className="mx_MStickerBody_tooltip">
<Tooltip label={content.body} />
</div>;
}

View file

@ -16,13 +16,16 @@ limitations under the License.
*/
import React from 'react';
import { decode } from "blurhash";
import MFileBody from './MFileBody';
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";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD } from "../../../ContentMessages";
interface IProps {
/* the MatrixEvent to show */
@ -32,11 +35,13 @@ interface IProps {
}
interface IState {
decryptedUrl: string|null,
decryptedThumbnailUrl: string|null,
decryptedBlob: Blob|null,
error: any|null,
fetchingData: boolean,
decryptedUrl?: string;
decryptedThumbnailUrl?: string;
decryptedBlob?: Blob;
error?: any;
fetchingData: boolean;
posterLoading: boolean;
blurhashUrl: string;
}
@replaceableComponent("views.messages.MVideoBody")
@ -51,10 +56,12 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
decryptedThumbnailUrl: null,
decryptedBlob: null,
error: null,
}
posterLoading: false,
blurhashUrl: null,
};
}
thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy
@ -92,8 +99,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
private getThumbUrl(): string|null {
const content = this.props.mxEvent.getContent();
const media = mediaFromContent(content);
if (media.isEncrypted) {
if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
return this.state.decryptedThumbnailUrl;
} else if (this.state.posterLoading) {
return this.state.blurhashUrl;
} else if (media.hasThumbnail) {
return media.thumbnailHttp;
} else {
@ -101,18 +111,57 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
}
}
private loadBlurhash() {
const info = this.props.mxEvent.getContent()?.info;
if (!info[BLURHASH_FIELD]) return;
const canvas = document.createElement("canvas");
let width = info.w;
let height = info.h;
const scale = this.thumbScale(info.w, info.h);
if (scale) {
width = Math.floor(info.w * scale);
height = Math.floor(info.h * scale);
}
canvas.width = width;
canvas.height = height;
const pixels = decode(info[BLURHASH_FIELD], width, height);
const ctx = canvas.getContext("2d");
const imgData = ctx.createImageData(width, height);
imgData.data.set(pixels);
ctx.putImageData(imgData, 0, 0);
this.setState({
blurhashUrl: canvas.toDataURL(),
posterLoading: true,
});
const content = this.props.mxEvent.getContent();
const media = mediaFromContent(content);
if (media.hasThumbnail) {
const image = new Image();
image.onload = () => {
this.setState({ posterLoading: false });
};
image.src = media.thumbnailHttp;
}
}
async componentDidMount() {
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
const content = this.props.mxEvent.getContent();
this.loadBlurhash();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);
if (content.info && content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
return URL.createObjectURL(blob);
});
if (content?.info?.thumbnail_file) {
thumbnailPromise = decryptFile(content.info.thumbnail_file)
.then(blob => URL.createObjectURL(blob));
}
try {
const thumbnailUrl = await thumbnailPromise;
if (autoplay) {
@ -182,7 +231,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
this.videoRef.current.play();
});
this.props.onHeightChanged();
}
};
render() {
const content = this.props.mxEvent.getContent();
@ -218,7 +267,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
let poster = null;
let preload = "metadata";
if (content.info) {
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
const scale = this.thumbScale(content.info.w, content.info.h);
if (scale) {
width = Math.floor(content.info.w * scale);
height = Math.floor(content.info.h * scale);

View file

@ -15,16 +15,16 @@ limitations under the License.
*/
import React from "react";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {Playback} from "../../../voice/Playback";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Playback } from "../../../voice/Playback";
import MFileBody from "./MFileBody";
import InlineSpinner from '../elements/InlineSpinner';
import {_t} from "../../../languageHandler";
import {mediaFromContent} from "../../../customisations/Media";
import {decryptFile} from "../../../utils/DecryptFile";
import RecordingPlayback from "../voice_messages/RecordingPlayback";
import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent";
import { _t } from "../../../languageHandler";
import { mediaFromContent } from "../../../customisations/Media";
import { decryptFile } from "../../../utils/DecryptFile";
import RecordingPlayback from "../audio_messages/RecordingPlayback";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
interface IProps {
mxEvent: MatrixEvent;
@ -52,9 +52,9 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
try {
const blob = await decryptFile(content.file);
buffer = await blob.arrayBuffer();
this.setState({decryptedBlob: blob});
this.setState({ decryptedBlob: blob });
} catch (e) {
this.setState({error: e});
this.setState({ error: e });
console.warn("Unable to decrypt voice message", e);
return; // stop processing the audio file
}
@ -62,7 +62,7 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
try {
buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
} catch (e) {
this.setState({error: e});
this.setState({ error: e });
console.warn("Unable to download voice message", e);
return; // stop processing the audio file
}
@ -106,6 +106,6 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
<RecordingPlayback playback={this.state.playback} />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
</span>
)
);
}
}

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
import React from "react";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import MAudioBody from "./MAudioBody";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import MVoiceMessageBody from "./MVoiceMessageBody";

View file

@ -16,24 +16,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useEffect} from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { EventStatus } from 'matrix-js-sdk/src/models/event';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from '../../structures/ContextMenu';
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
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";
import {canCancel} from "../context_menus/MessageContextMenu";
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { canCancel } from "../context_menus/MessageContextMenu";
import Resend from "../../../Resend";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
useEffect(() => {
@ -48,15 +48,14 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
const replyThread = getReplyThread && getReplyThread();
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<MessageContextMenu
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
onFinished={closeMenu}
/>
</ContextMenu>;
contextMenu = <MessageContextMenu
{...aboveLeftOf(buttonRect)}
mxEvent={mxEvent}
permalinkCreator={permalinkCreator}
eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
collapseReplyThread={replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined}
onFinished={closeMenu}
/>;
}
return <React.Fragment>
@ -74,7 +73,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
</React.Fragment>;
};
const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
const ReactButton = ({ mxEvent, reactions, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const [onFocus, isActive, ref] = useRovingTabIndex(button);
useEffect(() => {

View file

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import {Mjolnir} from "../../../mjolnir/Mjolnir";
import { Mjolnir } from "../../../mjolnir/Mjolnir";
import RedactedBody from "./RedactedBody";
import UnknownBody from "./UnknownBody";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.messages.MessageEvent")
export default class MessageEvent extends React.Component {

View file

@ -16,8 +16,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.messages.MjolnirBody")
export default class MjolnirBody extends React.Component {
@ -43,7 +43,7 @@ export default class MjolnirBody extends React.Component {
return (
<div className='mx_MjolnirBody'><i>{_t(
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
{}, {a: (sub) => <a href="#" onClick={this._onAllowClick}>{sub}</a>},
{}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{sub}</a> },
)}</i></div>
);
}

View file

@ -125,7 +125,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
private onDecrypted = () => {
// Decryption changes whether the event is actionable
this.forceUpdate();
}
};
private onReactionsChange = () => {
// TODO: Call `onHeightChanged` as needed
@ -136,7 +136,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
// has changed (this is triggered by events for that purpose only) and
// `PureComponent`s shallow state / props compare would otherwise filter this out.
this.forceUpdate();
}
};
private getMyReactions() {
const reactions = this.props.reactions;
@ -155,7 +155,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
this.setState({
showAll: true,
});
}
};
render() {
const { mxEvent, reactions } = this.props;

View file

@ -68,7 +68,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
"key": content,
},
});
dis.dispatch({action: "message_sent"});
dis.dispatch({ action: "message_sent" });
}
};
@ -79,13 +79,13 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
tooltipRendered: true,
tooltipVisible: true,
});
}
};
onMouseLeave = () => {
this.setState({
tooltipVisible: false,
});
}
};
render() {
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;

View file

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext} from "react";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import React, { useContext } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {formatFullDate} from "../../../DateUtils";
import { formatFullDate } from "../../../DateUtils";
import SettingsStore from "../../../settings/SettingsStore";
interface IProps {
mxEvent: MatrixEvent;
}
const RedactedBody = React.forwardRef<any, IProps>(({mxEvent}, ref) => {
const RedactedBody = React.forwardRef<any, IProps>(({ mxEvent }, ref) => {
const cli: MatrixClient = useContext(MatrixClientContext);
let text = _t("Message deleted");

View file

@ -18,13 +18,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
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";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
@replaceableComponent("views.messages.RoomAvatarEvent")
export default class RoomAvatarEvent extends React.Component {
@ -60,7 +60,7 @@ export default class RoomAvatarEvent extends React.Component {
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return (
<div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
{ _t('%(senderDisplayName)s removed the room avatar.', { senderDisplayName }) }
</div>
);
}

View file

@ -21,9 +21,9 @@ import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import EventTileBubble from "./EventTileBubble";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.messages.RoomCreate")
export default class RoomCreate extends React.Component {

View file

@ -15,7 +15,7 @@
*/
import React from 'react';
import Flair from '../elements/Flair.js';
import Flair from '../elements/Flair';
import FlairStore from '../../../stores/FlairStore';
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -39,7 +39,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
private unmounted: boolean;
constructor(props: IProps) {
super(props)
super(props);
const senderId = this.props.mxEvent.getSender();
this.state = {
@ -56,7 +56,6 @@ export default class SenderProfile extends React.Component<IProps, IState> {
this.getPublicisedGroups();
}
this.context.on('RoomState.events', this.onRoomStateEvents);
}
@ -70,7 +69,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
const userGroups = await FlairStore.getPublicisedGroupsCached(
this.context, this.props.mxEvent.getSender(),
);
this.setState({userGroups});
this.setState({ userGroups });
}
}
@ -106,9 +105,9 @@ export default class SenderProfile extends React.Component<IProps, IState> {
}
render() {
const {mxEvent} = this.props;
const { mxEvent } = this.props;
const colorClass = getUserNameColorClass(mxEvent.getSender());
const {msgtype} = mxEvent.getContent();
const { msgtype } = mxEvent.getContent();
const disambiguate = mxEvent.sender?.disambiguate;
const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
@ -140,7 +139,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
}
return (
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
<span className={`mx_SenderProfile_displayName ${colorClass}`}>
{ displayName }
</span>

View file

@ -1,7 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@ -16,134 +14,151 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import React, { createRef, SyntheticEvent } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import highlight from 'highlight.js';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { MsgType } from "matrix-js-sdk/src/@types/event";
import * as HtmlUtils from '../../../HtmlUtils';
import { formatDate } from '../../../DateUtils';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import * as ContextMenu from '../../structures/ContextMenu';
import { toRightOf } from '../../structures/ContextMenu';
import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread";
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
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";
import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../dispatcher/actions";
import { TileShape } from '../rooms/EventTile';
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
import Spoiler from "../elements/Spoiler";
import QuestionDialog from "../dialogs/QuestionDialog";
import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
import EditMessageComposer from '../rooms/EditMessageComposer';
import LinkPreviewWidget from '../rooms/LinkPreviewWidget';
interface IProps {
/* the MatrixEvent to show */
mxEvent: MatrixEvent;
/* a list of words to highlight */
highlights?: string[];
/* link URL for the highlights */
highlightLink?: string;
/* should show URL previews for this event */
showUrlPreview?: boolean;
/* the shape of the tile, used */
tileShape?: TileShape;
editState?: EditorStateTransfer;
replacingEventId?: string;
/* callback for when our widget has loaded */
onHeightChanged(): void;
}
interface IState {
// the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody.
links: string[];
// track whether the preview widget is hidden
widgetHidden: boolean;
}
@replaceableComponent("views.messages.TextualBody")
export default class TextualBody extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
export default class TextualBody extends React.Component<IProps, IState> {
private readonly contentRef = createRef<HTMLSpanElement>();
/* a list of words to highlight */
highlights: PropTypes.array,
/* link URL for the highlights */
highlightLink: PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: PropTypes.bool,
/* callback for when our widget has loaded */
onHeightChanged: PropTypes.func,
/* the shape of the tile, used */
tileShape: PropTypes.string,
};
private unmounted = false;
private pills: Element[] = [];
constructor(props) {
super(props);
this._content = createRef();
this.state = {
// the URLs (if any) to be previewed with a LinkPreviewWidget
// inside this TextualBody.
links: [],
// track whether the preview widget is hidden
widgetHidden: false,
};
}
componentDidMount() {
this._unmounted = false;
this._pills = [];
if (!this.props.editState) {
this._applyFormatting();
this.applyFormatting();
}
}
_applyFormatting() {
private applyFormatting(): void {
const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
this.activateSpoilers([this._content.current]);
this.activateSpoilers([this.contentRef.current]);
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
pillifyLinks([this._content.current], this.props.mxEvent, this._pills);
HtmlUtils.linkifyElement(this._content.current);
pillifyLinks([this.contentRef.current], this.props.mxEvent, this.pills);
HtmlUtils.linkifyElement(this.contentRef.current);
this.calculateUrlPreview();
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
// Handle expansion and add buttons
const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
const pres = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("pre");
if (pres.length > 0) {
for (let i = 0; i < pres.length; i++) {
// 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;
if (pres[i].parentElement.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]);
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]);
this._handleCodeBlockExpansion(pres[i]);
this._addCodeExpansionButton(div, pres[i]);
this._addCodeCopyButton(div);
const div = this.wrapInDiv(pres[i]);
this.handleCodeBlockExpansion(pres[i]);
this.addCodeExpansionButton(div, pres[i]);
this.addCodeCopyButton(div);
if (showLineNumbers) {
this._addLineNumbers(pres[i]);
this.addLineNumbers(pres[i]);
}
}
}
// Highlight code
const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
const codes = (ReactDOM.findDOMNode(this) as Element).getElementsByTagName("code");
if (codes.length > 0) {
// Do this asynchronously: parsing code takes time and we don't
// need to block the DOM update on it.
setTimeout(() => {
if (this._unmounted) return;
if (this.unmounted) return;
for (let i = 0; i < codes.length; i++) {
// If the code already has the hljs class we want to skip this.
// This happens after the codeblock was edited.
if (codes[i].className.includes("hljs")) continue;
this._highlightCode(codes[i]);
this.highlightCode(codes[i]);
}
}, 10);
}
}
}
_addCodeElement(pre) {
private addCodeElement(pre: HTMLPreElement): void {
const code = document.createElement("code");
code.append(...pre.childNodes);
pre.appendChild(code);
}
_addCodeExpansionButton(div, pre) {
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;
@ -175,7 +190,7 @@ export default class TextualBody extends React.Component {
div.appendChild(button);
}
_addCodeCopyButton(div) {
private addCodeCopyButton(div: HTMLDivElement): void {
const button = document.createElement("span");
button.className = "mx_EventTile_button mx_EventTile_copyButton ";
@ -185,12 +200,11 @@ export default class TextualBody extends React.Component {
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
button.onclick = async () => {
const copyCode = button.parentNode.getElementsByTagName("code")[0];
const copyCode = button.parentElement.getElementsByTagName("code")[0];
const successful = await copyPlaintext(copyCode.textContent);
const buttonRect = button.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
@ -200,7 +214,7 @@ export default class TextualBody extends React.Component {
div.appendChild(button);
}
_wrapInDiv(pre) {
private wrapInDiv(pre: HTMLPreElement): HTMLDivElement {
const div = document.createElement("div");
div.className = "mx_EventTile_pre_container";
@ -212,13 +226,13 @@ export default class TextualBody extends React.Component {
return div;
}
_handleCodeBlockExpansion(pre) {
private handleCodeBlockExpansion(pre: HTMLPreElement): void {
if (!SettingsStore.getValue("expandCodeByDefault")) {
pre.className = "mx_EventTile_collapsedCodeBlock";
}
}
_addLineNumbers(pre) {
private addLineNumbers(pre: HTMLPreElement): void {
// Calculate number of lines in pre
const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
@ -229,7 +243,7 @@ export default class TextualBody extends React.Component {
}
}
_highlightCode(code) {
private highlightCode(code: HTMLElement): void {
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
highlight.highlightBlock(code);
} else {
@ -249,14 +263,14 @@ export default class TextualBody extends React.Component {
const stoppedEditing = prevProps.editState && !this.props.editState;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
if (messageWasEdited || stoppedEditing) {
this._applyFormatting();
this.applyFormatting();
}
}
}
componentWillUnmount() {
this._unmounted = true;
unmountPills(this._pills);
this.unmounted = true;
unmountPills(this.pills);
}
shouldComponentUpdate(nextProps, nextState) {
@ -273,12 +287,12 @@ export default class TextualBody extends React.Component {
nextState.widgetHidden !== this.state.widgetHidden);
}
calculateUrlPreview() {
private calculateUrlPreview(): void {
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview) {
// pass only the first child which is the event tile otherwise this recurses on edited events
let links = this.findLinks([this._content.current]);
let links = this.findLinks([this.contentRef.current]);
if (links.length) {
// de-duplicate the links after stripping hashes as they don't affect the preview
// using a set here maintains the order
@ -291,8 +305,8 @@ export default class TextualBody extends React.Component {
this.setState({ links });
// lazy-load the hidden state of the preview widget from localstorage
if (global.localStorage) {
const hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
if (window.localStorage) {
const hidden = !!window.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
this.setState({ widgetHidden: hidden });
}
} else if (this.state.links.length) {
@ -301,19 +315,15 @@ export default class TextualBody extends React.Component {
}
}
activateSpoilers(nodes) {
private activateSpoilers(nodes: ArrayLike<Element>): void {
let node = nodes[0];
while (node) {
if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
const spoilerContainer = document.createElement('span');
const reason = node.getAttribute("data-mx-spoiler");
const Spoiler = sdk.getComponent('elements.Spoiler');
node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
const spoiler = <Spoiler
reason={reason}
contentHtml={node.outerHTML}
/>;
const spoiler = <Spoiler reason={reason} contentHtml={node.outerHTML} />;
ReactDOM.render(spoiler, spoilerContainer);
node.parentNode.replaceChild(spoilerContainer, node);
@ -322,15 +332,15 @@ export default class TextualBody extends React.Component {
}
if (node.childNodes && node.childNodes.length) {
this.activateSpoilers(node.childNodes);
this.activateSpoilers(node.childNodes as NodeListOf<Element>);
}
node = node.nextSibling;
node = node.nextSibling as Element;
}
}
findLinks(nodes) {
let links = [];
private findLinks(nodes: ArrayLike<Element>): string[] {
let links: string[] = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
@ -348,7 +358,7 @@ export default class TextualBody extends React.Component {
return links;
}
isLinkPreviewable(node) {
private isLinkPreviewable(node: Element): boolean {
// don't try to preview relative links
if (!node.getAttribute("href").startsWith("http://") &&
!node.getAttribute("href").startsWith("https://")) {
@ -381,7 +391,7 @@ export default class TextualBody extends React.Component {
}
}
onCancelClick = event => {
private onCancelClick = (): void => {
this.setState({ widgetHidden: true });
// FIXME: persist this somewhere smarter than local storage
if (global.localStorage) {
@ -390,7 +400,7 @@ export default class TextualBody extends React.Component {
this.forceUpdate();
};
onEmoteSenderClick = event => {
private onEmoteSenderClick = (): void => {
const mxEvent = this.props.mxEvent;
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
@ -398,20 +408,20 @@ export default class TextualBody extends React.Component {
});
};
getEventTileOps = () => ({
public getEventTileOps = () => ({
isWidgetHidden: () => {
return this.state.widgetHidden;
},
unhideWidget: () => {
this.setState({widgetHidden: false});
this.setState({ widgetHidden: false });
if (global.localStorage) {
global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId());
}
},
});
onStarterLinkClick = (starterLink, ev) => {
private onStarterLinkClick = (starterLink: string, ev: SyntheticEvent): void => {
ev.preventDefault();
// We need to add on our scalar token to the starter link, but we may not have one!
// In addition, we can't fetch one on click and then go to it immediately as that
@ -431,7 +441,6 @@ export default class TextualBody extends React.Component {
const scalarClient = integrationManager.getScalarClient();
scalarClient.connect().then(() => {
const completeUrl = scalarClient.getStarterLink(starterLink);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const integrationsUrl = integrationManager.uiUrl;
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
title: _t("Add an Integration"),
@ -458,18 +467,17 @@ export default class TextualBody extends React.Component {
});
};
_openHistoryDialog = async () => {
const MessageEditHistoryDialog = sdk.getComponent("views.dialogs.MessageEditHistoryDialog");
Modal.createDialog(MessageEditHistoryDialog, {mxEvent: this.props.mxEvent});
private openHistoryDialog = async (): Promise<void> => {
Modal.createDialog(MessageEditHistoryDialog, { mxEvent: this.props.mxEvent });
};
_renderEditedMarker() {
private renderEditedMarker() {
const date = this.props.mxEvent.replacingEventDate();
const dateString = date && formatDate(date);
const tooltip = <div>
<div className="mx_Tooltip_title">
{_t("Edited at %(date)s", {date: dateString})}
{_t("Edited at %(date)s", { date: dateString })}
</div>
<div className="mx_Tooltip_sub">
{_t("Click to view edits")}
@ -479,8 +487,8 @@ export default class TextualBody extends React.Component {
return (
<AccessibleTooltipButton
className="mx_EventTile_edited"
onClick={this._openHistoryDialog}
title={_t("Edited at %(date)s. Click to view edits.", {date: dateString})}
onClick={this.openHistoryDialog}
title={_t("Edited at %(date)s. Click to view edits.", { date: dateString })}
tooltip={tooltip}
>
<span>{`(${_t("edited")})`}</span>
@ -490,24 +498,25 @@ export default class TextualBody extends React.Component {
render() {
if (this.props.editState) {
const EditMessageComposer = sdk.getComponent('rooms.EditMessageComposer');
return <EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />;
}
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
const stripReply = !mxEvent.replacingEvent() && ReplyThread.getParentEventId(mxEvent);
const stripReply = !mxEvent.replacingEvent() && !!ReplyThread.getParentEventId(mxEvent);
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
disableBigEmoji: content.msgtype === MsgType.Emote
|| !SettingsStore.getValue<boolean>('TextualBody.enableBigEmoji'),
// Part of Replies fallback support
stripReplyFallback: stripReply,
ref: this._content,
ref: this.contentRef,
returnString: false,
});
if (this.props.replacingEventId) {
body = <>
{body}
{this._renderEditedMarker()}
{this.renderEditedMarker()}
</>;
}
@ -521,7 +530,6 @@ export default class TextualBody extends React.Component {
let widgets;
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
widgets = this.state.links.map((link)=>{
return <LinkPreviewWidget
key={link}
@ -534,7 +542,7 @@ export default class TextualBody extends React.Component {
}
switch (content.msgtype) {
case "m.emote":
case MsgType.Emote:
return (
<span className="mx_MEmoteBody mx_EventTile_content">
*&nbsp;
@ -549,7 +557,7 @@ export default class TextualBody extends React.Component {
{ widgets }
</span>
);
case "m.notice":
case MsgType.Notice:
return (
<span className="mx_MNoticeBody mx_EventTile_content">
{ body }

View file

@ -1,6 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2015 - 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.
@ -16,20 +15,20 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import * as TextForEvent from "../../../TextForEvent";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
mxEvent: MatrixEvent;
}
@replaceableComponent("views.messages.TextualEvent")
export default class TextualEvent extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};
export default class TextualEvent extends React.Component<IProps> {
render() {
const text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length === 0) return null;
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
if (!text || (text as string).length === 0) return null;
return (
<div className="mx_TextualEvent">{ text }</div>
);

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020 - 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.
@ -16,14 +16,24 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BugReportDialog from '../dialogs/BugReportDialog';
interface IProps {
mxEvent: MatrixEvent;
}
interface IState {
error: Error;
}
@replaceableComponent("views.messages.TileErrorBoundary")
export default class TileErrorBoundary extends React.Component {
export default class TileErrorBoundary extends React.Component<IProps, IState> {
constructor(props) {
super(props);
@ -32,17 +42,13 @@ export default class TileErrorBoundary extends React.Component {
};
}
static getDerivedStateFromError(error) {
static getDerivedStateFromError(error: Error): Partial<IState> {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
_onBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash-tile',
});
@ -60,7 +66,7 @@ export default class TileErrorBoundary extends React.Component {
let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) {
submitLogsButton = <a onClick={this._onBugReport} href="#">
submitLogsButton = <a onClick={this.onBugReport} href="#">
{_t("Submit logs")}
</a>;
}

View file

@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {forwardRef} from "react";
import React, { forwardRef } from "react";
export default forwardRef(({mxEvent}, ref) => {
export default forwardRef(({ mxEvent }, ref) => {
const text = mxEvent.getContent().body;
return (
<span className="mx_UnknownBody" ref={ref}>

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@replaceableComponent("views.messages.ViewSourceEvent")
@ -36,7 +36,7 @@ export default class ViewSourceEvent extends React.PureComponent {
}
componentDidMount() {
const {mxEvent} = this.props;
const { mxEvent } = this.props;
const client = MatrixClientPeg.get();
client.decryptEventIfNeeded(mxEvent);