Merge pull request #6556 from SimonBrandner/feature/narrow-voip-tiles/18398

Fix call tile behaviour on narrow layouts
This commit is contained in:
David Baker 2021-08-13 09:44:29 +01:00 committed by GitHub
commit ebc7d9ee1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 220 additions and 143 deletions

View file

@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_CallEvent { .mx_CallEvent_wrapper {
display: flex;
justify-content: center;
width: 100%;
.mx_CallEvent {
position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -22,10 +28,34 @@ limitations under the License.
background-color: $dark-panel-bg-color; background-color: $dark-panel-bg-color;
border-radius: 8px; border-radius: 8px;
margin: 10px auto;
width: 75%; width: 75%;
box-sizing: border-box; box-sizing: border-box;
height: 60px; height: 60px;
margin: 4px 0;
.mx_CallEvent_iconButton {
display: inline-flex;
margin-right: 8px;
&::before {
content: '';
height: 16px;
width: 16px;
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.mx_CallEvent_silence::before {
mask-image: url('$(res)/img/voip/silence.svg');
}
.mx_CallEvent_unSilence::before {
mask-image: url('$(res)/img/voip/un-silence.svg');
}
&.mx_CallEvent_voice { &.mx_CallEvent_voice {
.mx_CallEvent_type_icon::before, .mx_CallEvent_type_icon::before,
@ -102,11 +132,11 @@ limitations under the License.
align-items: center; align-items: center;
color: $secondary-fg-color; color: $secondary-fg-color;
margin-right: 16px; margin-right: 16px;
gap: 8px;
.mx_CallEvent_content_button { .mx_CallEvent_content_button {
height: 24px; height: 24px;
padding: 0px 12px; padding: 0px 12px;
margin-left: 8px;
span { span {
padding: 8px 0; padding: 8px 0;
@ -134,29 +164,39 @@ limitations under the License.
.mx_CallEvent_content_tooltip { .mx_CallEvent_content_tooltip {
margin-right: 5px; margin-right: 5px;
} }
}
&.mx_CallEvent_narrow {
height: unset;
width: 290px;
flex-direction: column;
align-items: unset;
gap: 16px;
.mx_CallEvent_iconButton { .mx_CallEvent_iconButton {
display: inline-flex; position: absolute;
margin-right: 8px; margin-right: 0;
top: 12px;
&::before { right: 12px;
content: '';
height: 16px; height: 16px;
width: 16px; width: 16px;
background-color: $tertiary-fg-color; display: flex;
mask-repeat: no-repeat; }
mask-size: contain;
mask-position: center; .mx_CallEvent_info {
align-items: unset;
margin-top: 12px;
margin-right: 12px;
.mx_CallEvent_sender {
margin-bottom: 8px;
} }
} }
.mx_CallEvent_silence::before { .mx_CallEvent_content {
mask-image: url('$(res)/img/voip/silence.svg'); margin-left: 54px; // mx_CallEvent margin (12px) + avatar (32px) + mx_CallEvent_info_basic margin (10px)
margin-bottom: 16px;
} }
.mx_CallEvent_unSilence::before {
mask-image: url('$(res)/img/voip/un-silence.svg');
} }
} }
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { createRef } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
@ -27,6 +27,8 @@ import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { formatCallTime } from "../../../DateUtils"; import { formatCallTime } from "../../../DateUtils";
const MAX_NON_NARROW_WIDTH = 400 / 70 * 100;
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
callEventGrouper: CallEventGrouper; callEventGrouper: CallEventGrouper;
@ -35,6 +37,7 @@ interface IProps {
interface IState { interface IState {
callState: CallState | CustomCallState; callState: CallState | CustomCallState;
silenced: boolean; silenced: boolean;
narrow: boolean;
} }
const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([ 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")], [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) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
callState: this.props.callEventGrouper.state, callState: this.props.callEventGrouper.state,
silenced: false, silenced: false,
narrow: false,
}; };
} }
componentDidMount() { componentDidMount() {
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged); this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
this.resizeObserver.observe(this.wrapperElement.current);
} }
componentWillUnmount() { componentWillUnmount() {
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged); 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) => { private onSilencedChanged = (newState) => {
this.setState({ silenced: newState }); this.setState({ silenced: newState });
}; };
@ -82,8 +101,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
); );
} }
private renderContent(state: CallState | CustomCallState): JSX.Element { private renderSilenceIcon(): JSX.Element {
if (state === CallState.Ringing) {
const silenceClass = classNames({ const silenceClass = classNames({
"mx_CallEvent_iconButton": true, "mx_CallEvent_iconButton": true,
"mx_CallEvent_unSilence": this.state.silenced, "mx_CallEvent_unSilence": this.state.silenced,
@ -91,12 +109,24 @@ export default class CallEvent extends React.Component<IProps, IState> {
}); });
return ( return (
<div className="mx_CallEvent_content">
<AccessibleTooltipButton <AccessibleTooltipButton
className={silenceClass} className={silenceClass}
onClick={this.props.callEventGrouper.toggleSilenced} onClick={this.props.callEventGrouper.toggleSilenced}
title={this.state.silenced ? _t("Sound on"): _t("Silence call")} title={this.state.silenced ? _t("Sound on") : _t("Silence call")}
/> />
);
}
private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) {
let silenceIcon;
if (!this.state.narrow) {
silenceIcon = this.renderSilenceIcon();
}
return (
<div className="mx_CallEvent_content">
{ silenceIcon }
<AccessibleButton <AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_reject" className="mx_CallEvent_content_button mx_CallEvent_content_button_reject"
onClick={this.props.callEventGrouper.rejectCall} onClick={this.props.callEventGrouper.rejectCall}
@ -215,18 +245,24 @@ export default class CallEvent extends React.Component<IProps, IState> {
const callState = this.state.callState; const callState = this.state.callState;
const hangupReason = this.props.callEventGrouper.hangupReason; const hangupReason = this.props.callEventGrouper.hangupReason;
const content = this.renderContent(callState); const content = this.renderContent(callState);
const className = classNames({ const className = classNames("mx_CallEvent", {
mx_CallEvent: true,
mx_CallEvent_voice: isVoice, mx_CallEvent_voice: isVoice,
mx_CallEvent_video: !isVoice, mx_CallEvent_video: !isVoice,
mx_CallEvent_narrow: this.state.narrow,
mx_CallEvent_missed: ( mx_CallEvent_missed: (
callState === CustomCallState.Missed || callState === CustomCallState.Missed ||
(callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout) (callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout)
), ),
}); });
let silenceIcon;
if (this.state.narrow && this.state.callState === CallState.Ringing) {
silenceIcon = this.renderSilenceIcon();
}
return ( return (
<div className="mx_CallEvent_wrapper" ref={this.wrapperElement}>
<div className={className}> <div className={className}>
{ silenceIcon }
<div className="mx_CallEvent_info"> <div className="mx_CallEvent_info">
<MemberAvatar <MemberAvatar
member={event.sender} member={event.sender}
@ -245,6 +281,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
</div> </div>
{ content } { content }
</div> </div>
</div>
); );
} }
} }