From a2d58e2f1f69acadcb5e4abf8bb66f15e5e83c4e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Nov 2020 14:22:38 +0000 Subject: [PATCH] Make new in-call UI work * Buttons on the main view will disappear after 1 second of the user not moving the mouse over the call view. * PIP view has no buttons, and not moveable yet * No call status in room view yet * Room status bar is still there currently --- res/css/views/voip/_CallView.scss | 117 +++++++++------ res/img/hangup.svg | 15 -- res/img/voip_buttons/hangup.svg | 17 +++ res/img/voip_buttons/mic_off.svg | 18 +++ res/img/voip_buttons/mic_on.svg | 17 +++ res/img/voip_buttons/vid_off.svg | 19 +++ res/img/voip_buttons/vid_on.svg | 18 +++ src/components/views/voip/CallView.tsx | 196 +++++++++++++++++-------- 8 files changed, 293 insertions(+), 124 deletions(-) delete mode 100644 res/img/hangup.svg create mode 100644 res/img/voip_buttons/hangup.svg create mode 100644 res/img/voip_buttons/mic_off.svg create mode 100644 res/img/voip_buttons/mic_on.svg create mode 100644 res/img/voip_buttons/vid_off.svg create mode 100644 res/img/voip_buttons/vid_on.svg diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index ceb8e798b8..f3f4eb63de 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -41,58 +41,13 @@ limitations under the License. } .mx_CallView_voice { + position: relative; display: flex; align-items: center; justify-content: center; background-color: $inverted-bg-color; } -/* -.mx_CallView_voice { - padding: 6px; - font-weight: bold; - - min-width: 200px; - text-align: center; - vertical-align: middle; -} - -.mx_CallView_hangup { - position: absolute; - - right: 8px; - bottom: 10px; - - height: 35px; - width: 35px; - - border-radius: 35px; - - background-color: $notice-primary-color; - - z-index: 101; - - cursor: pointer; - - &::before { - content: ''; - position: absolute; - - height: 20px; - width: 20px; - - top: 6.5px; - left: 7.5px; - - mask: url('$(res)/img/hangup.svg'); - mask-size: contain; - background-size: contain; - - background-color: $primary-fg-color; - } -} -*/ - .mx_CallView_video { width: 100%; position: relative; @@ -172,3 +127,73 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } } + +.mx_CallView_callControls { + position: absolute; + display: flex; + justify-content: center; + bottom: 5px; + width: 100%; + opacity: 1; + transition: opacity 0.5s; +} + +.mx_CallView_callControls_hidden { + opacity: 0.001; + pointer-events: none; +} + +.mx_CallView_callControls_button { + cursor: pointer; + + &::before { + content: ''; + display: inline-block; + + height: 48px; + width: 48px; + + background-repeat: no-repeat; + background-size: contain; + background-position: center; + } +} + +.mx_CallView_callControls_button_micOn { + &::before { + background-image: url('$(res)/img/voip_buttons/mic_on.svg'); + } +} + +.mx_CallView_callControls_button_micOff { + &::before { + background-image: url('$(res)/img/voip_buttons/mic_off.svg'); + } +} + +.mx_CallView_callControls_button_vidOn { + &::before { + background-image: url('$(res)/img/voip_buttons/vid_on.svg'); + } +} + +.mx_CallView_callControls_button_vidOff { + &::before { + background-image: url('$(res)/img/voip_buttons/vid_off.svg'); + } +} + +.mx_CallView_callControls_button_hangup { + margin-left: 16px; + margin-right: 16px; + + &::before { + background-image: url('$(res)/img/voip_buttons/hangup.svg'); + } +} + +.mx_CallView_callControls_button_invisible { + visibility: hidden; + pointer-events: none; + position: absolute; +} diff --git a/res/img/hangup.svg b/res/img/hangup.svg deleted file mode 100644 index be038d2b30..0000000000 --- a/res/img/hangup.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - Fill 72 + Path 98 - Created with Sketch. - - - - - - - - - - \ No newline at end of file diff --git a/res/img/voip_buttons/hangup.svg b/res/img/voip_buttons/hangup.svg new file mode 100644 index 0000000000..dfb20bd519 --- /dev/null +++ b/res/img/voip_buttons/hangup.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip_buttons/mic_off.svg b/res/img/voip_buttons/mic_off.svg new file mode 100644 index 0000000000..6409f1fd07 --- /dev/null +++ b/res/img/voip_buttons/mic_off.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip_buttons/mic_on.svg b/res/img/voip_buttons/mic_on.svg new file mode 100644 index 0000000000..3493b3c581 --- /dev/null +++ b/res/img/voip_buttons/mic_on.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip_buttons/vid_off.svg b/res/img/voip_buttons/vid_off.svg new file mode 100644 index 0000000000..199d97ab97 --- /dev/null +++ b/res/img/voip_buttons/vid_off.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip_buttons/vid_on.svg b/res/img/voip_buttons/vid_on.svg new file mode 100644 index 0000000000..d8146d01d3 --- /dev/null +++ b/res/img/voip_buttons/vid_on.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 7288cd1d5b..fb6a33d2a8 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -25,6 +25,8 @@ import VideoFeed, { VideoFeedType } from "./VideoFeed"; import RoomAvatar from "../avatars/RoomAvatar"; import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { CallEvent } from 'matrix-js-sdk/src/webrtc/call'; +import classNames from 'classnames'; +import AccessibleButton from '../elements/AccessibleButton'; interface IProps { // js-sdk room object. If set, we will only show calls for the given @@ -48,6 +50,10 @@ interface IProps { interface IState { call: MatrixCall; isLocalOnHold: boolean, + micMuted: boolean, + vidMuted: boolean, + callState: CallState, + controlsVisible: boolean, } function getFullScreenElement() { @@ -78,10 +84,12 @@ function exitFullscreen() { if (exitMethod) exitMethod.call(document); } +const CONTROLS_HIDE_DELAY = 1000; + export default class CallView extends React.Component { private dispatcherRef: string; private contentRef = createRef(); - + private controlsHideTimer: number = null; constructor(props: IProps) { super(props); @@ -89,6 +97,10 @@ export default class CallView extends React.Component { this.state = { call, isLocalOnHold: call ? call.isLocalOnHold() : null, + micMuted: call ? call.isMicrophoneMuted() : null, + vidMuted: call ? call.isLocalVideoMuted() : null, + callState: call ? call.state : null, + controlsVisible: true, } this.updateCallListeners(null, call); @@ -120,9 +132,21 @@ export default class CallView extends React.Component { const newCall = this.getCall(); if (newCall !== this.state.call) { this.updateCallListeners(this.state.call, newCall); + let newControlsVisible = this.state.controlsVisible; + if (newCall && !this.state.call) { + newControlsVisible = true; + if (this.controlsHideTimer !== null) { + clearTimeout(this.controlsHideTimer); + } + this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); + } this.setState({ call: newCall, isLocalOnHold: newCall ? newCall.isLocalOnHold() : null, + micMuted: newCall ? newCall.isMicrophoneMuted() : null, + vidMuted: newCall ? newCall.isLocalVideoMuted() : null, + callState: newCall ? newCall.state : null, + controlsVisible: newControlsVisible, }); } if (!newCall && getFullScreenElement()) { @@ -174,15 +198,115 @@ export default class CallView extends React.Component { }); }; + onControlsHideTimer = () => { + this.controlsHideTimer = null; + this.setState({ + controlsVisible: false, + }); + } + + onMouseMove = () => { + if (!this.state.controlsVisible) { + this.setState({ + controlsVisible: true, + }); + } + if (this.controlsHideTimer !== null) { + clearTimeout(this.controlsHideTimer); + } + this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); + } + + onMicMuteClick = () => { + const newVal = !this.state.micMuted; + + this.state.call.setMicrophoneMuted(newVal); + this.setState({micMuted: newVal}); + } + + onVidMuteClick = () => { + const newVal = !this.state.vidMuted; + + this.state.call.setLocalVideoMuted(newVal); + this.setState({vidMuted: newVal}); + } + + onRoomAvatarClick = () => { + dis.dispatch({ + action: 'view_room', + room_id: this.state.call.roomId, + }); + } + public render() { if (!this.state.call) return null; const client = MatrixClientPeg.get(); const callRoom = client.getRoom(this.state.call.roomId); - //const callControls =
+ let callControls; + if (this.props.room) { + 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, + }); + + // 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, + }); + + callControls =
+
+
{ + dis.dispatch({ + action: 'hangup', + room_id: this.state.call.roomId, + }); + }} + /> +
+
+
+
; + } // The 'content' for the call, ie. the videos for a video call and profile picture // for voice calls (fills the bg) @@ -196,76 +320,20 @@ export default class CallView extends React.Component { maxHeight={maxVideoHeight} /> + {callControls}
; } else { const avatarSize = this.props.room ? 200 : 75; - contentView =
+ contentView =
+ {callControls}
; } - /* - if (!this.props.room) { - const client = MatrixClientPeg.get(); - const callRoom = client.getRoom(this.state.call.roomId); - - let caption = _t("Active call"); - if (this.state.isLocalOnHold) { - // we currently have no UI for holding / unholding a call (apart from slash - // commands) so we don't disintguish between when we've put the call on hold - // (ie. we'd show an unhold button) and when the other side has put us on hold - // (where obviously we would not show such a button). - caption = _t("Call Paused"); - } - - view = - - - -
-

{callRoom.name}

-

{ caption }

-
-
; - } else { - // For video calls, we currently ignore the call hold state altogether - // (the video will just go black) - - // if we're fullscreen, we don't want to set a maxHeight on the video element. - const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight; - view =
- - -
; - } - */ - - - /* - let hangup: React.ReactNode; - if (this.props.showHangup) { - hangup =
{ - dis.dispatch({ - action: 'hangup', - room_id: this.state.call.roomId, - }); - }} - />; - } - */ - const callTypeText = this.state.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call"); let myClassName; @@ -290,7 +358,9 @@ export default class CallView extends React.Component { myClassName = 'mx_CallView_large'; } else { header =
- + + +
{callRoom.name}
{callTypeText}