Use CallViewButtons

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-08-08 11:20:17 +02:00
parent 9f28c30145
commit d0e76a0ecd
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
2 changed files with 46 additions and 368 deletions

View file

@ -47,11 +47,11 @@ limitations under the License.
height: 180px; height: 180px;
} }
.mx_CallView_callControls { .mx_CallViewButtons {
bottom: 0px; bottom: 0px;
} }
.mx_CallView_callControls_button { .mx_CallViewButtons_button {
&::before { &::before {
width: 36px; width: 36px;
height: 36px; height: 36px;
@ -199,20 +199,6 @@ limitations under the License.
} }
} }
.mx_CallView_callControls {
position: absolute;
display: flex;
justify-content: center;
bottom: 5px;
opacity: 1;
transition: opacity 0.5s;
z-index: 200; // To be above _all_ feeds
}
.mx_CallView_callControls_hidden {
opacity: 0.001; // opacity 0 can cause a re-layout
pointer-events: none;
}
.mx_CallView_presenting { .mx_CallView_presenting {
opacity: 1; opacity: 1;
@ -232,94 +218,3 @@ limitations under the License.
opacity: 0.001; // opacity 0 can cause a re-layout opacity: 0.001; // opacity 0 can cause a re-layout
pointer-events: none; pointer-events: none;
} }
.mx_CallView_callControls_button {
cursor: pointer;
margin-left: 2px;
margin-right: 2px;
&::before {
content: '';
display: inline-block;
height: 48px;
width: 48px;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
}
}
.mx_CallView_callControls_dialpad {
&::before {
background-image: url('$(res)/img/voip/dialpad.svg');
}
}
.mx_CallView_callControls_button_micOn {
&::before {
background-image: url('$(res)/img/voip/mic-on.svg');
}
}
.mx_CallView_callControls_button_micOff {
&::before {
background-image: url('$(res)/img/voip/mic-off.svg');
}
}
.mx_CallView_callControls_button_vidOn {
&::before {
background-image: url('$(res)/img/voip/vid-on.svg');
}
}
.mx_CallView_callControls_button_vidOff {
&::before {
background-image: url('$(res)/img/voip/vid-off.svg');
}
}
.mx_CallView_callControls_button_screensharingOn {
&::before {
background-image: url('$(res)/img/voip/screensharing-on.svg');
}
}
.mx_CallView_callControls_button_screensharingOff {
&::before {
background-image: url('$(res)/img/voip/screensharing-off.svg');
}
}
.mx_CallView_callControls_button_sidebarOn {
&::before {
background-image: url('$(res)/img/voip/sidebar-on.svg');
}
}
.mx_CallView_callControls_button_sidebarOff {
&::before {
background-image: url('$(res)/img/voip/sidebar-off.svg');
}
}
.mx_CallView_callControls_button_hangup {
&::before {
background-image: url('$(res)/img/voip/hangup.svg');
}
}
.mx_CallView_callControls_button_more {
&::before {
background-image: url('$(res)/img/voip/more.svg');
}
}
.mx_CallView_callControls_button_invisible {
visibility: hidden;
pointer-events: none;
position: absolute;
}

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com> Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -27,15 +27,7 @@ import { CallEvent, CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/we
import classNames from 'classnames'; import classNames from 'classnames';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard'; import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard';
import {
alwaysAboveLeftOf,
alwaysAboveRightOf,
ChevronFace,
ContextMenuTooltipButton,
} from '../../structures/ContextMenu';
import CallContextMenu from '../context_menus/CallContextMenu';
import { avatarUrlForMember } from '../../../Avatar'; import { avatarUrlForMember } from '../../../Avatar';
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed'; import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker"; import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
@ -43,8 +35,7 @@ import Modal from '../../../Modal';
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes'; import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
import CallViewSidebar from './CallViewSidebar'; import CallViewSidebar from './CallViewSidebar';
import CallViewHeader from './CallView/CallViewHeader'; import CallViewHeader from './CallView/CallViewHeader';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import CallViewButtons from "./CallView/CallViewButtons";
import { Alignment } from "../elements/Tooltip";
interface IProps { interface IProps {
// The call for us to display // The call for us to display
@ -83,8 +74,6 @@ interface IState {
sidebarShown: boolean; sidebarShown: boolean;
} }
const tooltipYOffset = -24;
function getFullScreenElement() { function getFullScreenElement() {
return ( return (
document.fullscreenElement || document.fullscreenElement ||
@ -113,18 +102,11 @@ function exitFullscreen() {
if (exitMethod) exitMethod.call(document); if (exitMethod) exitMethod.call(document);
} }
const CONTROLS_HIDE_DELAY = 2000;
// Height of the header duplicated from CSS because we need to subtract it from our max
// height to get the max height of the video
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
@replaceableComponent("views.voip.CallView") @replaceableComponent("views.voip.CallView")
export default class CallView extends React.Component<IProps, IState> { export default class CallView extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private contentRef = createRef<HTMLDivElement>(); private contentRef = createRef<HTMLDivElement>();
private controlsHideTimer: number = null; private buttonsRef = createRef<CallViewButtons>();
private dialpadButton = createRef<HTMLDivElement>();
private contextMenuButton = createRef<HTMLDivElement>();
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -240,16 +222,8 @@ export default class CallView extends React.Component<IProps, IState> {
}); });
}; };
private onControlsHideTimer = () => {
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
this.controlsHideTimer = null;
this.setState({
controlsVisible: false,
});
};
private onMouseMove = () => { private onMouseMove = () => {
this.showControls(); this.buttonsRef.current?.showControls();
}; };
private getOrderedFeeds(feeds: Array<CallFeed>): { primary: CallFeed, secondary: Array<CallFeed> } { private getOrderedFeeds(feeds: Array<CallFeed>): { primary: CallFeed, secondary: Array<CallFeed> } {
@ -275,29 +249,6 @@ export default class CallView extends React.Component<IProps, IState> {
return { primary, secondary }; return { primary, secondary };
} }
private showControls(): void {
if (this.state.showMoreMenu || this.state.showDialpad) return;
if (!this.state.controlsVisible) {
this.setState({
controlsVisible: true,
});
}
if (this.controlsHideTimer !== null) {
clearTimeout(this.controlsHideTimer);
}
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
}
private onDialpadClick = (): void => {
if (!this.state.showDialpad) {
this.setState({ showDialpad: true });
this.showControls();
} else {
this.setState({ showDialpad: false });
}
};
private onMicMuteClick = (): void => { private onMicMuteClick = (): void => {
const newVal = !this.state.micMuted; const newVal = !this.state.micMuted;
@ -328,19 +279,6 @@ export default class CallView extends React.Component<IProps, IState> {
}); });
}; };
private onMoreClick = (): void => {
this.setState({ showMoreMenu: true });
this.showControls();
};
private closeDialpad = (): void => {
this.setState({ showDialpad: false });
};
private closeContextMenu = (): void => {
this.setState({ showMoreMenu: false });
};
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire // we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
// Note that this assumes we always have a CallView on screen at any given time // Note that this assumes we always have a CallView on screen at any given time
// CallHandler would probably be a better place for this // CallHandler would probably be a better place for this
@ -353,7 +291,7 @@ export default class CallView extends React.Component<IProps, IState> {
if (ctrlCmdOnly) { if (ctrlCmdOnly) {
this.onMicMuteClick(); this.onMicMuteClick();
// show the controls to give feedback // show the controls to give feedback
this.showControls(); this.buttonsRef.current?.showControls();
handled = true; handled = true;
} }
break; break;
@ -362,7 +300,7 @@ export default class CallView extends React.Component<IProps, IState> {
if (ctrlCmdOnly) { if (ctrlCmdOnly) {
this.onVidMuteClick(); this.onVidMuteClick();
// show the controls to give feedback // show the controls to give feedback
this.showControls(); this.buttonsRef.current?.showControls();
handled = true; handled = true;
} }
break; break;
@ -374,15 +312,6 @@ export default class CallView extends React.Component<IProps, IState> {
} }
}; };
private onCallControlsMouseEnter = (): void => {
this.setState({ hoveringControls: true });
this.showControls();
};
private onCallControlsMouseLeave = (): void => {
this.setState({ hoveringControls: false });
};
private onCallResumeClick = (): void => { private onCallResumeClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId); CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
@ -401,206 +330,60 @@ export default class CallView extends React.Component<IProps, IState> {
}; };
private onToggleSidebar = (): void => { private onToggleSidebar = (): void => {
this.setState({ this.setState({ sidebarShown: !this.state.sidebarShown });
sidebarShown: !this.state.sidebarShown,
});
}; };
private renderCallControls(): JSX.Element { private renderCallControls(): JSX.Element {
const micClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_micOn: !this.state.micMuted,
mx_CallView_callControls_button_micOff: this.state.micMuted,
});
const vidClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
});
const screensharingClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_screensharingOn: this.state.screensharing,
mx_CallView_callControls_button_screensharingOff: !this.state.screensharing,
});
const sidebarButtonClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_sidebarOn: this.state.sidebarShown,
mx_CallView_callControls_button_sidebarOff: !this.state.sidebarShown,
});
// Put the other states of the mic/video icons in the document to make sure they're cached
// (otherwise the icon disappears briefly when toggled)
const micCacheClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_micOn: this.state.micMuted,
mx_CallView_callControls_button_micOff: !this.state.micMuted,
mx_CallView_callControls_button_invisible: true,
});
const vidCacheClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_vidOn: this.state.micMuted,
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
mx_CallView_callControls_button_invisible: true,
});
const callControlsClasses = classNames({
mx_CallView_callControls: true,
mx_CallView_callControls_hidden: !this.state.controlsVisible,
});
// We don't support call upgrades (yet) so hide the video mute button in voice calls // We don't support call upgrades (yet) so hide the video mute button in voice calls
let vidMuteButton; const vidMuteButtonShown = this.props.call.type === CallType.Video;
if (this.props.call.type === CallType.Video) {
vidMuteButton = (
<AccessibleTooltipButton
className={vidClasses}
onClick={this.onVidMuteClick}
title={this.state.vidMuted ? _t("Start the camera") : _t("Stop the camera")}
alignment={Alignment.Top}
yOffset={tooltipYOffset}
/>
);
}
// Screensharing is possible, if we can send a second stream and // Screensharing is possible, if we can send a second stream and
// identify it using SDPStreamMetadata or if we can replace the already // identify it using SDPStreamMetadata or if we can replace the already
// existing usermedia track by a screensharing track. We also need to be // existing usermedia track by a screensharing track. We also need to be
// connected to know the state of the other side // connected to know the state of the other side
let screensharingButton; const screensharingButtonShown = (
if (
(this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) && (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
this.props.call.state === CallState.Connected this.props.call.state === CallState.Connected
) {
screensharingButton = (
<AccessibleTooltipButton
className={screensharingClasses}
onClick={this.onScreenshareClick}
title={this.state.screensharing
? _t("Stop sharing your screen")
: _t("Start sharing your screen")
}
alignment={Alignment.Top}
yOffset={tooltipYOffset}
/>
); );
}
// To show the sidebar we need secondary feeds, if we don't have them, // To show the sidebar we need secondary feeds, if we don't have them,
// we can hide this button. If we are in PiP, sidebar is also hidden, so // we can hide this button. If we are in PiP, sidebar is also hidden, so
// we can hide the button too // we can hide the button too
let sidebarButton; const sidebarButtonShown = (
if (
!this.props.pipMode &&
(
this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare || this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
this.props.call.isScreensharing() this.props.call.isScreensharing()
)
) {
sidebarButton = (
<AccessibleButton
className={sidebarButtonClasses}
onClick={this.onToggleSidebar}
aria-label={this.state.sidebarShown ? _t("Hide sidebar") : _t("Show sidebar")}
/>
); );
}
// The dial pad & 'more' button actions are only relevant in a connected call // The dial pad & 'more' button actions are only relevant in a connected call
let contextMenuButton; const contextMenuButtonShown = this.state.callState === CallState.Connected;
if (this.state.callState === CallState.Connected) { const dialpadButtonShown = (
contextMenuButton = ( this.state.callState === CallState.Connected &&
<ContextMenuTooltipButton this.props.call.opponentSupportsDTMF()
className="mx_CallView_callControls_button mx_CallView_callControls_button_more"
onClick={this.onMoreClick}
inputRef={this.contextMenuButton}
isExpanded={this.state.showMoreMenu}
title={_t("More")}
alignment={Alignment.Top}
yOffset={tooltipYOffset}
/>
); );
}
let dialpadButton;
if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) {
dialpadButton = (
<ContextMenuTooltipButton
className="mx_CallView_callControls_button mx_CallView_callControls_dialpad"
inputRef={this.dialpadButton}
onClick={this.onDialpadClick}
isExpanded={this.state.showDialpad}
title={_t("Dialpad")}
alignment={Alignment.Top}
yOffset={tooltipYOffset}
/>
);
}
let dialPad;
if (this.state.showDialpad) {
dialPad = <DialpadContextMenu
{...alwaysAboveRightOf(
this.dialpadButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
// We mount the context menus as a as a child typically in order to include the
// context menus when fullscreening the call content.
// However, this does not work as well when the call is embedded in a
// picture-in-picture frame. Thus, only mount as child when we are *not* in PiP.
mountAsChild={!this.props.pipMode}
onFinished={this.closeDialpad}
call={this.props.call}
/>;
}
let contextMenu;
if (this.state.showMoreMenu) {
contextMenu = <CallContextMenu
{...alwaysAboveLeftOf(
this.contextMenuButton.current.getBoundingClientRect(),
ChevronFace.None,
CONTEXT_MENU_VPADDING,
)}
mountAsChild={!this.props.pipMode}
onFinished={this.closeContextMenu}
call={this.props.call}
/>;
}
return ( return (
<div <CallViewButtons
className={callControlsClasses} ref={this.buttonsRef}
onMouseEnter={this.onCallControlsMouseEnter} call={this.props.call}
onMouseLeave={this.onCallControlsMouseLeave} pipMode={this.props.pipMode}
> handlers={{
{ dialPad } onToggleSidebarClick: this.onToggleSidebar,
{ contextMenu } onScreenshareClick: this.onScreenshareClick,
{ dialpadButton } onHangupClick: this.onHangupClick,
<AccessibleTooltipButton onMicMuteClick: this.onMicMuteClick,
className={micClasses} onVidMuteClick: this.onVidMuteClick,
onClick={this.onMicMuteClick} }}
title={this.state.micMuted ? _t("Unmute the microphone") : _t("Mute the microphone")} buttonsState={{
alignment={Alignment.Top} micMuted: this.state.micMuted,
yOffset={tooltipYOffset} vidMuted: this.state.vidMuted,
sidebarShown: this.state.sidebarShown,
screensharing: this.state.screensharing,
}}
buttonsVisibility={{
vidMute: vidMuteButtonShown,
screensharing: screensharingButtonShown,
sidebar: sidebarButtonShown,
contextMenu: contextMenuButtonShown,
dialpad: dialpadButtonShown,
}}
/> />
{ vidMuteButton }
<div className={micCacheClasses} />
<div className={vidCacheClasses} />
{ screensharingButton }
{ sidebarButton }
{ contextMenuButton }
<AccessibleTooltipButton
className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
onClick={this.onHangupClick}
title={_t("Hangup")}
alignment={Alignment.Top}
yOffset={tooltipYOffset}
/>
</div>
); );
} }