Merge remote-tracking branch 'origin/develop' into travis/voice-messages/download

This commit is contained in:
Travis Ralston 2021-07-20 08:55:53 -06:00
commit 5994111e5d
325 changed files with 5224 additions and 4075 deletions

View file

@ -0,0 +1,218 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 { _t, _td } from '../../../languageHandler';
import MemberAvatar from '../avatars/MemberAvatar';
import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper';
import AccessibleButton from '../elements/AccessibleButton';
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';
interface IProps {
mxEvent: MatrixEvent;
callEventGrouper: CallEventGrouper;
}
interface IState {
callState: CallState | CustomCallState;
silenced: boolean;
}
const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([
[CallState.Connected, _td("Connected")],
[CallState.Connecting, _td("Connecting")],
]);
export default class CallEvent extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
callState: this.props.callEventGrouper.state,
silenced: false,
};
}
componentDidMount() {
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
}
componentWillUnmount() {
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
}
private onSilencedChanged = (newState) => {
this.setState({ silenced: newState });
};
private onStateChanged = (newState: CallState) => {
this.setState({ callState: newState });
};
private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) {
const silenceClass = classNames({
"mx_CallEvent_iconButton": true,
"mx_CallEvent_unSilence": this.state.silenced,
"mx_CallEvent_silence": !this.state.silenced,
});
return (
<div className="mx_CallEvent_content">
<AccessibleTooltipButton
className={silenceClass}
onClick={this.props.callEventGrouper.toggleSilenced}
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
/>
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_reject"
onClick={this.props.callEventGrouper.rejectCall}
kind="danger"
>
<span> { _t("Decline") } </span>
</AccessibleButton>
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_answer"
onClick={this.props.callEventGrouper.answerCall}
kind="primary"
>
<span> { _t("Accept") } </span>
</AccessibleButton>
</div>
);
}
if (state === CallState.Ended) {
const hangupReason = this.props.callEventGrouper.hangupReason;
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
return (
<div className="mx_CallEvent_content">
{ _t("This call has ended") }
</div>
);
}
let reason;
if (hangupReason === CallErrorCode.IceFailed) {
// We couldn't establish a connection at all
reason = _t("Could not connect media");
} else if (hangupReason === "ice_timeout") {
// We established a connection but it died
reason = _t("Connection failed");
} else if (hangupReason === CallErrorCode.NoUserMedia) {
// The other side couldn't open capture devices
reason = _t("Their device couldn't start the camera or microphone");
} else if (hangupReason === "unknown_error") {
// An error code the other side doesn't have a way to express
// (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 {
reason = _t('Unknown failure: %(reason)s)', { reason: hangupReason });
}
return (
<div className="mx_CallEvent_content">
<InfoTooltip
tooltip={reason}
className="mx_CallEvent_content_tooltip"
kind={InfoTooltipKind.Warning}
/>
{ _t("This call has failed") }
</div>
);
}
if (Array.from(TEXTUAL_STATES.keys()).includes(state)) {
return (
<div className="mx_CallEvent_content">
{ TEXTUAL_STATES.get(state) }
</div>
);
}
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>
</div>
);
}
return (
<div className="mx_CallEvent_content">
{ _t("The call is in an unknown state!") }
</div>
);
}
render() {
const event = this.props.mxEvent;
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 className = classNames({
mx_CallEvent: true,
mx_CallEvent_voice: isVoice,
mx_CallEvent_video: !isVoice,
});
return (
<div className={className}>
<div className="mx_CallEvent_info">
<MemberAvatar
member={event.sender}
width={32}
height={32}
/>
<div className="mx_CallEvent_info_basic">
<div className="mx_CallEvent_sender">
{ sender }
</div>
<div className="mx_CallEvent_type">
<div className="mx_CallEvent_type_icon"></div>
{ callType }
</div>
</div>
</div>
{ content }
</div>
);
}
}

View file

@ -110,20 +110,20 @@ export default class EditHistoryMessage extends React.PureComponent {
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
redactButton = (
<AccessibleButton onClick={this._onRedactClick}>
{_t("Remove")}
{ _t("Remove") }
</AccessibleButton>
);
}
const viewSourceButton = (
<AccessibleButton onClick={this._onViewSourceClick}>
{_t("View Source")}
{ _t("View Source") }
</AccessibleButton>
);
// disabled remove button when not allowed
return (
<div className="mx_MessageActionBar">
{redactButton}
{viewSourceButton}
{ redactButton }
{ viewSourceButton }
</div>
);
}
@ -146,11 +146,11 @@ export default class EditHistoryMessage extends React.PureComponent {
contentContainer = (
<div className="mx_EventTile_content" ref={this._content}>*&nbsp;
<span className="mx_MEmoteBody_sender">{ name }</span>
&nbsp;{contentElements}
&nbsp;{ contentElements }
</div>
);
} else {
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{contentElements}</div>;
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{ contentElements }</div>;
}
}
@ -165,7 +165,7 @@ export default class EditHistoryMessage extends React.PureComponent {
<li>
<div className={classes}>
<div className="mx_EventTile_line">
<span className="mx_MessageTimestamp">{timestamp}</span>
<span className="mx_MessageTimestamp">{ timestamp }</span>
{ contentContainer }
{ this._renderActionBar() }
</div>

View file

@ -273,13 +273,6 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
this.downloadImage();
this.setState({ showImage: true });
} // else don't download anything because we don't want to display anything.
this._afterComponentDidMount();
}
// To be overridden by subclasses (e.g. MStickerBody) for further
// initialisation after componentDidMount
_afterComponentDidMount() {
}
componentWillUnmount() {
@ -394,7 +387,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
// Overidden by MStickerBody
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
return <a href={contentUrl} onClick={this.onClick}>
{children}
{ children }
</a>;
}
@ -402,9 +395,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
protected getPlaceholder(width: number, height: number): JSX.Element {
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
return <div className="mx_MImageBody_thumbnail_spinner">
return (
<InlineSpinner w={32} h={32} />
</div>;
);
}
// Overidden by MStickerBody
@ -464,7 +457,7 @@ export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProp
<div className={className} style={{ maxWidth: maxWidth }}>
<div className='mx_HiddenImagePlaceholder_button'>
<span className='mx_HiddenImagePlaceholder_eye' />
<span>{_t("Show image")}</span>
<span>{ _t("Show image") }</span>
</div>
</div>
);

View file

@ -131,7 +131,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
const accepted = request.ready || request.started || request.done;
if (accepted) {
stateLabel = (<AccessibleButton onClick={this.openRequest}>
{this.acceptedLabel(request.receivingUserId)}
{ this.acceptedLabel(request.receivingUserId) }
</AccessibleButton>);
} else if (request.cancelled) {
stateLabel = this.cancelledLabel(request.cancellingUserId);
@ -140,7 +140,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
} else if (request.declining) {
stateLabel = _t("Declining …");
}
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
stateNode = (<div className="mx_cryptoEvent_state">{ stateLabel }</div>);
}
if (!request.initiatedByMe) {
@ -150,10 +150,10 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
if (request.canAccept) {
stateNode = (<div className="mx_cryptoEvent_buttons">
<AccessibleButton kind="danger" onClick={this.onRejectClicked}>
{_t("Decline")}
{ _t("Decline") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onAcceptClicked}>
{_t("Accept")}
{ _t("Accept") }
</AccessibleButton>
</div>);
}

View file

@ -297,7 +297,7 @@ export default class MessageActionBar extends React.PureComponent {
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
{toolbarOpts}
{ toolbarOpts }
</Toolbar>;
}
}

View file

@ -45,7 +45,7 @@ export default class MessageTimestamp extends React.Component<IProps> {
title={formatFullDate(date, this.props.showTwelveHour)}
aria-hidden={true}
>
{timestamp}
{ timestamp }
</span>
);
}

View file

@ -41,10 +41,10 @@ export default class MjolnirBody extends React.Component {
render() {
return (
<div className='mx_MjolnirBody'><i>{_t(
<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> },
)}</i></div>
{}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{ sub }</a> },
) }</i></div>
);
}
}

View file

@ -199,7 +199,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
href="#"
onClick={this.onShowAllClick}
>
{_t("Show all")}
{ _t("Show all") }
</a>;
}

View file

@ -142,12 +142,12 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
onMouseLeave={this.onMouseLeave}
>
<span className="mx_ReactionsRowButton_content" aria-hidden="true">
{content}
{ content }
</span>
<span className="mx_ReactionsRowButton_count" aria-hidden="true">
{count}
{ count }
</span>
{tooltip}
{ tooltip }
</AccessibleButton>;
}
}

View file

@ -51,7 +51,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
senders.push(name);
}
const shortName = unicodeToShortcode(content);
tooltipLabel = <div>{_t(
tooltipLabel = <div>{ _t(
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
{
shortName,
@ -59,7 +59,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
{
reactors: () => {
return <div className="mx_Tooltip_title">
{formatCommaSeparatedList(senders, 6)}
{ formatCommaSeparatedList(senders, 6) }
</div>;
},
reactedWith: (sub) => {
@ -67,11 +67,11 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
return null;
}
return <div className="mx_Tooltip_sub">
{sub}
{ sub }
</div>;
},
},
)}</div>;
) }</div>;
}
let tooltip;

View file

@ -56,7 +56,7 @@ export default class RoomCreate extends React.Component {
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
const link = (
<a href={predecessorPermalink} onClick={this._onLinkClicked}>
{_t("Click here to see older messages.")}
{ _t("Click here to see older messages.") }
</a>
);

View file

@ -450,10 +450,10 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
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")}
{ _t("Click to view edits") }
</div>
</div>;
@ -464,7 +464,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
title={_t("Edited at %(date)s. Click to view edits.", { date: dateString })}
tooltip={tooltip}
>
<span>{`(${_t("edited")})`}</span>
<span>{ `(${_t("edited")})` }</span>
</AccessibleTooltipButton>
);
}
@ -488,8 +488,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
});
if (this.props.replacingEventId) {
body = <>
{body}
{this.renderEditedMarker()}
{ body }
{ this.renderEditedMarker() }
</>;
}

View file

@ -14,9 +14,10 @@ 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 React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import RoomContext from "../../../contexts/RoomContext";
import * as TextForEvent from "../../../TextForEvent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -26,11 +27,11 @@ interface IProps {
@replaceableComponent("views.messages.TextualEvent")
export default class TextualEvent extends React.Component<IProps> {
render() {
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
if (!text || (text as string).length === 0) return null;
return (
<div className="mx_TextualEvent">{ text }</div>
);
static contextType = RoomContext;
public render() {
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
if (!text) return null;
return <div className="mx_TextualEvent">{ text }</div>;
}
}

View file

@ -67,14 +67,14 @@ export default class TileErrorBoundary extends React.Component<IProps, IState> {
let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) {
submitLogsButton = <a onClick={this.onBugReport} href="#">
{_t("Submit logs")}
{ _t("Submit logs") }
</a>;
}
return (<div className={classNames(classes)}>
<div className="mx_EventTile_line">
<span>
{_t("Can't load this message")}
{ _t("Can't load this message") }
{ mxEvent && ` (${mxEvent.getType()})` }
{ submitLogsButton }
</span>

View file

@ -16,12 +16,19 @@ limitations under the License.
*/
import React, { forwardRef } from "react";
import { MatrixEvent } from "matrix-js-sdk/src";
export default forwardRef(({ mxEvent }, ref) => {
interface IProps {
mxEvent: MatrixEvent;
children?: React.ReactNode;
}
export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject<HTMLSpanElement>) => {
const text = mxEvent.getContent().body;
return (
<span className="mx_UnknownBody" ref={ref}>
{ text }
{ children }
</span>
);
});

View file

@ -60,9 +60,9 @@ export default class ViewSourceEvent extends React.PureComponent {
let content;
if (expanded) {
content = <pre>{JSON.stringify(mxEvent, null, 4)}</pre>;
content = <pre>{ JSON.stringify(mxEvent, null, 4) }</pre>;
} else {
content = <code>{`{ "type": ${mxEvent.getType()} }`}</code>;
content = <code>{ `{ "type": ${mxEvent.getType()} }` }</code>;
}
const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", {
@ -70,7 +70,7 @@ export default class ViewSourceEvent extends React.PureComponent {
});
return <span className={classes}>
{content}
{ content }
<a
className="mx_ViewSourceEvent_toggle"
href="#"