diff --git a/res/css/_components.scss b/res/css/_components.scss index af161c92c6..e1e6b607df 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -270,6 +270,7 @@ @import "./views/toasts/_IncomingCallToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voip/CallView/_CallViewButtons.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallPreview.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/views/voip/CallView/_CallViewButtons.scss b/res/css/views/voip/CallView/_CallViewButtons.scss new file mode 100644 index 0000000000..8e343f0ff3 --- /dev/null +++ b/res/css/views/voip/CallView/_CallViewButtons.scss @@ -0,0 +1,102 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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. +*/ + +.mx_CallViewButtons { + position: absolute; + display: flex; + justify-content: center; + bottom: 5px; + opacity: 1; + transition: opacity 0.5s; + z-index: 200; // To be above _all_ feeds + + &.mx_CallViewButtons_hidden { + opacity: 0.001; // opacity 0 can cause a re-layout + pointer-events: none; + } + + .mx_CallViewButtons_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_CallViewButtons_dialpad::before { + background-image: url('$(res)/img/voip/dialpad.svg'); + } + + &.mx_CallViewButtons_button_micOn::before { + background-image: url('$(res)/img/voip/mic-on.svg'); + } + + &.mx_CallViewButtons_button_micOff::before { + background-image: url('$(res)/img/voip/mic-off.svg'); + } + + &.mx_CallViewButtons_button_vidOn::before { + background-image: url('$(res)/img/voip/vid-on.svg'); + } + + &.mx_CallViewButtons_button_vidOff::before { + background-image: url('$(res)/img/voip/vid-off.svg'); + } + + &.mx_CallViewButtons_button_screensharingOn::before { + background-image: url('$(res)/img/voip/screensharing-on.svg'); + } + + &.mx_CallViewButtons_button_screensharingOff::before { + background-image: url('$(res)/img/voip/screensharing-off.svg'); + } + + &.mx_CallViewButtons_button_sidebarOn::before { + background-image: url('$(res)/img/voip/sidebar-on.svg'); + } + + &.mx_CallViewButtons_button_sidebarOff::before { + background-image: url('$(res)/img/voip/sidebar-off.svg'); + } + + &.mx_CallViewButtons_button_hangup::before { + background-image: url('$(res)/img/voip/hangup.svg'); + } + + &.mx_CallViewButtons_button_more::before { + background-image: url('$(res)/img/voip/more.svg'); + } + + &.mx_CallViewButtons_button_invisible { + visibility: hidden; + pointer-events: none; + position: absolute; + } + } +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7752edddfa..498dd8e096 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -47,11 +47,11 @@ limitations under the License. height: 180px; } - .mx_CallView_callControls { + .mx_CallViewButtons { bottom: 0px; } - .mx_CallView_callControls_button { + .mx_CallViewButtons_button { &::before { width: 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 { opacity: 1; @@ -232,94 +218,3 @@ limitations under the License. opacity: 0.001; // opacity 0 can cause a re-layout 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; -} diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 76e6a43ca5..fbb69a51ec 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -1,6 +1,6 @@ /* 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 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 AccessibleButton from '../elements/AccessibleButton'; import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../../Keyboard'; -import { - alwaysAboveLeftOf, - alwaysAboveRightOf, - ChevronFace, - ContextMenuTooltipButton, -} from '../../structures/ContextMenu'; -import CallContextMenu from '../context_menus/CallContextMenu'; import { avatarUrlForMember } from '../../../Avatar'; -import DialpadContextMenu from '../context_menus/DialpadContextMenu'; import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker"; @@ -43,8 +35,7 @@ import Modal from '../../../Modal'; import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes'; import CallViewSidebar from './CallViewSidebar'; import CallViewHeader from './CallView/CallViewHeader'; -import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import { Alignment } from "../elements/Tooltip"; +import CallViewButtons from "./CallView/CallViewButtons"; interface IProps { // The call for us to display @@ -83,8 +74,6 @@ interface IState { sidebarShown: boolean; } -const tooltipYOffset = -24; - function getFullScreenElement() { return ( document.fullscreenElement || @@ -113,18 +102,11 @@ function exitFullscreen() { 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") export default class CallView extends React.Component { private dispatcherRef: string; private contentRef = createRef(); - private controlsHideTimer: number = null; - private dialpadButton = createRef(); - private contextMenuButton = createRef(); + private buttonsRef = createRef(); constructor(props: IProps) { super(props); @@ -241,16 +223,8 @@ export default class CallView extends React.Component { }); }; - private onControlsHideTimer = () => { - if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return; - this.controlsHideTimer = null; - this.setState({ - controlsVisible: false, - }); - }; - private onMouseMove = () => { - this.showControls(); + this.buttonsRef.current?.showControls(); }; private getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } { @@ -276,29 +250,6 @@ export default class CallView extends React.Component { 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 => { const newVal = !this.state.micMuted; @@ -329,19 +280,6 @@ export default class CallView extends React.Component { }); }; - 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 // Note that this assumes we always have a CallView on screen at any given time // CallHandler would probably be a better place for this @@ -354,7 +292,7 @@ export default class CallView extends React.Component { if (ctrlCmdOnly) { this.onMicMuteClick(); // show the controls to give feedback - this.showControls(); + this.buttonsRef.current?.showControls(); handled = true; } break; @@ -363,7 +301,7 @@ export default class CallView extends React.Component { if (ctrlCmdOnly) { this.onVidMuteClick(); // show the controls to give feedback - this.showControls(); + this.buttonsRef.current?.showControls(); handled = true; } break; @@ -375,15 +313,6 @@ export default class CallView extends React.Component { } }; - private onCallControlsMouseEnter = (): void => { - this.setState({ hoveringControls: true }); - this.showControls(); - }; - - private onCallControlsMouseLeave = (): void => { - this.setState({ hoveringControls: false }); - }; - private onCallResumeClick = (): void => { const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId); @@ -402,206 +331,60 @@ export default class CallView extends React.Component { }; private onToggleSidebar = (): void => { - this.setState({ - sidebarShown: !this.state.sidebarShown, - }); + this.setState({ sidebarShown: !this.state.sidebarShown }); }; 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 - let vidMuteButton; - if (this.props.call.type === CallType.Video) { - vidMuteButton = ( - - ); - } - + const vidMuteButtonShown = this.props.call.type === CallType.Video; // Screensharing is possible, if we can send a second stream and // identify it using SDPStreamMetadata or if we can replace the already // existing usermedia track by a screensharing track. We also need to be // connected to know the state of the other side - let screensharingButton; - if ( + const screensharingButtonShown = ( (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) && this.props.call.state === CallState.Connected - ) { - screensharingButton = ( - - ); - } - + ); // 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 the button too - let sidebarButton; - if ( - !this.props.pipMode && - ( - this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare || - this.props.call.isScreensharing() - ) - ) { - sidebarButton = ( - - ); - } - + const sidebarButtonShown = ( + this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare || + this.props.call.isScreensharing() + ); // The dial pad & 'more' button actions are only relevant in a connected call - let contextMenuButton; - if (this.state.callState === CallState.Connected) { - contextMenuButton = ( - - ); - } - let dialpadButton; - if (this.state.callState === CallState.Connected && this.props.call.opponentSupportsDTMF()) { - dialpadButton = ( - - ); - } - - let dialPad; - if (this.state.showDialpad) { - dialPad = ; - } - - let contextMenu; - if (this.state.showMoreMenu) { - contextMenu = ; - } + const contextMenuButtonShown = this.state.callState === CallState.Connected; + const dialpadButtonShown = ( + this.state.callState === CallState.Connected && + this.props.call.opponentSupportsDTMF() + ); return ( -
- { dialPad } - { contextMenu } - { dialpadButton } - - { vidMuteButton } -
-
- { screensharingButton } - { sidebarButton } - { contextMenuButton } - -
+ ); } diff --git a/src/components/views/voip/CallView/CallViewButtons.tsx b/src/components/views/voip/CallView/CallViewButtons.tsx new file mode 100644 index 0000000000..8c48bd767d --- /dev/null +++ b/src/components/views/voip/CallView/CallViewButtons.tsx @@ -0,0 +1,315 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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, { createRef } from "react"; +import classNames from "classnames"; +import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton"; +import CallContextMenu from "../../context_menus/CallContextMenu"; +import DialpadContextMenu from "../../context_menus/DialpadContextMenu"; +import AccessibleButton from "../../elements/AccessibleButton"; +import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { Alignment } from "../../elements/Tooltip"; +import { + alwaysAboveLeftOf, + alwaysAboveRightOf, + ChevronFace, + ContextMenuTooltipButton, +} from '../../../structures/ContextMenu'; +import { _t } from "../../../../languageHandler"; + +// 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) + +const TOOLTIP_Y_OFFSET = -24; + +const CONTROLS_HIDE_DELAY = 2000; + +interface IProps { + call: MatrixCall; + pipMode: boolean; + handlers: { + onHangupClick: () => void; + onScreenshareClick: () => void; + onToggleSidebarClick: () => void; + onMicMuteClick: () => void; + onVidMuteClick: () => void; + }; + buttonsState: { + micMuted: boolean; + vidMuted: boolean; + sidebarShown: boolean; + screensharing: boolean; + }; + buttonsVisibility: { + screensharing: boolean; + vidMute: boolean; + sidebar: boolean; + dialpad: boolean; + contextMenu: boolean; + }; +} + +interface IState { + visible: boolean; + showDialpad: boolean; + hoveringControls: boolean; + showMoreMenu: boolean; +} + +export default class CallViewButtons extends React.Component { + private dialpadButton = createRef(); + private contextMenuButton = createRef(); + private controlsHideTimer: number = null; + + constructor(props: IProps) { + super(props); + + this.state = { + showDialpad: false, + hoveringControls: false, + showMoreMenu: false, + visible: true, + }; + } + + public componentDidMount(): void { + this.showControls(); + } + + public showControls(): void { + if (this.state.showMoreMenu || this.state.showDialpad) return; + + if (!this.state.visible) { + this.setState({ + visible: true, + }); + } + if (this.controlsHideTimer !== null) { + clearTimeout(this.controlsHideTimer); + } + this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); + } + + private onControlsHideTimer = (): void => { + if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return; + this.controlsHideTimer = null; + this.setState({ visible: false }); + }; + + private onMouseEnter = (): void => { + this.setState({ hoveringControls: true }); + }; + + private onMouseLeave = (): void => { + this.setState({ hoveringControls: false }); + }; + + private onDialpadClick = (): void => { + if (!this.state.showDialpad) { + this.setState({ showDialpad: true }); + this.showControls(); + } else { + this.setState({ showDialpad: false }); + } + }; + + private onMoreClick = (): void => { + this.setState({ showMoreMenu: true }); + this.showControls(); + }; + + private closeDialpad = (): void => { + this.setState({ showDialpad: false }); + }; + + private closeContextMenu = (): void => { + this.setState({ showMoreMenu: false }); + }; + + public render(): JSX.Element { + const micClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_micOn: !this.props.buttonsState.micMuted, + mx_CallViewButtons_button_micOff: this.props.buttonsState.micMuted, + }); + + const vidClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_vidOn: !this.props.buttonsState.vidMuted, + mx_CallViewButtons_button_vidOff: this.props.buttonsState.vidMuted, + }); + + const screensharingClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_screensharingOn: this.props.buttonsState.screensharing, + mx_CallViewButtons_button_screensharingOff: !this.props.buttonsState.screensharing, + }); + + const sidebarButtonClasses = classNames("mx_CallViewButtons_button", { + mx_CallViewButtons_button_sidebarOn: this.props.buttonsState.sidebarShown, + mx_CallViewButtons_button_sidebarOff: !this.props.buttonsState.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_CallViewButtons_button", "mx_CallViewButtons_button_invisible", { + mx_CallViewButtons_button_micOn: this.props.buttonsState.micMuted, + mx_CallViewButtons_button_micOff: !this.props.buttonsState.micMuted, + }); + + const vidCacheClasses = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_button_invisible", { + mx_CallViewButtons_button_vidOn: this.props.buttonsState.micMuted, + mx_CallViewButtons_button_vidOff: !this.props.buttonsState.micMuted, + }); + + const callControlsClasses = classNames("mx_CallViewButtons", { + mx_CallViewButtons_hidden: !this.state.visible, + }); + + let vidMuteButton; + if (this.props.buttonsVisibility.vidMute) { + vidMuteButton = ( + + ); + } + + let screensharingButton; + if (this.props.buttonsVisibility.screensharing) { + screensharingButton = ( + + ); + } + + let sidebarButton; + if (this.props.buttonsVisibility.sidebar) { + sidebarButton = ( + + ); + } + + let contextMenuButton; + if (this.props.buttonsVisibility.contextMenu) { + contextMenuButton = ( + + ); + } + let dialpadButton; + if (this.props.buttonsVisibility.dialpad) { + dialpadButton = ( + + ); + } + + let dialPad; + if (this.state.showDialpad) { + dialPad = ; + } + + let contextMenu; + if (this.state.showMoreMenu) { + contextMenu = ; + } + + return ( +
+ { dialPad } + { contextMenu } + { dialpadButton } + + { vidMuteButton } +
+
+ { screensharingButton } + { sidebarButton } + { contextMenuButton } + +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f19de74685..826fe5ff88 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -905,6 +905,16 @@ "sends snowfall": "sends snowfall", "Sends the given message with a space themed effect": "Sends the given message with a space themed effect", "sends space invaders": "sends space invaders", + "unknown person": "unknown person", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", + "You held the call Switch": "You held the call Switch", + "You held the call Resume": "You held the call Resume", + "%(peerName)s held the call": "%(peerName)s held the call", + "Connecting": "Connecting", + "You are presenting": "You are presenting", + "%(sharerName)s is presenting": "%(sharerName)s is presenting", + "Your camera is turned off": "Your camera is turned off", + "Your camera is still enabled": "Your camera is still enabled", "Start the camera": "Start the camera", "Stop the camera": "Stop the camera", "Stop sharing your screen": "Stop sharing your screen", @@ -916,16 +926,6 @@ "Unmute the microphone": "Unmute the microphone", "Mute the microphone": "Mute the microphone", "Hangup": "Hangup", - "unknown person": "unknown person", - "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", - "You held the call Switch": "You held the call Switch", - "You held the call Resume": "You held the call Resume", - "%(peerName)s held the call": "%(peerName)s held the call", - "Connecting": "Connecting", - "You are presenting": "You are presenting", - "%(sharerName)s is presenting": "%(sharerName)s is presenting", - "Your camera is turned off": "Your camera is turned off", - "Your camera is still enabled": "Your camera is still enabled", "Video Call": "Video Call", "Voice Call": "Voice Call", "Fill Screen": "Fill Screen",