+
-
-
-
;
}
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index c9475d4849..f8b2297f5c 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -32,7 +32,6 @@ import Modal from "../../../Modal";
import { humanizeTime } from "../../../utils/humanize";
import createRoom, {
canEncryptToAllUsers,
- ensureDMExists,
findDMForUser,
privateShouldBeEncrypted,
} from "../../../createRoom";
@@ -64,9 +63,14 @@ import { copyPlaintext, selectText } from "../../../utils/strings";
import * as ContextMenu from "../../structures/ContextMenu";
import { toRightOf } from "../../structures/ContextMenu";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
+import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload';
+import Field from '../elements/Field';
+import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
+import Dialpad from '../voip/DialPad';
import QuestionDialog from "./QuestionDialog";
import Spinner from "../elements/Spinner";
import BaseDialog from "./BaseDialog";
+import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@@ -79,11 +83,19 @@ interface IRecentUser {
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
+// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
+// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
+// be passed when creating the modal
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 the Invite Dialog and RoomInvite.
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
// for 3PIDs/email addresses.
@@ -356,6 +368,8 @@ interface IInviteDialogState {
canUseIdentityServer: boolean;
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;
@@ -407,6 +421,8 @@ 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.consultFirst) {
- const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
-
- dis.dispatch({
- action: 'place_call',
- type: this.props.call.type,
- room_id: dmRoomId,
- transferee: this.props.call,
- });
- dis.dispatch({
- action: 'view_room',
- room_id: dmRoomId,
- should_peek: false,
- joining: false,
- });
- this.props.onFinished();
- } else {
- this.setState({ busy: true });
- try {
- await this.props.call.transfer(targetIds[0]);
- this.setState({ busy: false });
- this.props.onFinished();
- } catch (e) {
+ 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({
- busy: false,
- errorText: _t("Failed to transfer call"),
+ errorText: _t("A call can only be transferred to a single user."),
});
+ return;
}
+
+ 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();
};
private onKeyDown = (e) => {
@@ -827,6 +831,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) {
@@ -962,11 +970,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
}
@@ -1189,6 +1200,11 @@ export default class InviteDialog extends React.PureComponent (
));
@@ -1201,8 +1217,9 @@ export default class InviteDialog extends React.PureComponent 0)}
autoComplete="off"
+ placeholder={hasPlaceholder ? _t("Search") : null}
/>
);
return (
@@ -1249,6 +1266,28 @@ export default class InviteDialog extends React.PureComponent {
+ ev.preventDefault();
+ this.transferCall();
+ };
+
+ private onDialChange = ev => {
+ this.setState({ dialPadValue: ev.currentTarget.value });
+ };
+
+ private onDigitPress = digit => {
+ this.setState({ dialPadValue: this.state.dialPadValue + digit });
+ };
+
+ 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 });
+ };
+
private async onLinkClick(e) {
e.preventDefault();
selectText(e.target);
@@ -1278,12 +1317,16 @@ export default class InviteDialog extends React.PureComponent;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
+ const hasSelection = this.state.targets.length > 0
+ || (this.state.filterText && this.state.filterText.includes('@'));
+
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
if (this.props.kind === KIND_DM) {
@@ -1421,23 +1464,116 @@ export default class InviteDialog extends React.PureComponent
+
+ consultConnectSection = ;
} else {
console.error("Unknown kind of InviteDialog: " + this.props.kind);
}
- const hasSelection = this.state.targets.length > 0
- || (this.state.filterText && this.state.filterText.includes('@'));
+ const goButton = this.props.kind == KIND_CALL_TRANSFER ? null :
+ {buttonText}
+ ;
+
+ const usersSection =
+ {helpText}
+
+ {this.renderEditor()}
+
+ {goButton}
+ {spinner}
+
+
+ {keySharingWarning}
+ {this.renderIdentityServerWarning()}
+ {this.state.errorText}
+
+ {this.renderSection('recents')}
+ {this.renderSection('suggestions')}
+ {extraSection}
+
+ {footer}
+ ;
+
+ let dialogContent;
+ if (this.props.kind === KIND_CALL_TRANSFER) {
+ const tabs = [];
+ tabs.push(new Tab(
+ TabId.UserDirectory, _td("User Directory"), 'mx_InviteDialog_userDirectoryIcon', usersSection,
+ ));
+
+ const backspaceButton = (
+
+ );
+
+ // Only show the backspace button if the field has content
+ let dialPadField;
+ if (this.state.dialPadValue.length !== 0) {
+ dialPadField = ;
+ } else {
+ dialPadField = ;
+ }
+
+ const dialPadSection =
+
+
+
;
+ tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
+ dialogContent =
+
+ {consultConnectSection}
+ ;
+ } else {
+ dialogContent =
+ {usersSection}
+ {consultConnectSection}
+ ;
+ }
+
return (
-
{helpText}
-
- {this.renderEditor()}
-
-
- {buttonText}
-
- {spinner}
-
-
- {keySharingWarning}
- {this.renderIdentityServerWarning()}
-
{this.state.errorText}
-
- {this.renderSection('recents')}
- {this.renderSection('suggestions')}
- {extraSection}
-
- {footer}
+ {dialogContent}
);
diff --git a/src/components/views/elements/DialPadBackspaceButton.tsx b/src/components/views/elements/DialPadBackspaceButton.tsx
new file mode 100644
index 0000000000..69f0fcb39a
--- /dev/null
+++ b/src/components/views/elements/DialPadBackspaceButton.tsx
@@ -0,0 +1,31 @@
+/*
+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 * as React from "react";
+import AccessibleButton from "./AccessibleButton";
+
+interface IProps {
+ // Callback for when the button is pressed
+ onBackspacePress: () => void;
+}
+
+export default class DialPadBackspaceButton extends React.PureComponent {
+ render() {
+ return ;
+ }
+}
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index dff7a8f748..6687c89b52 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -19,16 +19,17 @@ import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
+const BUTTON_LETTERS = ['', 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '', '+', ''];
enum DialPadButtonKind {
Digit,
- Delete,
Dial,
}
interface IButtonProps {
kind: DialPadButtonKind;
digit?: string;
+ digitSubtext?: string;
onButtonPress: (string) => void;
}
@@ -42,11 +43,10 @@ class DialPadButton extends React.PureComponent {
case DialPadButtonKind.Digit:
return
{this.props.digit}
+
+ {this.props.digitSubtext}
+
;
- case DialPadButtonKind.Delete:
- return ;
case DialPadButtonKind.Dial:
return ;
}
@@ -55,7 +55,7 @@ class DialPadButton extends React.PureComponent {
interface IProps {
onDigitPress: (string) => void;
- hasDialAndDelete: boolean;
+ hasDial: boolean;
onDeletePress?: (string) => void;
onDialPress?: (string) => void;
}
@@ -65,16 +65,15 @@ export default class Dialpad extends React.PureComponent {
render() {
const buttonNodes = [];
- for (const button of BUTTONS) {
+ for (let i = 0; i < BUTTONS.length; i++) {
+ const button = BUTTONS[i];
+ const digitSubtext = BUTTON_LETTERS[i];
buttonNodes.push();
}
- if (this.props.hasDialAndDelete) {
- buttonNodes.push();
+ if (this.props.hasDial) {
buttonNodes.push();
diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx
index 5e5903531e..033aa2e700 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -15,7 +15,6 @@ limitations under the License.
*/
import * as React from "react";
-import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import DialPad from './DialPad';
@@ -23,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
import { Action } from "../../../dispatcher/actions";
+import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
interface IProps {
onFinished: (boolean) => void;
@@ -74,22 +74,38 @@ export default class DialpadModal extends React.PureComponent {
};
render() {
+ const backspaceButton = (
+
+ );
+
+ // Only show the backspace button if the field has content
+ let dialPadField;
+ if (this.state.value.length !== 0) {
+ dialPadField = ;
+ } else {
+ dialPadField = ;
+ }
+
return