+
{labels}
diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx
index 428e18ed30d..76e16706696 100644
--- a/src/components/views/context_menus/CallContextMenu.tsx
+++ b/src/components/views/context_menus/CallContextMenu.tsx
@@ -53,7 +53,7 @@ export default class CallContextMenu extends React.Component
{
onTransferClick = () => {
Modal.createTrackedDialog(
'Transfer Call', '', InviteDialog, { kind: KIND_CALL_TRANSFER, call: this.props.call },
- /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
+ /*className=*/"mx_InviteDialog_transferWrapper", /*isPriority=*/false, /*isStatic=*/true,
);
this.props.onFinished();
};
diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx
index 28a73ba8d40..3a1f0aafaff 100644
--- a/src/components/views/context_menus/DialpadContextMenu.tsx
+++ b/src/components/views/context_menus/DialpadContextMenu.tsx
@@ -62,7 +62,7 @@ export default class DialpadContextMenu extends React.Component
-
+
;
}
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index d9dcb8fe001..a603884758e 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -33,7 +33,6 @@ import Modal from "../../../Modal";
import { humanizeTime } from "../../../utils/humanize";
import createRoom, {
canEncryptToAllUsers,
- ensureDMExists,
findDMForUser,
privateShouldBeEncrypted,
} from "../../../createRoom";
@@ -65,6 +64,10 @@ 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';
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@@ -77,11 +80,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.
@@ -354,6 +365,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,
@@ -405,6 +418,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) => {
@@ -825,6 +828,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) {
@@ -960,11 +967,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 +1199,11 @@ export default class InviteDialog extends React.PureComponent (
));
@@ -1201,8 +1216,9 @@ export default class InviteDialog extends React.PureComponent 0)}
autoComplete="off"
+ placeholder={hasPlaceholder ? _t("Search") : null}
/>
);
return (
@@ -1249,6 +1265,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);
@@ -1282,12 +1320,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) {
@@ -1425,23 +1467,98 @@ 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 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/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index dff7a8f7485..65878e4dfc4 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -55,7 +55,8 @@ class DialPadButton extends React.PureComponent {
interface IProps {
onDigitPress: (string) => void;
- hasDialAndDelete: boolean;
+ hasDial: boolean;
+ hasDelete: boolean;
onDeletePress?: (string) => void;
onDialPress?: (string) => void;
}
@@ -71,10 +72,12 @@ export default class Dialpad extends React.PureComponent {
/>);
}
- if (this.props.hasDialAndDelete) {
+ if (this.props.hasDelete) {
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 5e5903531ef..3d5e130b978 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -89,7 +89,7 @@ export default class DialpadModal extends React.PureComponent {