Handle narrow layouts

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-08-05 11:47:58 +02:00
parent 4dda4b241a
commit 9e4f5719a4
No known key found for this signature in database
GPG key ID: CC823428E9B582FB
2 changed files with 218 additions and 143 deletions

View file

@ -14,126 +14,23 @@ 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; display: flex;
flex-direction: row; justify-content: center;
align-items: center; width: 100%;
justify-content: space-between;
background-color: $dark-panel-bg-color; .mx_CallEvent {
border-radius: 8px; position: relative;
margin: 10px auto;
width: 75%;
box-sizing: border-box;
height: 60px;
&.mx_CallEvent_voice {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
}
&.mx_CallEvent_video {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
}
&.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-voice.svg');
}
&.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-video.svg');
}
.mx_CallEvent_info {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-left: 12px; justify-content: space-between;
.mx_CallEvent_info_basic { background-color: $dark-panel-bg-color;
display: flex; border-radius: 8px;
flex-direction: column; width: 75%;
margin-left: 10px; // To match mx_CallEvent box-sizing: border-box;
height: 60px;
.mx_CallEvent_sender {
font-weight: 600;
font-size: 1.5rem;
line-height: 1.8rem;
margin-bottom: 3px;
}
.mx_CallEvent_type {
font-weight: 400;
color: $secondary-fg-color;
font-size: 1.2rem;
line-height: $font-13px;
display: flex;
align-items: center;
.mx_CallEvent_type_icon {
height: 13px;
width: 13px;
margin-right: 5px;
&::before {
content: '';
position: absolute;
height: 13px;
width: 13px;
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
}
}
}
}
}
.mx_CallEvent_content {
display: flex;
flex-direction: row;
align-items: center;
color: $secondary-fg-color;
margin-right: 16px;
.mx_CallEvent_content_button {
height: 24px;
padding: 0px 12px;
margin-left: 8px;
span {
padding: 8px 0;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
background-color: $button-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 16px;
width: 16px;
height: 16px;
margin-right: 8px;
}
}
}
.mx_CallEvent_content_button_reject span::before {
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
}
.mx_CallEvent_content_tooltip {
margin-right: 5px;
}
.mx_CallEvent_iconButton { .mx_CallEvent_iconButton {
display: inline-flex; display: inline-flex;
@ -158,5 +55,146 @@ limitations under the License.
.mx_CallEvent_unSilence::before { .mx_CallEvent_unSilence::before {
mask-image: url('$(res)/img/voip/un-silence.svg'); mask-image: url('$(res)/img/voip/un-silence.svg');
} }
&.mx_CallEvent_voice {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
}
&.mx_CallEvent_video {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
}
&.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-voice.svg');
}
&.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-video.svg');
}
.mx_CallEvent_info {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 12px;
.mx_CallEvent_info_basic {
display: flex;
flex-direction: column;
margin-left: 10px; // To match mx_CallEvent
.mx_CallEvent_sender {
font-weight: 600;
font-size: 1.5rem;
line-height: 1.8rem;
margin-bottom: 3px;
}
.mx_CallEvent_type {
font-weight: 400;
color: $secondary-fg-color;
font-size: 1.2rem;
line-height: $font-13px;
display: flex;
align-items: center;
.mx_CallEvent_type_icon {
height: 13px;
width: 13px;
margin-right: 5px;
&::before {
content: '';
position: absolute;
height: 13px;
width: 13px;
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
}
}
}
}
}
.mx_CallEvent_content {
display: flex;
flex-direction: row;
align-items: center;
color: $secondary-fg-color;
margin-right: 16px;
.mx_CallEvent_content_button {
height: 24px;
padding: 0px 12px;
margin-left: 8px;
span {
padding: 8px 0;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
background-color: $button-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 16px;
width: 16px;
height: 16px;
margin-right: 8px;
}
}
}
.mx_CallEvent_content_button_reject span::before {
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
}
.mx_CallEvent_content_tooltip {
margin-right: 5px;
}
}
&.mx_CallEvent_narrow {
height: unset;
width: 290px;
flex-direction: column;
align-items: unset;
gap: 16px;
.mx_CallEvent_iconButton {
position: absolute;
margin-right: 0;
top: 12px;
right: 12px;
height: 16px;
width: 16px;
display: flex;
}
.mx_CallEvent_info {
margin-top: 12px;
margin-right: 12px;
.mx_CallEvent_sender {
margin-bottom: 8px;
}
}
.mx_CallEvent_content {
margin-left: 54px; // mx_CallEvent margin (12px) + avatar (32px) + mx_CallEvent_info_basic margin (10px)
margin-bottom: 16px;
}
}
} }
} }

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';
@ -26,6 +26,8 @@ import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import classNames from 'classnames'; import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
const MAX_NON_NARROW_WIDTH = 400 / 70 * 100;
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
callEventGrouper: CallEventGrouper; callEventGrouper: CallEventGrouper;
@ -34,6 +36,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([
@ -41,26 +44,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 });
}; };
@ -81,21 +100,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 { private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) { if (state === CallState.Ringing) {
const silenceClass = classNames({ let silenceIcon;
"mx_CallEvent_iconButton": true, if (!this.state.narrow) {
"mx_CallEvent_unSilence": this.state.silenced, silenceIcon = this.renderSilenceIcon();
"mx_CallEvent_silence": !this.state.silenced, }
});
return ( return (
<div className="mx_CallEvent_content"> <div className="mx_CallEvent_content">
<AccessibleTooltipButton { silenceIcon }
className={silenceClass}
onClick={this.props.callEventGrouper.toggleSilenced}
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
/>
<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}
@ -209,35 +239,42 @@ 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={className}> <div className="mx_CallEvent_wrapper" ref={this.wrapperElement}>
<div className="mx_CallEvent_info"> <div className={className}>
<MemberAvatar { silenceIcon }
member={event.sender} <div className="mx_CallEvent_info">
width={32} <MemberAvatar
height={32} member={event.sender}
/> width={32}
<div className="mx_CallEvent_info_basic"> height={32}
<div className="mx_CallEvent_sender"> />
{ sender } <div className="mx_CallEvent_info_basic">
</div> <div className="mx_CallEvent_sender">
<div className="mx_CallEvent_type"> { sender }
<div className="mx_CallEvent_type_icon" /> </div>
{ callType } <div className="mx_CallEvent_type">
<div className="mx_CallEvent_type_icon" />
{ callType }
</div>
</div> </div>
</div> </div>
{ content }
</div> </div>
{ content }
</div> </div>
); );
} }