mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +03:00
Handle narrow layouts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
parent
4dda4b241a
commit
9e4f5719a4
2 changed files with 218 additions and 143 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue