From 2cc6bcec29be908ac45bf181272e2542aadd73f9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 18 Jun 2021 16:36:37 +0100 Subject: [PATCH] Much theming & visual of transfer window dial pad --- res/css/structures/_TabbedView.scss | 107 +++++++++++--- res/css/views/dialogs/_InviteDialog.scss | 54 ++++++- res/css/views/voip/_DialPad.scss | 2 + src/CallHandler.tsx | 2 +- src/components/structures/TabbedView.tsx | 21 ++- src/components/views/dialogs/InviteDialog.tsx | 132 +++++++++++------- src/i18n/strings/en_EN.json | 3 +- 7 files changed, 244 insertions(+), 77 deletions(-) diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 39a8ebed32e..cd0b95e632e 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -1,6 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd +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. @@ -20,7 +21,6 @@ limitations under the License. padding: 0 0 0 16px; display: flex; flex-direction: column; - position: absolute; top: 0; bottom: 0; left: 0; @@ -28,11 +28,92 @@ limitations under the License. margin-top: 8px; } +.mx_TabbedView_tabsOnLeft { + flex-direction: column; + position: absolute; + + .mx_TabbedView_tabLabels { + width: 170px; + max-width: 170px; + position: fixed; + } + + .mx_TabbedView_tabPanel { + margin-left: 240px; // 170px sidebar + 70px padding + flex-direction: column; + } + + .mx_TabbedView_tabLabel_active { + background-color: $tab-label-active-bg-color; + color: $tab-label-active-fg-color; + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $tab-label-active-icon-bg-color; + } + + .mx_TabbedView_maskedIcon { + width: 16px; + height: 16px; + margin-left: 8px; + margin-right: 16px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 16px; + width: 16px; + height: 16px; + } +} + +.mx_TabbedView_tabsOnTop { + flex-direction: column; + + .mx_TabbedView_tabLabels { + display: flex; + } + + .mx_TabbedView_tabLabel { + padding-left: 0px; + padding-right: 52px; + + .mx_TabbedView_tabLabel_text { + font-size: 15px; + color: $tertiary-fg-color; + } + } + + .mx_TabbedView_tabPanel { + flex-direction: row; + } + + .mx_TabbedView_tabLabel_active { + color: $accent-color; + .mx_TabbedView_tabLabel_text { + color: $accent-color; + } + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $accent-color; + } + + .mx_TabbedView_maskedIcon { + width: 22px; + height: 22px; + margin-left: 0px; + margin-right: 8px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 22px; + width: 22px; + height: 22px; + } +} + .mx_TabbedView_tabLabels { - width: 170px; - max-width: 170px; color: $tab-label-fg-color; - position: fixed; } .mx_TabbedView_tabLabel { @@ -46,16 +127,7 @@ limitations under the License. position: relative; } -.mx_TabbedView_tabLabel_active { - background-color: $tab-label-active-bg-color; - color: $tab-label-active-fg-color; -} - .mx_TabbedView_maskedIcon { - margin-left: 8px; - margin-right: 16px; - width: 16px; - height: 16px; display: inline-block; } @@ -63,26 +135,17 @@ limitations under the License. display: inline-block; background-color: $tab-label-icon-bg-color; mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 16px; mask-position: center; content: ''; } -.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { - background-color: $tab-label-active-icon-bg-color; -} - .mx_TabbedView_tabLabel_text { vertical-align: middle; } .mx_TabbedView_tabPanel { - margin-left: 240px; // 170px sidebar + 70px padding flex-grow: 1; display: flex; - flex-direction: column; min-height: 0; // firefox } diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index d8ff56663ab..6b332d742b8 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -210,17 +210,37 @@ limitations under the License. } } -.mx_InviteDialog { +.mx_InviteDialog_other { // Prevent the dialog from jumping around randomly when elements change. height: 590px; padding-left: 20px; // the design wants some padding on the left + + .mx_InviteDialog_userSections { + height: 455px; // mx_InviteDialog's height minus some for the upper elements + } +} + +.mx_InviteDialog_transfer { + width: 496px; + height: 466px; + display: flex; + flex-direction: column; + + .mx_InviteDialog_content { + flex: 1; + display: flex; + flex-direction: column; + + .mx_TabbedView { + flex: 1; + } + } } .mx_InviteDialog_userSections { margin-top: 10px; overflow-y: auto; padding-right: 45px; - height: 455px; // mx_InviteDialog's height minus some for the upper elements } // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar @@ -233,3 +253,33 @@ limitations under the License. .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { padding: 0; } + +.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField { + border-top: 0px; + border-left: 0px; + border-right: 0px; + border-radius: 0px; + + input { + font-size: 18px; + font-weight: 600; + } +} + +.mx_InviteDialog_dialPad { + width: 224px; + margin-left: auto; + margin-right: auto; +} + +.mx_InviteDialog_transferButton { + float: right; +} + +.mx_InviteDialog_userDirectoryIcon::before { + mask-image: url('$(res)/img/voip/tab-userdirectory.svg'); +} + +.mx_InviteDialog_dialPadIcon::before { + mask-image: url('$(res)/img/voip/tab-dialpad.svg'); +} diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index 0c7bff0ce84..724189894ce 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -30,6 +30,8 @@ limitations under the License. text-align: center; vertical-align: middle; line-height: 40px; + margin-left: auto; + margin-right: auto; } .mx_DialPad_deleteButton, .mx_DialPad_dialButton { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index f02f50d577f..0cb2e71fdd8 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -394,7 +394,7 @@ export default class CallHandler extends EventEmitter { } private setCallListeners(call: MatrixCall) { - let mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + let mappedRoomId = this.roomIdForCall(call); call.on(CallEvent.Error, (err: CallError) => { if (!this.matchesCallForThisRoom(call)) return; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 0097d55cf53..173d55b7eae 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -21,6 +21,7 @@ import {_t} from '../../languageHandler'; import * as sdk from "../../index"; import AutoHideScrollbar from './AutoHideScrollbar'; import {replaceableComponent} from "../../utils/replaceableComponent"; +import classNames from "classnames"; /** * Represents a tab for the TabbedView. @@ -37,9 +38,16 @@ export class Tab { } } +export enum TabLocation { + LEFT = 'left', + TOP = 'top', +} + interface IProps { tabs: Tab[]; initialTabId?: string; + tabLocation: TabLocation; + onChange: (tabId: string) => void; } interface IState { @@ -62,6 +70,10 @@ export default class TabbedView extends React.Component { }; } + static defaultProps = { + tabLocation: TabLocation.LEFT, + } + private _getActiveTabIndex() { if (!this.state || !this.state.activeTabIndex) return 0; return this.state.activeTabIndex; @@ -75,6 +87,7 @@ export default class TabbedView extends React.Component { private _setActiveTab(tab: Tab) { const idx = this.props.tabs.indexOf(tab); if (idx !== -1) { + if (this.props.onChange) this.props.onChange(tab.id); this.setState({activeTabIndex: idx}); } else { console.error("Could not find tab " + tab.label + " in tabs"); @@ -121,8 +134,14 @@ export default class TabbedView extends React.Component { const labels = this.props.tabs.map(tab => this._renderTabLabel(tab)); const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]); + const tabbedViewClasses = classNames({ + 'mx_TabbedView': true, + 'mx_TabbedView_tabsOnLeft': this.props.tabLocation == TabLocation.LEFT, + 'mx_TabbedView_tabsOnTop': this.props.tabLocation == TabLocation.TOP, + }); + return ( -
+
{labels}
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index d5f76094e4f..a082a2e8ce8 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -50,7 +50,7 @@ import {getAddressType} from "../../../UserAddress"; import BaseAvatar from '../avatars/BaseAvatar'; import AccessibleButton from '../elements/AccessibleButton'; import { compare } from '../../../utils/strings'; -import TabbedView, { Tab } from '../../structures/TabbedView'; +import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; import Dialpad from '../voip/DialPad'; import Field from '../elements/Field'; import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload'; @@ -71,6 +71,11 @@ export const KIND_CALL_TRANSFER = "call_transfer"; const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked +enum TabId { + UserDirectory = 'users', + DialPad = 'dialpad', +} + // This is the interface that is expected by various components in this file. It is a bit // awkward because it also matches the RoomMember class from the js-sdk with some extra support // for 3PIDs/email addresses. @@ -343,6 +348,7 @@ interface IInviteDialogState { tryingIdentityServer: boolean; consultFirst: boolean; dialPadValue: string; + currentTabId: TabId; // These two flags are used for the 'Go' button to communicate what is going on. busy: boolean, @@ -394,6 +400,7 @@ export default class InviteDialog extends React.PureComponent { - this.convertFilter(); - const targets = this.convertFilter(); - const targetIds = targets.map(t => t.userId); - if (targetIds.length > 1) { - this.setState({ - errorText: _t("A call can only be transferred to a single user."), - }); - } + if (this.state.currentTabId == TabId.UserDirectory) { + this.convertFilter(); + const targets = this.convertFilter(); + const targetIds = targets.map(t => t.userId); + if (targetIds.length > 1) { + this.setState({ + errorText: _t("A call can only be transferred to a single user."), + }); + } - dis.dispatch({ - action: Action.TransferCallToMatrixID, - call: this.props.call, - destination: targetIds[0], - consultFirst: this.state.consultFirst, - } as TransferCallPayload); + dis.dispatch({ + action: Action.TransferCallToMatrixID, + call: this.props.call, + destination: targetIds[0], + consultFirst: this.state.consultFirst, + } as TransferCallPayload); + } else { + dis.dispatch({ + action: Action.TransferCallToPhoneNumber, + call: this.props.call, + destination: this.state.dialPadValue, + consultFirst: this.state.consultFirst, + } as TransferCallPayload); + } this.props.onFinished(); } @@ -798,6 +814,10 @@ export default class InviteDialog extends React.PureComponent { + this.props.onFinished([]); + }; + private updateSuggestions = async (term) => { MatrixClientPeg.get().searchUserDirectory({term}).then(async r => { if (term !== this.state.filterText) { @@ -933,11 +953,14 @@ export default class InviteDialog extends React.PureComponent { if (!this.state.busy) { let filterText = this.state.filterText; - const targets = this.state.targets.map(t => t); // cheap clone for mutation + let targets = this.state.targets.map(t => t); // cheap clone for mutation const idx = targets.indexOf(member); if (idx >= 0) { targets.splice(idx, 1); } else { + if (this.props.kind === KIND_CALL_TRANSFER && targets.length > 0) { + targets = []; + } targets.push(member); filterText = ""; // clear the filter when the user accepts a suggestion } @@ -1162,6 +1185,11 @@ export default class InviteDialog extends React.PureComponent ( )); @@ -1176,6 +1204,7 @@ export default class InviteDialog extends React.PureComponent ); return ( @@ -1224,7 +1253,7 @@ export default class InviteDialog extends React.PureComponent { ev.preventDefault(); - this.onDialPress(); + this.transferCall(); } private onDialChange = ev => { @@ -1235,19 +1264,8 @@ export default class InviteDialog extends React.PureComponent { - dis.dispatch({ - action: Action.TransferCallToPhoneNumber, - call: this.props.call, - destination: this.state.dialPadValue, - consultFirst: this.state.consultFirst, - } as TransferCallPayload); - this.props.onFinished(); - } - - private onDeletePress = () => { - if (this.state.dialPadValue.length === 0) return; - this.setState({dialPadValue: this.state.dialPadValue.slice(0, -1)}); + private onTabChange = (tabId: TabId) => { + this.setState({currentTabId: tabId}); } render() { @@ -1391,18 +1409,25 @@ export default class InviteDialog extends React.PureComponent - {_t("Transfer")} + + {_t("Cancel")} + +
; } else { console.error("Unknown kind of InviteDialog: " + this.props.kind); @@ -1433,39 +1458,46 @@ export default class InviteDialog extends React.PureComponent - {consultConnectSection} ; let dialogContent; if (this.props.kind === KIND_CALL_TRANSFER) { const tabs = []; - tabs.push(new Tab('UsersTab', _td("Users"), null, usersSection)); + tabs.push(new Tab( + TabId.UserDirectory, _td("User Directory"), 'mx_InviteDialog_userDirectoryIcon', usersSection, + )); - const dialPadSection = + const dialPadSection =
- -
- -
+ +
; + tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection)); + dialogContent = + {consultConnectSection} ; - tabs.push(new Tab('DialPadTab', _td("Dial pad"), null, dialPadSection)); - dialogContent = ; } else { - dialogContent = usersSection; + dialogContent = + usersSection + {consultConnectSection} + ; } + const dialogClass = this.props.kind === KIND_CALL_TRANSFER ? + 'mx_InviteDialog_transfer' : 'mx_InviteDialog_other'; + return (