From 4b43929aa33992ddc9514ed344b7b3f660a19866 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 4 Jan 2021 20:01:43 +0000 Subject: [PATCH] Add in-call dialpad for DTMF sending Requires https://github.com/matrix-org/matrix-js-sdk/pull/1573 --- res/css/_components.scss | 1 + res/css/views/voip/_CallView.scss | 10 ++- res/css/views/voip/_DialPadContextMenu.scss | 47 +++++++++++++ res/img/voip/dialpad.svg | 17 +++++ src/components/structures/ContextMenu.tsx | 38 +++++++++- .../context_menus/DialpadContextMenu.tsx | 59 ++++++++++++++++ src/components/views/voip/CallView.tsx | 69 ++++++++++++++++--- src/components/views/voip/DialPad.tsx | 19 ++--- src/components/views/voip/DialPadModal.tsx | 3 +- 9 files changed, 242 insertions(+), 21 deletions(-) create mode 100644 res/css/views/voip/_DialPadContextMenu.scss create mode 100644 res/img/voip/dialpad.svg create mode 100644 src/components/views/context_menus/DialpadContextMenu.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 9041eef13f..a6e1f81583 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -237,5 +237,6 @@ @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_DialPad.scss"; +@import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadModal.scss"; @import "./views/voip/_VideoFeed.scss"; diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index a9b02ff5d8..7eb329594a 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -310,8 +310,14 @@ limitations under the License. } } -// Makes the alignment correct -.mx_CallView_callControls_nothing { +.mx_CallView_callControls_dialpad { + margin-right: auto; + &::before { + background-image: url('$(res)/img/voip/dialpad.svg'); + } +} + +.mx_CallView_callControls_button_dialpad_hidden { margin-right: auto; cursor: initial; } diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss new file mode 100644 index 0000000000..520f51cf93 --- /dev/null +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -0,0 +1,47 @@ +/* +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. +*/ + +.mx_DialPadContextMenu_header { + margin-top: 12px; + margin-left: 12px; + margin-right: 12px; +} + +.mx_DialPadContextMenu_title { + color: $muted-fg-color; + font-size: 12px; + font-weight: 600; +} + +.mx_DialPadContextMenu_dialled { + height: 1em; + font-size: 18px; + font-weight: 600; +} + +.mx_DialPadContextMenu_dialPad { + margin: 16px; +} + +.mx_DialPadContextMenu_horizSep { + position: relative; + &::before { + content: ''; + position: absolute; + width: 100%; + border-bottom: 1px solid $input-darker-bg-color; + } +} diff --git a/res/img/voip/dialpad.svg b/res/img/voip/dialpad.svg new file mode 100644 index 0000000000..79c9ba1612 --- /dev/null +++ b/res/img/voip/dialpad.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 190b231b74..aab7701f26 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -397,7 +397,8 @@ export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => { return {left, top, chevronOffset}; }; -// Placement method for to position context menu right-aligned and flowing to the left of elementRect +// Placement method for to position context menu right-aligned and flowing to the left of elementRect, +// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?) export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => { const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; @@ -416,6 +417,41 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None return menuOptions; }; +// Placement method for to position context menu right-aligned and flowing to the left of elementRect +// and always above elementRect +export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => { + const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; + + const buttonRight = elementRect.right + window.pageXOffset; + const buttonBottom = elementRect.bottom + window.pageYOffset; + const buttonTop = elementRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom + vPadding; + } else { + menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + } + + return menuOptions; +}; + +// Placement method for to position context menu right-aligned and flowing to the right of elementRect +// and always above elementRect +export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => { + const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; + + const buttonLeft = elementRect.left + window.pageXOffset; + const buttonTop = elementRect.top + window.pageYOffset; + // Align the left edge of the menu to the left edge of the button + menuOptions.left = buttonLeft; + // Align the menu vertically above the menu + menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + + return menuOptions; +}; + type ContextMenuTuple = [boolean, RefObject, () => void, () => void, (val: boolean) => void]; export const useContextMenu = (): ContextMenuTuple => { const button = useRef(null); diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx new file mode 100644 index 0000000000..e3aed0179b --- /dev/null +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -0,0 +1,59 @@ +/* +Copyright 2021 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'; +import { _t } from '../../../languageHandler'; +import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; +import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import Dialpad from '../voip/DialPad'; + +interface IProps extends IContextMenuProps { + call: MatrixCall; +} + +interface IState { + value: string; +} + +export default class DialpadContextMenu extends React.Component { + constructor(props) { + super(props); + + this.state = { + value: '', + } + } + + onDigitPress = (digit) => { + this.props.call.sendDtmfDigit(digit); + this.setState({value: this.state.value + digit}); + } + + render() { + return +
+
+ {_t("Dial pad")} +
+
{this.state.value}
+
+
+
+ +
+ ; + } +} diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 6748728278..6fbc396bee 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -27,9 +27,10 @@ import { CallEvent } from 'matrix-js-sdk/src/webrtc/call'; import classNames from 'classnames'; import AccessibleButton from '../elements/AccessibleButton'; import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard'; -import {aboveLeftOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu'; +import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} from '../../structures/ContextMenu'; import CallContextMenu from '../context_menus/CallContextMenu'; import { avatarUrlForMember } from '../../../Avatar'; +import DialpadContextMenu from '../context_menus/DialpadContextMenu'; interface IProps { // The call for us to display @@ -60,6 +61,7 @@ interface IState { callState: CallState, controlsVisible: boolean, showMoreMenu: boolean, + showDialpad: boolean, } function getFullScreenElement() { @@ -102,6 +104,7 @@ export default class CallView extends React.Component { private dispatcherRef: string; private contentRef = createRef(); private controlsHideTimer: number = null; + private dialpadButton = createRef(); private contextMenuButton = createRef(); constructor(props: IProps) { @@ -115,6 +118,7 @@ export default class CallView extends React.Component { callState: this.props.call.state, controlsVisible: true, showMoreMenu: false, + showDialpad: false, } this.updateCallListeners(null, this.props.call); @@ -226,7 +230,7 @@ export default class CallView extends React.Component { } private showControls() { - if (this.state.showMoreMenu) return; + if (this.state.showMoreMenu || this.state.showDialpad) return; if (!this.state.controlsVisible) { this.setState({ @@ -239,6 +243,29 @@ export default class CallView extends React.Component { this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); } + private onDialpadClick = () => { + if (!this.state.showDialpad) { + if (this.controlsHideTimer) { + clearTimeout(this.controlsHideTimer); + this.controlsHideTimer = null; + } + + this.setState({ + showDialpad: true, + controlsVisible: true, + }); + } else { + if (this.controlsHideTimer !== null) { + clearTimeout(this.controlsHideTimer); + } + this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); + + this.setState({ + showDialpad: false, + }); + } + } + private onMicMuteClick = () => { const newVal = !this.state.micMuted; @@ -265,6 +292,13 @@ export default class CallView extends React.Component { }); } + private closeDialpad = () => { + this.setState({ + showDialpad: false, + }); + this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY); + } + private closeContextMenu = () => { this.setState({ showMoreMenu: false, @@ -323,20 +357,29 @@ export default class CallView extends React.Component { CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId); } - private onSecondaryCallResumeClick = () => { - CallHandler.sharedInstance().setActiveCallRoomId(this.props.secondaryCall.roomId); - } - public render() { const client = MatrixClientPeg.get(); const callRoom = client.getRoom(this.props.call.roomId); const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null; + let dialPad; let contextMenu; + if (this.state.showDialpad) { + dialPad = ; + } + if (this.state.showMoreMenu) { contextMenu = { onClick={this.onVidMuteClick} /> : null; - // The 'more' button actions are only relevant in a connected call + // The dial pad & 'more' button actions are only relevant in a connected call // When not connected, we have to put something there to make the flexbox alignment correct + const dialpadButton = this.state.callState === CallState.Connected ? :
; + const contextMenuButton = this.state.callState === CallState.Connected ? { // in the near future, the dial pad button will go on the left. For now, it's the nothing button // because something needs to have margin-right: auto to make the alignment correct. const callControls =
-
+ {dialpadButton} { return
{header} {contentView} + {dialPad} {contextMenu}
; } diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx index 4ab7241f53..da88f49adf 100644 --- a/src/components/views/voip/DialPad.tsx +++ b/src/components/views/voip/DialPad.tsx @@ -54,8 +54,9 @@ class DialPadButton extends React.PureComponent { interface IProps { onDigitPress: (string) => void; - onDeletePress: (string) => void; - onDialPress: (string) => void; + hasDialAndDelete: boolean; + onDeletePress?: (string) => void; + onDialPress?: (string) => void; } export default class Dialpad extends React.PureComponent { @@ -68,12 +69,14 @@ export default class Dialpad extends React.PureComponent { />); } - buttonNodes.push(); - buttonNodes.push(); + if (this.props.hasDialAndDelete) { + buttonNodes.push(); + buttonNodes.push(); + } return
{buttonNodes} diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index 9f7e4140c9..74b39e0721 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -101,7 +101,8 @@ export default class DialpadModal extends React.PureComponent {
-