Merge remote-tracking branch 'origin' into sentry-rageshakes
This commit is contained in:
commit
834f72a9a8
87 changed files with 2998 additions and 1463 deletions
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
@ -27,6 +27,8 @@ import classNames from 'classnames';
|
|||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||
import { formatCallTime } from "../../../DateUtils";
|
||||
|
||||
const MAX_NON_NARROW_WIDTH = 400 / 70 * 100;
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
callEventGrouper: CallEventGrouper;
|
||||
|
@ -35,6 +37,7 @@ interface IProps {
|
|||
interface IState {
|
||||
callState: CallState | CustomCallState;
|
||||
silenced: boolean;
|
||||
narrow: boolean;
|
||||
}
|
||||
|
||||
const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([
|
||||
|
@ -42,26 +45,42 @@ const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([
|
|||
[CallState.Connecting, _td("Connecting")],
|
||||
]);
|
||||
|
||||
export default class CallEvent extends React.Component<IProps, IState> {
|
||||
export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||
private wrapperElement = createRef<HTMLDivElement>();
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
callState: this.props.callEventGrouper.state,
|
||||
silenced: false,
|
||||
narrow: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
||||
this.resizeObserver.observe(this.wrapperElement.current);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
private resizeObserverCallback = (entries: ResizeObserverEntry[]): void => {
|
||||
const wrapperElementEntry = entries.find((entry) => entry.target === this.wrapperElement.current);
|
||||
if (!wrapperElementEntry) return;
|
||||
|
||||
this.setState({ narrow: wrapperElementEntry.contentRect.width < MAX_NON_NARROW_WIDTH });
|
||||
};
|
||||
|
||||
private onSilencedChanged = (newState) => {
|
||||
this.setState({ silenced: newState });
|
||||
};
|
||||
|
@ -82,21 +101,32 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderSilenceIcon(): JSX.Element {
|
||||
const silenceClass = classNames({
|
||||
"mx_CallEvent_iconButton": true,
|
||||
"mx_CallEvent_unSilence": this.state.silenced,
|
||||
"mx_CallEvent_silence": !this.state.silenced,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className={silenceClass}
|
||||
onClick={this.props.callEventGrouper.toggleSilenced}
|
||||
title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
let silenceIcon;
|
||||
if (!this.state.narrow) {
|
||||
silenceIcon = this.renderSilenceIcon();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<AccessibleTooltipButton
|
||||
className={silenceClass}
|
||||
onClick={this.props.callEventGrouper.toggleSilenced}
|
||||
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
|
||||
/>
|
||||
{ silenceIcon }
|
||||
<AccessibleButton
|
||||
className="mx_CallEvent_content_button mx_CallEvent_content_button_reject"
|
||||
onClick={this.props.callEventGrouper.rejectCall}
|
||||
|
@ -145,7 +175,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
{ _t("Missed call") }
|
||||
{ _t("No answer") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
</div>
|
||||
);
|
||||
|
@ -169,7 +199,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
} else if (hangupReason === CallErrorCode.UserBusy) {
|
||||
reason = _t("The user you called is busy.");
|
||||
} else {
|
||||
reason = _t('Unknown failure: %(reason)s)', { reason: hangupReason });
|
||||
reason = _t('Unknown failure: %(reason)s', { reason: hangupReason });
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -215,35 +245,41 @@ export default class CallEvent extends React.Component<IProps, IState> {
|
|||
const callState = this.state.callState;
|
||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||
const content = this.renderContent(callState);
|
||||
const className = classNames({
|
||||
mx_CallEvent: true,
|
||||
const className = classNames("mx_CallEvent", {
|
||||
mx_CallEvent_voice: isVoice,
|
||||
mx_CallEvent_video: !isVoice,
|
||||
mx_CallEvent_missed: (
|
||||
callState === CustomCallState.Missed ||
|
||||
(callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout)
|
||||
),
|
||||
mx_CallEvent_narrow: this.state.narrow,
|
||||
mx_CallEvent_missed: callState === CustomCallState.Missed,
|
||||
mx_CallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
|
||||
mx_CallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
|
||||
});
|
||||
let silenceIcon;
|
||||
if (this.state.narrow && this.state.callState === CallState.Ringing) {
|
||||
silenceIcon = this.renderSilenceIcon();
|
||||
}
|
||||
|
||||
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" />
|
||||
{ callType }
|
||||
<div className="mx_CallEvent_wrapper" ref={this.wrapperElement}>
|
||||
<div className={className}>
|
||||
{ silenceIcon }
|
||||
<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" />
|
||||
{ callType }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ content }
|
||||
</div>
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,12 +25,14 @@ 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 { Media, mediaFromContent } from "../../../customisations/Media";
|
||||
import { BLURHASH_FIELD } from "../../../ContentMessages";
|
||||
import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import { SyncState } from 'matrix-js-sdk/src/sync.api';
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
import classNames from 'classnames';
|
||||
import { CSSTransition, SwitchTransition } from 'react-transition-group';
|
||||
|
||||
interface IState {
|
||||
decryptedUrl?: string;
|
||||
|
@ -157,19 +159,21 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
// this is only used as a fallback in case content.info.w/h is missing
|
||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||
}
|
||||
|
||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||
};
|
||||
|
||||
protected getContentUrl(): string {
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
if (media.isEncrypted) {
|
||||
if (this.media.isEncrypted) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return media.srcHttp;
|
||||
return this.media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
private get media(): Media {
|
||||
return mediaFromContent(this.props.mxEvent.getContent());
|
||||
}
|
||||
|
||||
protected getThumbUrl(): string {
|
||||
// FIXME: we let images grow as wide as you like, rather than capped to 800x600.
|
||||
// So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
|
||||
|
@ -225,7 +229,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
info.w > thumbWidth ||
|
||||
info.h > thumbHeight
|
||||
);
|
||||
const isLargeFileSize = info.size > 1*1024*1024; // 1mb
|
||||
const isLargeFileSize = info.size > 1 * 1024 * 1024; // 1mb
|
||||
|
||||
if (isLargeFileSize && isLargerThanThumbnail) {
|
||||
// image is too large physically and bytewise to clutter our timeline so
|
||||
|
@ -374,23 +378,40 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_MImageBody_thumbnail': true,
|
||||
'mx_MImageBody_thumbnail--blurhash': this.props.mxEvent.getContent().info[BLURHASH_FIELD],
|
||||
});
|
||||
|
||||
// This has incredibly broken types.
|
||||
const C = CSSTransition as any;
|
||||
const thumbnail = (
|
||||
<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: `min(100%, ${infoWidth}px)`,
|
||||
}}
|
||||
<SwitchTransition mode="out-in">
|
||||
<C
|
||||
classNames="mx_rtg--fade"
|
||||
key={`img-${showPlaceholder}`}
|
||||
timeout={300}
|
||||
>
|
||||
{ placeholder }
|
||||
</div>
|
||||
}
|
||||
{ /* This weirdly looking div is necessary here, otherwise SwitchTransition fails */ }
|
||||
<div>
|
||||
{ showPlaceholder && <div
|
||||
className={classes}
|
||||
style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: `min(100%, ${infoWidth}px)`,
|
||||
maxHeight: maxHeight,
|
||||
aspectRatio: `${infoWidth}/${infoHeight}`,
|
||||
}}
|
||||
>
|
||||
{ placeholder }
|
||||
</div> }
|
||||
</div>
|
||||
</C>
|
||||
</SwitchTransition>
|
||||
|
||||
<div style={{
|
||||
display: !showPlaceholder ? undefined : 'none',
|
||||
height: '100%', // Also force to size of a parent to prevent scroll-jumps (see above)
|
||||
height: '100%',
|
||||
}}>
|
||||
{ img }
|
||||
{ gifLabel }
|
||||
|
@ -413,7 +434,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
// Overidden by MStickerBody
|
||||
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} />;
|
||||
if (blurhash) return <Blurhash className="mx_Blurhash" hash={blurhash} width={width} height={height} />;
|
||||
return (
|
||||
<InlineSpinner w={32} h={32} />
|
||||
);
|
||||
|
@ -455,10 +476,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||
const thumbnail = this.messageContent(contentUrl, thumbUrl, content);
|
||||
const fileBody = this.getFileBody();
|
||||
|
||||
return <div className="mx_MImageBody">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_MImageBody">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue