From 443744733dc8fbea8c3bb805c29b1dbcb80a2a98 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 Jan 2020 23:32:00 -0700 Subject: [PATCH 1/3] Move DM creation logic into DMInviteDialog Fixes https://github.com/vector-im/riot-web/issues/11645 The copy hasn't been reviewed by anyone and could probably use some work. --- res/css/views/dialogs/_DMInviteDialog.scss | 11 +++ src/RoomInvite.js | 14 ++- .../views/dialogs/DMInviteDialog.js | 92 +++++++++++++++++-- src/i18n/strings/en_EN.json | 2 + src/utils/DMRoomMap.js | 21 +++++ 5 files changed, 122 insertions(+), 18 deletions(-) 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 ba9fe1f541..675efe53c8 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 371768eb4e..e82d63acad 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -31,6 +31,8 @@ import {abbreviateUrl} from "../../../utils/UrlUtils"; import dis from "../../../dispatcher"; import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; +import createRoom from "../../../createRoom"; +import {inviteMultipleToRoom} from "../../../RoomInvite"; // TODO: [TravisR] Make this generic for all kinds of invites @@ -295,6 +297,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: true, + errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."), }; this._editorRef = createRef(); @@ -381,11 +387,66 @@ 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. + let room; + createRoomPromise = createRoom().then(roomId => { + room = MatrixClientPeg.get().getRoom(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) => { @@ -735,6 +796,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 ( @@ -755,15 +822,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 9627ac0e14..82f0cf8521 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1423,6 +1423,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 498c073e0e..e42d724748 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 From b9852c7264cff54246bfb3bbd9f42b5dd8cf047d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 Jan 2020 23:35:07 -0700 Subject: [PATCH 2/3] Appease the linter --- src/components/views/dialogs/DMInviteDialog.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index e82d63acad..904d531c60 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -407,12 +407,10 @@ export default class DMInviteDialog extends React.PureComponent { // 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]}) + createRoomPromise = createRoom({dmUserId: targetIds[0]}); } else { // Create a boring room and try to invite the targets manually. - let room; createRoomPromise = createRoom().then(roomId => { - room = MatrixClientPeg.get().getRoom(roomId); return inviteMultipleToRoom(roomId, targetIds); }).then(result => { const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error'); From 8b6a5d37aa10d4155a833c0b91ddd8de5ed903c4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 Jan 2020 23:35:45 -0700 Subject: [PATCH 3/3] Remove simulated error state used for screenshot --- src/components/views/dialogs/DMInviteDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index 904d531c60..6422749d60 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -299,8 +299,8 @@ export default class DMInviteDialog extends React.PureComponent { tryingIdentityServer: false, // These two flags are used for the 'Go' button to communicate what is going on. - busy: true, - errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."), + busy: false, + errorText: null, }; this._editorRef = createRef();