diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss index f806e85120..5d58f3ae8b 100644 --- a/res/css/views/dialogs/_DMInviteDialog.scss +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -67,6 +67,17 @@ limitations under the License. height: 25px; line-height: 25px; } + + .mx_DMInviteDialog_buttonAndSpinner { + .mx_Spinner { + // Width and height are required to trick the layout engine. + width: 20px; + height: 20px; + margin-left: 5px; + display: inline-block; + vertical-align: middle; + } + } } .mx_DMInviteDialog_section { diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 2fe64c994f..8b7324d4f5 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -36,21 +36,19 @@ import SettingsStore from "./settings/SettingsStore"; * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -function inviteMultipleToRoom(roomId, addrs) { +export function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); } export function showStartChatInviteDialog() { if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { + // This new dialog handles the room creation internally - we don't need to worry about it. const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog"); - Modal.createTrackedDialog('Start DM', '', DMInviteDialog, { - onFinished: (inviteIds) => { - // TODO: Replace _onStartDmFinished with less hacks - if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i}))); - // else ignore and just do nothing - }, - }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + Modal.createTrackedDialog( + 'Start DM', '', DMInviteDialog, {}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); return; } diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index c0ff9b96fe..2a5c896a75 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -31,6 +31,8 @@ import dis from "../../../dispatcher"; import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; +import createRoom from "../../../createRoom"; +import {inviteMultipleToRoom} from "../../../RoomInvite"; // TODO: [TravisR] Make this generic for all kinds of invites @@ -293,6 +295,10 @@ export default class DMInviteDialog extends React.PureComponent { threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(), tryingIdentityServer: false, + + // These two flags are used for the 'Go' button to communicate what is going on. + busy: false, + errorText: null, }; this._editorRef = createRef(); @@ -385,11 +391,64 @@ export default class DMInviteDialog extends React.PureComponent { } _startDm = () => { - this.props.onFinished(this.state.targets.map(t => t.userId)); + this.setState({busy: true}); + const targetIds = this.state.targets.map(t => t.userId); + + // Check if there is already a DM with these people and reuse it if possible. + const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds); + if (existingRoom) { + dis.dispatch({ + action: 'view_room', + room_id: existingRoom.roomId, + should_peek: false, + joining: false, + }); + this.props.onFinished(); + return; + } + + // Check if it's a traditional DM and create the room if required. + // TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM + let createRoomPromise = Promise.resolve(); + if (targetIds.length === 1) { + createRoomPromise = createRoom({dmUserId: targetIds[0]}); + } else { + // Create a boring room and try to invite the targets manually. + createRoomPromise = createRoom().then(roomId => { + return inviteMultipleToRoom(roomId, targetIds); + }).then(result => { + const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error'); + if (failedUsers.length > 0) { + console.log("Failed to invite users: ", result); + this.setState({ + busy: false, + errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", { + csvUsers: failedUsers.join(", "), + }), + }); + return true; // abort + } + }); + } + + // the createRoom call will show the room for us, so we don't need to worry about that. + createRoomPromise.then(abort => { + if (abort === true) return; // only abort on true booleans, not roomIds or something + this.props.onFinished(); + }).catch(err => { + console.error(err); + this.setState({ + busy: false, + errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."), + }); + }); }; _cancel = () => { - this.props.onFinished([]); + // We do not want the user to close the dialog while an action is in progress + if (this.state.busy) return; + + this.props.onFinished(); }; _updateFilter = (e) => { @@ -739,6 +798,12 @@ export default class DMInviteDialog extends React.PureComponent { render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + const Spinner = sdk.getComponent("elements.Spinner"); + + let spinner = null; + if (this.state.busy) { + spinner = ; + } const userId = MatrixClientPeg.get().getUserId(); return ( @@ -759,15 +824,20 @@ export default class DMInviteDialog extends React.PureComponent {

{this._renderEditor()} - {this._renderIdentityServerWarning()} - - {_t("Go")} - +
+ + {_t("Go")} + + {spinner} +
+ {this._renderIdentityServerWarning()} +
{this.state.errorText}
{this._renderSection('recents')} {this._renderSection('suggestions')} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3756b4c60b..b6f61570cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1438,6 +1438,8 @@ "View Servers in Room": "View Servers in Room", "Toolbox": "Toolbox", "Developer Tools": "Developer Tools", + "Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s", + "We couldn't create your DM. Please check the users you want to invite and try again.": "We couldn't create your DM. Please check the users you want to invite and try again.", "Failed to find the following users": "Failed to find the following users", "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s", "Recent Conversations": "Recent Conversations", diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js index 547da0863b..43ef0035fc 100644 --- a/src/utils/DMRoomMap.js +++ b/src/utils/DMRoomMap.js @@ -124,6 +124,27 @@ export default class DMRoomMap { return this._getUserToRooms()[userId] || []; } + /** + * Gets the DM room which the given IDs share, if any. + * @param {string[]} ids The identifiers (user IDs and email addresses) to look for. + * @returns {Room} The DM room which all IDs given share, or falsey if no common room. + */ + getDMRoomForIdentifiers(ids) { + // TODO: [Canonical DMs] Handle lookups for email addresses. + // For now we'll pretend we only get user IDs and end up returning nothing for email addresses + + let commonRooms = this.getDMRoomsForUserId(ids[0]); + for (let i = 1; i < ids.length; i++) { + const userRooms = this.getDMRoomsForUserId(ids[i]); + commonRooms = commonRooms.filter(r => userRooms.includes(r)); + } + + const joinedRooms = commonRooms.map(r => MatrixClientPeg.get().getRoom(r)) + .filter(r => r && r.getMyMembership() === 'join'); + + return joinedRooms[0]; + } + getUserIdForRoomId(roomId) { if (this.roomToUser == null) { // we lazily populate roomToUser so you can use