diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 2d5359c0eb..5bf2aee3ae 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -19,57 +19,6 @@ limitations under the License. min-height: 50px; } -/* position the indicator in the same place horizontally as .mx_EventTile_avatar. */ -.mx_RoomStatusBar_indicator { - padding-left: 17px; - padding-right: 12px; - margin-left: -73px; - margin-top: 15px; - float: left; - width: 24px; - text-align: center; -} - -.mx_RoomStatusBar_callBar { - height: 50px; - line-height: $font-50px; -} - -.mx_RoomStatusBar_placeholderIndicator span { - color: $primary-fg-color; - opacity: 0.5; - position: relative; - top: -4px; - /* - animation-duration: 1s; - animation-name: bounce; - animation-direction: alternate; - animation-iteration-count: infinite; - */ -} - -.mx_RoomStatusBar_placeholderIndicator span:nth-child(1) { - animation-delay: 0.3s; -} -.mx_RoomStatusBar_placeholderIndicator span:nth-child(2) { - animation-delay: 0.6s; -} -.mx_RoomStatusBar_placeholderIndicator span:nth-child(3) { - animation-delay: 0.9s; -} - -@keyframes bounce { - from { - opacity: 0.5; - top: 0; - } - - to { - opacity: 0.2; - top: -3px; - } -} - .mx_RoomStatusBar_typingIndicatorAvatars { width: 52px; margin-top: -1px; @@ -162,11 +111,6 @@ limitations under the License. margin-top: 10px; } - .mx_RoomStatusBar_callBar { - height: 40px; - line-height: $font-40px; - } - .mx_RoomStatusBar_typingBar { height: 40px; line-height: $font-40px; diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index 1a1e14e7ac..cbddd97e18 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -41,7 +41,7 @@ limitations under the License. .mx_BaseAvatar_image { object-fit: cover; - border-radius: 40px; + border-radius: 125px; vertical-align: top; background-color: $avatar-bg-color; } diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 2aeaaa87dc..2b87181b1e 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -15,87 +15,196 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_CallView_voice { - background-color: $accent-color; - color: $accent-fg-color; - cursor: pointer; - padding: 6px; - font-weight: bold; +.mx_CallView { + border-radius: 10px; + background-color: $input-lighter-bg-color; + padding-left: 8px; + padding-right: 8px; + // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place + pointer-events: initial; +} - border-radius: 8px; - min-width: 200px; +.mx_CallView_large { + padding-bottom: 10px; - display: flex; - align-items: center; - - img { - margin: 4px; - margin-right: 10px; - } - - > div { - display: flex; - flex-direction: column; - // Hacky vertical align - padding-top: 3px; - } - - > div > p, - > div > h1 { - padding: 0; - margin: 0; - font-size: $font-13px; - line-height: $font-15px; - } - - > div > p { - font-weight: bold; - } - - > * { - flex-grow: 0; - flex-shrink: 0; + .mx_CallView_voice { + height: 360px; } } -.mx_CallView_hangup { - position: absolute; +.mx_CallView_pip { + width: 320px; - 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_voice { + height: 180px; } } +.mx_CallView_voice { + position: relative; + display: flex; + align-items: center; + justify-content: center; + background-color: $inverted-bg-color; +} + .mx_CallView_video { width: 100%; position: relative; z-index: 30; } +.mx_CallView_header { + height: 44px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: left; + + .mx_BaseAvatar { + margin-right: 12px; + } +} + +.mx_CallView_header_callType { + font-weight: bold; + vertical-align: middle; +} + +.mx_CallView_header_controls { + margin-left: auto; +} + +.mx_CallView_header_button { + display: inline-block; + vertical-align: middle; + cursor: pointer; + + &::before { + content: ''; + display: inline-block; + height: 20px; + width: 20px; + vertical-align: middle; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } +} + +.mx_CallView_header_button_fullscreen { + &::before { + mask-image: url('$(res)/img/element-icons/call/fullscreen.svg'); + } +} + +.mx_CallView_header_button_expand { + &::before { + mask-image: url('$(res)/img/element-icons/call/expand.svg'); + } +} + +.mx_CallView_header_roomName { + font-weight: bold; + font-size: 12px; + line-height: initial; +} + +.mx_CallView_header_callTypeSmall { + font-size: 12px; + color: $secondary-fg-color; + line-height: initial; +} + +.mx_CallView_header_phoneIcon { + display: inline-block; + margin-right: 6px; + height: 16px; + width: 16px; + vertical-align: middle; + + &::before { + content: ''; + display: inline-block; + vertical-align: top; + + height: 16px; + width: 16px; + background-color: $warning-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + 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; // opacity 0 can cause a re-layout + pointer-events: none; +} + +.mx_CallView_callControls_button { + cursor: pointer; + margin-left: 8px; + margin-right: 8px; + + + &::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/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_hangup { + &::before { + background-image: url('$(res)/img/voip/hangup.svg'); + } +} + +.mx_CallView_callControls_button_invisible { + visibility: hidden; + pointer-events: none; + position: absolute; +} diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index e5e3587dac..931410dba3 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VideoFeed video { - width: 100%; -} - .mx_VideoFeed_remote { width: 100%; background-color: #000; @@ -28,16 +24,12 @@ limitations under the License. width: 25%; height: 25%; position: absolute; - left: 10px; - bottom: 10px; + right: 10px; + top: 10px; z-index: 100; + border-radius: 4px; } -.mx_VideoFeed_local video { - width: auto; - height: 100%; -} - -.mx_VideoFeed_mirror video { +.mx_VideoFeed_mirror { transform: scale(-1, 1); } diff --git a/res/img/element-icons/call/expand.svg b/res/img/element-icons/call/expand.svg new file mode 100644 index 0000000000..91ef4d8a76 --- /dev/null +++ b/res/img/element-icons/call/expand.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/call/video-muted.svg b/res/img/element-icons/call/video-muted.svg deleted file mode 100644 index d2aea71d11..0000000000 --- a/res/img/element-icons/call/video-muted.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/element-icons/call/voice-muted.svg b/res/img/element-icons/call/voice-muted.svg deleted file mode 100644 index 32abafb04a..0000000000 --- a/res/img/element-icons/call/voice-muted.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/element-icons/call/voice-unmuted.svg b/res/img/element-icons/call/voice-unmuted.svg deleted file mode 100644 index e664080217..0000000000 --- a/res/img/element-icons/call/voice-unmuted.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/room/in-call.svg b/res/img/element-icons/room/in-call.svg deleted file mode 100644 index 0e574faa84..0000000000 --- a/res/img/element-icons/room/in-call.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - 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/hangup.svg b/res/img/voip/hangup.svg new file mode 100644 index 0000000000..dfb20bd519 --- /dev/null +++ b/res/img/voip/hangup.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/mic-off.svg b/res/img/voip/mic-off.svg new file mode 100644 index 0000000000..6409f1fd07 --- /dev/null +++ b/res/img/voip/mic-off.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/mic-on.svg b/res/img/voip/mic-on.svg new file mode 100644 index 0000000000..3493b3c581 --- /dev/null +++ b/res/img/voip/mic-on.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/vid-off.svg b/res/img/voip/vid-off.svg new file mode 100644 index 0000000000..199d97ab97 --- /dev/null +++ b/res/img/voip/vid-off.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/vid-on.svg b/res/img/voip/vid-on.svg new file mode 100644 index 0000000000..d8146d01d3 --- /dev/null +++ b/res/img/voip/vid-on.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index e6d2985073..c1c4ad6292 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -18,13 +18,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import Matrix from 'matrix-js-sdk'; import { _t, _td } from '../../languageHandler'; -import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; import dis from '../../dispatcher/dispatcher'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {Action} from "../../dispatcher/actions"; -import { CallState, CallType } from 'matrix-js-sdk/lib/webrtc/call'; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -42,13 +40,6 @@ export default class RoomStatusBar extends React.Component { // the room this statusbar is representing. room: PropTypes.object.isRequired, - // The active call in the room, if any (means we show the call bar - // along with the status of the call) - callState: PropTypes.string, - - // The type of the call in progress, or null if no call is in progress - callType: PropTypes.string, - // true if the room is being peeked at. This affects components that shouldn't // logically be shown when peeking, such as a prompt to invite people to a room. isPeeking: PropTypes.bool, @@ -115,12 +106,6 @@ export default class RoomStatusBar extends React.Component { }); }; - _showCallBar() { - return (this.props.callState && - (this.props.callState !== CallState.Ended && this.props.callState !== CallState.Ringing) - ); - } - _onResendAllClick = () => { Resend.resendUnsentEvents(this.props.room); dis.fire(Action.FocusComposer); @@ -152,7 +137,7 @@ export default class RoomStatusBar extends React.Component { // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. _getSize() { - if (this._shouldShowConnectionError() || this._showCallBar()) { + if (this._shouldShowConnectionError()) { return STATUS_BAR_EXPANDED; } else if (this.state.unsentMessages.length > 0) { return STATUS_BAR_EXPANDED_LARGE; @@ -160,22 +145,6 @@ export default class RoomStatusBar extends React.Component { return STATUS_BAR_HIDDEN; } - // return suitable content for the image on the left of the status bar. - _getIndicator() { - if (this._showCallBar()) { - const TintableSvg = sdk.getComponent("elements.TintableSvg"); - return ( - - ); - } - - if (this._shouldShowConnectionError()) { - return null; - } - - return null; - } - _shouldShowConnectionError() { // no conn bar trumps the "some not sent" msg since you can't resend without // a connection! @@ -266,25 +235,6 @@ export default class RoomStatusBar extends React.Component { ; } - _getCallStatusText() { - switch (this.props.callState) { - case CallState.CreateOffer: - case CallState.InviteSent: - return _t('Calling...'); - case CallState.Connecting: - case CallState.CreateAnswer: - return _t('Call connecting...'); - case CallState.Connected: - return _t('Active call'); - case CallState.WaitLocalMedia: - if (this.props.callType === CallType.Video) { - return _t('Starting camera...'); - } else { - return _t('Starting microphone...'); - } - } - } - // return suitable content for the main (text) part of the status bar. _getContent() { if (this._shouldShowConnectionError()) { @@ -307,26 +257,14 @@ export default class RoomStatusBar extends React.Component { return this._getUnsentMessageContent(); } - if (this._showCallBar()) { - return ( -
- { this._getCallStatusText() } -
- ); - } - return null; } render() { const content = this._getContent(); - const indicator = this._getIndicator(); return (
-
- { indicator } -
{ content }
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index a9647e6ef9..f4f7c6ceec 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -41,7 +41,7 @@ import rateLimitedFunc from '../../ratelimitedfunc'; import * as ObjectUtils from '../../ObjectUtils'; import * as Rooms from '../../Rooms'; import eventSearch, {searchPagination} from '../../Searching'; -import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard'; +import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard'; import MainSplit from './MainSplit'; import RightPanel from './RightPanel'; import RoomViewStore from '../../stores/RoomViewStore'; @@ -67,10 +67,9 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; -import TintableSvg from "../views/elements/TintableSvg"; import {XOR} from "../../@types/common"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; -import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import WidgetStore from "../../stores/WidgetStore"; import {UPDATE_EVENT} from "../../stores/AsyncStore"; import Notifier from "../../Notifier"; @@ -507,8 +506,6 @@ export default class RoomView extends React.Component { this.props.resizeNotifier.on("middlePanelResized", this.onResize); } this.onResize(); - - document.addEventListener("keydown", this.onNativeKeyDown); } shouldComponentUpdate(nextProps, nextState) { @@ -591,8 +588,6 @@ export default class RoomView extends React.Component { this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize); } - document.removeEventListener("keydown", this.onNativeKeyDown); - // Remove RoomStore listener if (this.roomStoreToken) { this.roomStoreToken.remove(); @@ -641,33 +636,6 @@ export default class RoomView extends React.Component { } }; - // we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire - private onNativeKeyDown = ev => { - let handled = false; - const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); - - switch (ev.key) { - case Key.D: - if (ctrlCmdOnly) { - this.onMuteAudioClick(); - handled = true; - } - break; - - case Key.E: - if (ctrlCmdOnly) { - this.onMuteVideoClick(); - handled = true; - } - break; - } - - if (handled) { - ev.stopPropagation(); - ev.preventDefault(); - } - }; - private onReactKeyDown = ev => { let handled = false; @@ -1754,8 +1722,6 @@ export default class RoomView extends React.Component { isStatusAreaExpanded = this.state.statusBarVisible; statusBar = { }; } - if (activeCall) { - let zoomButton; let videoMuteButton; - - if (activeCall.type === CallType.Video) { - zoomButton = ( -
- -
- ); - - videoMuteButton = -
- -
; - } - const voiceMuteButton = -
- -
; - - // wrap the existing status bar into a 'callStatusBar' which adds more knobs. - statusBar = -
- { voiceMuteButton } - { videoMuteButton } - { zoomButton } - { statusBar } -
; - } - // if we have search results, we keep the messagepanel (so that it preserves its // scroll state), but hide it. let searchResultsPanel; diff --git a/src/components/views/avatars/PulsedAvatar.tsx b/src/components/views/avatars/PulsedAvatar.tsx deleted file mode 100644 index b4e876b9f6..0000000000 --- a/src/components/views/avatars/PulsedAvatar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; - -interface IProps { -} - -const PulsedAvatar: React.FC = (props) => { - return
- {props.children} -
; -}; - -export default PulsedAvatar; diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 3d9235792b..8e1b0dd963 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -26,6 +26,15 @@ import PersistentApp from "../elements/PersistentApp"; import SettingsStore from "../../../settings/SettingsStore"; import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +const SHOW_CALL_IN_STATES = [ + CallState.Connected, + CallState.InviteSent, + CallState.Connecting, + CallState.CreateAnswer, + CallState.CreateOffer, + CallState.WaitLocalMedia, +]; + interface IProps { } @@ -94,14 +103,13 @@ export default class CallPreview extends React.Component { const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId); const showCall = ( this.state.activeCall && - this.state.activeCall.state === CallState.Connected && + SHOW_CALL_IN_STATES.includes(this.state.activeCall.state) && !callForRoom ); if (showCall) { return ( diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 653a72cca0..db6d2b7ae0 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -21,12 +21,13 @@ import dis from '../../../dispatcher/dispatcher'; import CallHandler from '../../../CallHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -import AccessibleButton from '../elements/AccessibleButton'; import VideoFeed, { VideoFeedType } from "./VideoFeed"; import RoomAvatar from "../avatars/RoomAvatar"; -import PulsedAvatar from '../avatars/PulsedAvatar'; 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'; +import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard'; interface IProps { // js-sdk room object. If set, we will only show calls for the given @@ -43,9 +44,6 @@ interface IProps { // in a way that is likely to cause a resize. onResize?: any; - // classname applied to view, - className?: string; - // Whether to show the hang up icon:W showHangup?: boolean; } @@ -53,6 +51,10 @@ interface IProps { interface IState { call: MatrixCall; isLocalOnHold: boolean, + micMuted: boolean, + vidMuted: boolean, + callState: CallState, + controlsVisible: boolean, } function getFullScreenElement() { @@ -83,10 +85,15 @@ function exitFullscreen() { if (exitMethod) exitMethod.call(document); } +const CONTROLS_HIDE_DELAY = 1000; +// 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 HEADER_HEIGHT = 44; + export default class CallView extends React.Component { private dispatcherRef: string; - private container = createRef(); - + private contentRef = createRef(); + private controlsHideTimer: number = null; constructor(props: IProps) { super(props); @@ -94,6 +101,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); @@ -101,9 +112,11 @@ export default class CallView extends React.Component { public componentDidMount() { this.dispatcherRef = dis.register(this.onAction); + document.addEventListener('keydown', this.onNativeKeyDown); } public componentWillUnmount() { + document.removeEventListener("keydown", this.onNativeKeyDown); this.updateCallListeners(this.state.call, null); dis.unregister(this.dispatcherRef); } @@ -111,11 +124,11 @@ export default class CallView extends React.Component { private onAction = (payload) => { switch (payload.action) { case 'video_fullscreen': { - if (!this.container.current) { + if (!this.contentRef.current) { return; } if (payload.fullscreen) { - requestFullscreen(this.container.current); + requestFullscreen(this.contentRef.current); } else if (getFullScreenElement()) { exitFullscreen(); } @@ -125,9 +138,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()) { @@ -144,11 +169,6 @@ export default class CallView extends React.Component { if (this.props.room) { const roomId = this.props.room.roomId; call = CallHandler.sharedInstance().getCallForRoom(roomId); - - // We don't currently show voice calls in this view when in the room: - // they're represented in the room status bar at the bottom instead - // (but this will all change with the new designs) - if (call && call.type == CallType.Voice) call = null; } else { call = CallHandler.sharedInstance().getAnyActiveCall(); // Ignore calls if we can't get the room associated with them. @@ -160,7 +180,7 @@ export default class CallView extends React.Component { } } - if (call && call.state == CallState.Ended) return null; + if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null; return call; } @@ -177,67 +197,240 @@ export default class CallView extends React.Component { }); }; - public render() { - let view: React.ReactNode; + private onFullscreenClick = () => { + dis.dispatch({ + action: 'video_fullscreen', + fullscreen: true, + }); + }; - if (this.state.call) { - if (this.state.call.type === "voice") { - const client = MatrixClientPeg.get(); - const callRoom = client.getRoom(this.state.call.roomId); + private onExpandClick = () => { + dis.dispatch({ + action: 'view_room', + room_id: 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"); + private onControlsHideTimer = () => { + this.controlsHideTimer = null; + this.setState({ + controlsVisible: false, + }); + } + + private onMouseMove = () => { + this.showControls(); + } + + private showControls() { + 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 onMicMuteClick = () => { + if (!this.state.call) return; + + const newVal = !this.state.micMuted; + + this.state.call.setMicrophoneMuted(newVal); + this.setState({micMuted: newVal}); + } + + private onVidMuteClick = () => { + if (!this.state.call) return; + + const newVal = !this.state.vidMuted; + + this.state.call.setLocalVideoMuted(newVal); + this.setState({vidMuted: newVal}); + } + + // 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 + // CallHandler would probably be a better place for this + private onNativeKeyDown = ev => { + let handled = false; + const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); + + switch (ev.key) { + case Key.D: + if (ctrlCmdOnly) { + this.onMicMuteClick(); + // show the controls to give feedback + this.showControls(); + handled = true; } + break; - 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 =
- - -
; - } + case Key.E: + if (ctrlCmdOnly) { + this.onVidMuteClick(); + // show the controls to give feedback + this.showControls(); + handled = true; + } + break; } - let hangup: React.ReactNode; - if (this.props.showHangup) { - hangup =
{ - dis.dispatch({ - action: 'hangup', - room_id: this.state.call.roomId, - }); - }} + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }; + + private 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); + + 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, + }); + + const vidMuteButton = this.state.call.type === CallType.Video ?
: null; + + callControls =
+
+
{ + dis.dispatch({ + action: 'hangup', + room_id: this.state.call.roomId, + }); + }} + /> + {vidMuteButton} +
+
+
; + } + + // The 'content' for the call, ie. the videos for a video call and profile picture + // for voice calls (fills the bg) + let contentView: React.ReactNode; + + if (this.state.call.type === CallType.Video) { + // if we're fullscreen, we don't want to set a maxHeight on the video element. + const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight - HEADER_HEIGHT; + contentView =
+ + + {callControls} +
; + } else { + const avatarSize = this.props.room ? 200 : 75; + contentView =
+ + {callControls} +
; + } + + const callTypeText = this.state.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call"); + let myClassName; + + let fullScreenButton; + if (this.state.call.type === CallType.Video && this.props.room) { + fullScreenButton =
; } - return
- {view} - {hangup} + let expandButton; + if (!this.props.room) { + expandButton =
; + } + + const headerControls =
+ {fullScreenButton} + {expandButton} +
; + + let header: React.ReactNode; + if (this.props.room) { + header =
+
+ {callTypeText} + {headerControls} +
; + myClassName = 'mx_CallView_large'; + } else { + header =
+ + + +
+
{callRoom.name}
+
{callTypeText}
+
+ {headerControls} +
; + myClassName = 'mx_CallView_pip'; + } + + return
+ {header} + {contentView}
; } } diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index 355dff9ff6..0403a9eb75 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -22,7 +22,6 @@ import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import { ActionPayload } from '../../../dispatcher/payloads'; import CallHandler from '../../../CallHandler'; -import PulsedAvatar from '../avatars/PulsedAvatar'; import RoomAvatar from '../avatars/RoomAvatar'; import FormButton from '../elements/FormButton'; import { CallState } from 'matrix-js-sdk/lib/webrtc/call'; @@ -108,13 +107,11 @@ export default class IncomingCallBox extends React.Component { return
- - - +

{caller}

{incomingCallText}

diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 9dba9fa9c8..5fb71a6d69 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -73,8 +73,6 @@ export default class VideoFeed extends React.Component { let videoStyle = {}; if (this.props.maxHeight) videoStyle = { maxHeight: this.props.maxHeight }; - return
- -
; + return