From 443744733dc8fbea8c3bb805c29b1dbcb80a2a98 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 Jan 2020 23:32:00 -0700 Subject: [PATCH 1/8] 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 f806e85120c..5d58f3ae8bc 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 ba9fe1f5410..675efe53c85 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 371768eb4e0..e82d63acad3 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 9627ac0e14a..82f0cf8521e 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 498c073e0e6..e42d7247486 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/8] 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 e82d63acad3..904d531c601 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/8] 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 904d531c601..6422749d603 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(); From 7da9e0582f3887f3dab030d5279c200ce97f1ce8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jan 2020 13:42:34 -0700 Subject: [PATCH 4/8] Rename DMInviteDialog to be a generic Invite Dialog --- res/css/_components.scss | 2 +- ...DMInviteDialog.scss => _InviteDialog.scss} | 40 ++++++++--------- src/RoomInvite.js | 4 +- .../{DMInviteDialog.js => InviteDialog.js} | 44 +++++++++---------- 4 files changed, 45 insertions(+), 45 deletions(-) rename res/css/views/dialogs/{_DMInviteDialog.scss => _InviteDialog.scss} (86%) rename src/components/views/dialogs/{DMInviteDialog.js => InviteDialog.js} (94%) diff --git a/res/css/_components.scss b/res/css/_components.scss index a9a114a4cf5..60f749de9c8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -57,13 +57,13 @@ @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; -@import "./views/dialogs/_DMInviteDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; +@import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss similarity index 86% rename from res/css/views/dialogs/_DMInviteDialog.scss rename to res/css/views/dialogs/_InviteDialog.scss index 5d58f3ae8bc..d0b53b77663 100644 --- a/res/css/views/dialogs/_DMInviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_DMInviteDialog_addressBar { +.mx_InviteDialog_addressBar { display: flex; flex-direction: row; - .mx_DMInviteDialog_editor { + .mx_InviteDialog_editor { flex: 1; width: 100%; // Needed to make the Field inside grow background-color: $user-tile-hover-bg-color; @@ -28,7 +28,7 @@ limitations under the License. overflow-x: hidden; overflow-y: auto; - .mx_DMInviteDialog_userTile { + .mx_InviteDialog_userTile { display: inline-block; float: left; position: relative; @@ -61,14 +61,14 @@ limitations under the License. } } - .mx_DMInviteDialog_goButton { + .mx_InviteDialog_goButton { width: 48px; margin-left: 10px; height: 25px; line-height: 25px; } - .mx_DMInviteDialog_buttonAndSpinner { + .mx_InviteDialog_buttonAndSpinner { .mx_Spinner { // Width and height are required to trick the layout engine. width: 20px; @@ -80,7 +80,7 @@ limitations under the License. } } -.mx_DMInviteDialog_section { +.mx_InviteDialog_section { padding-bottom: 10px; h3 { @@ -91,7 +91,7 @@ limitations under the License. } } -.mx_DMInviteDialog_roomTile { +.mx_InviteDialog_roomTile { cursor: pointer; padding: 5px 10px; @@ -104,7 +104,7 @@ limitations under the License. vertical-align: middle; } - .mx_DMInviteDialog_roomTile_avatarStack { + .mx_InviteDialog_roomTile_avatarStack { display: inline-block; position: relative; width: 36px; @@ -117,7 +117,7 @@ limitations under the License. } } - .mx_DMInviteDialog_roomTile_selected { + .mx_InviteDialog_roomTile_selected { width: 36px; height: 36px; border-radius: 36px; @@ -141,20 +141,20 @@ limitations under the License. } } - .mx_DMInviteDialog_roomTile_name { + .mx_InviteDialog_roomTile_name { font-weight: 600; font-size: 14px; color: $primary-fg-color; margin-left: 7px; } - .mx_DMInviteDialog_roomTile_userId { + .mx_InviteDialog_roomTile_userId { font-size: 12px; color: $muted-fg-color; margin-left: 7px; } - .mx_DMInviteDialog_roomTile_time { + .mx_InviteDialog_roomTile_time { text-align: right; font-size: 12px; color: $muted-fg-color; @@ -162,16 +162,16 @@ limitations under the License. line-height: 36px; // Height of the avatar to keep the time vertically aligned } - .mx_DMInviteDialog_roomTile_highlight { + .mx_InviteDialog_roomTile_highlight { font-weight: 900; } } // Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog. -.mx_DMInviteDialog_userTile { +.mx_InviteDialog_userTile { margin-right: 8px; - .mx_DMInviteDialog_userTile_pill { + .mx_InviteDialog_userTile_pill { background-color: $username-variant1-color; border-radius: 12px; display: inline-block; @@ -181,27 +181,27 @@ limitations under the License. padding-right: 8px; color: #ffffff; // this is fine without a var because it's for both themes - .mx_DMInviteDialog_userTile_avatar { + .mx_InviteDialog_userTile_avatar { border-radius: 20px; position: relative; left: -5px; top: 2px; } - img.mx_DMInviteDialog_userTile_avatar { + img.mx_InviteDialog_userTile_avatar { vertical-align: top; } - .mx_DMInviteDialog_userTile_name { + .mx_InviteDialog_userTile_name { vertical-align: top; } - .mx_DMInviteDialog_userTile_threepidAvatar { + .mx_InviteDialog_userTile_threepidAvatar { background-color: #ffffff; // this is fine without a var because it's for both themes } } - .mx_DMInviteDialog_userTile_remove { + .mx_InviteDialog_userTile_remove { display: inline-block; margin-left: 4px; } diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 8b7324d4f59..aaddd58d0be 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -44,9 +44,9 @@ export function inviteMultipleToRoom(roomId, addrs) { 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"); + const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog( - 'Start DM', '', DMInviteDialog, {}, + 'Start DM', '', InviteDialog, {}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, ); return; diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/InviteDialog.js similarity index 94% rename from src/components/views/dialogs/DMInviteDialog.js rename to src/components/views/dialogs/InviteDialog.js index 2a5c896a75f..6b8e5328544 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -140,11 +140,11 @@ class DMUserTile extends React.PureComponent { const avatarSize = 20; const avatar = this.props.member.isEmail ? : ; return ( - - + + {avatar} - {this.props.member.name} + {this.props.member.name} {_t('Remove')} @@ -211,7 +211,7 @@ class DMRoomTile extends React.PureComponent { // Highlight the word the user entered const substr = str.substring(i, filterStr.length + i); - result.push({substr}); + result.push({substr}); i += substr.length; } @@ -229,7 +229,7 @@ class DMRoomTile extends React.PureComponent { let timestamp = null; if (this.props.lastActiveTs) { const humanTs = humanizeTime(this.props.lastActiveTs); - timestamp = {humanTs}; + timestamp = {humanTs}; } const avatarSize = 36; @@ -249,30 +249,30 @@ class DMRoomTile extends React.PureComponent { let checkmark = null; if (this.props.isSelected) { // To reduce flickering we put the 'selected' room tile above the real avatar - checkmark =
; + checkmark =
; } // To reduce flickering we put the checkmark on top of the actual avatar (prevents // the browser from reloading the image source when the avatar remounts). const stackedAvatar = ( - + {avatar} {checkmark} ); return ( -
+
{stackedAvatar} - {this._highlightName(this.props.member.name)} - {this._highlightName(this.props.member.userId)} + {this._highlightName(this.props.member.name)} + {this._highlightName(this.props.member.userId)} {timestamp}
); } } -export default class DMInviteDialog extends React.PureComponent { +export default class InviteDialog extends React.PureComponent { static propTypes = { // Takes an array of user IDs/emails to invite. onFinished: PropTypes.func.isRequired, @@ -690,7 +690,7 @@ export default class DMInviteDialog extends React.PureComponent { if (sourceMembers.length === 0 && additionalMembers.length === 0) { return ( -
+

{sectionName}

{_t("No results")}

@@ -731,7 +731,7 @@ export default class DMInviteDialog extends React.PureComponent { /> )); return ( -
+

{sectionName}

{tiles} {showMore} @@ -754,7 +754,7 @@ export default class DMInviteDialog extends React.PureComponent { /> ); return ( -
+
{targets} {input}
@@ -808,12 +808,12 @@ export default class DMInviteDialog extends React.PureComponent { const userId = MatrixClientPeg.get().getUserId(); return ( -
+

{_t( "If you can't find someone, ask them for their username, or share your " + @@ -822,13 +822,13 @@ export default class DMInviteDialog extends React.PureComponent { {a: (sub) => {sub}}, )}

-
+
{this._renderEditor()} -
+
{_t("Go")} From 73fc91aa20a76f8b3a8d2b4a13d3407e4084151f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jan 2020 13:44:59 -0700 Subject: [PATCH 5/8] Rename feature flag for use in both code paths --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 2b8c0aef89f..eacf63e55da 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -130,7 +130,7 @@ export const SETTINGS = { }, "feature_ftue_dms": { isFeature: true, - displayName: _td("New DM invite dialog (under development)"), + displayName: _td("New invite dialog"), supportedLevels: LEVELS_FEATURE, default: false, }, From f350167408b916db2fd3fefea84e500f7016f993 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jan 2020 14:40:12 -0700 Subject: [PATCH 6/8] Support using the InviteDialog for both DMs and invites For https://github.com/vector-im/riot-web/issues/11201 --- src/RoomInvite.js | 14 +- src/components/views/dialogs/InviteDialog.js | 127 +++++++++++++++---- src/i18n/strings/en_EN.json | 24 ++-- 3 files changed, 128 insertions(+), 37 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index aaddd58d0be..2eccf69b0fe 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2020 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. @@ -26,6 +27,7 @@ import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; import SettingsStore from "./settings/SettingsStore"; +import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; /** * Invites multiple addresses to a room @@ -46,7 +48,7 @@ export function showStartChatInviteDialog() { // This new dialog handles the room creation internally - we don't need to worry about it. const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog( - 'Start DM', '', InviteDialog, {}, + 'Start DM', '', InviteDialog, {kind: KIND_DM}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, ); return; @@ -72,6 +74,16 @@ export function showStartChatInviteDialog() { } export function showRoomInviteDialog(roomId) { + if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { + // This new dialog handles the room creation internally - we don't need to worry about it. + const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); + Modal.createTrackedDialog( + 'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); + return; + } + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, { diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 6b8e5328544..7448b1a5a35 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import * as sdk from "../../../index"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import {makeUserPermalink} from "../../../utils/permalinks/Permalinks"; +import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks"; import DMRoomMap from "../../../utils/DMRoomMap"; import {RoomMember} from "matrix-js-sdk/src/matrix"; import SdkConfig from "../../../SdkConfig"; @@ -34,7 +34,8 @@ import {humanizeTime} from "../../../utils/humanize"; import createRoom from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; -// TODO: [TravisR] Make this generic for all kinds of invites +export const KIND_DM = "dm"; +export const KIND_INVITE = "invite"; 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 @@ -276,13 +277,28 @@ export default class InviteDialog extends React.PureComponent { static propTypes = { // Takes an array of user IDs/emails to invite. onFinished: PropTypes.func.isRequired, + + // The kind of invite being performed. Assumed to be KIND_DM if + // not provided. + kind: PropTypes.string, + + // The room ID this dialog is for. Only required for KIND_INVITE. + roomId: PropTypes.string, + }; + + static defaultProps = { + kind: KIND_DM, }; _debounceTimer: number = null; _editorRef: any = null; - constructor() { - super(); + constructor(props) { + super(props); + + if (props.kind === KIND_INVITE && !props.roomId) { + throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog"); + } this.state = { targets: [], // array of Member objects (see interface above) @@ -390,6 +406,21 @@ export default class InviteDialog extends React.PureComponent { return members.map(m => ({userId: m.member.userId, user: m.member})); } + _shouldAbortAfterInviteError(result): boolean { + 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 + } + return false; + } + _startDm = () => { this.setState({busy: true}); const targetIds = this.state.targets.map(t => t.userId); @@ -417,15 +448,7 @@ export default class InviteDialog extends React.PureComponent { 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(", "), - }), - }); + if (this._shouldAbortAfterInviteError(result)) { return true; // abort } }); @@ -444,6 +467,33 @@ export default class InviteDialog extends React.PureComponent { }); }; + _inviteUsers = () => { + this.setState({busy: true}); + const targetIds = this.state.targets.map(t => t.userId); + + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + if (!room) { + console.error("Failed to find the room to invite users to"); + this.setState({ + busy: false, + errorText: _t("Something went wrong trying to invite the users."), + }); + return; + } + + inviteMultipleToRoom(this.props.roomId, targetIds).then(result => { + if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too + this.props.onFinished(); + } + }).catch(err => { + console.error(err); + this.setState({ + busy: false, + errorText: _t("We couldn't invite those users. Please check the users you want to invite and try again."), + }); + }); + }; + _cancel = () => { // We do not want the user to close the dialog while an action is in progress if (this.state.busy) return; @@ -658,7 +708,11 @@ export default class InviteDialog extends React.PureComponent { let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown; const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this); const lastActive = (m) => kind === 'recents' ? m.lastActive : null; - const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); + let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); + + if (this.props.kind === KIND_INVITE) { + sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions"); + } // Mix in the server results if we have any, but only if we're searching. We track the additional // members separately because we want to filter sourceMembers but trust the mixin arrays to have @@ -805,33 +859,54 @@ export default class InviteDialog extends React.PureComponent { spinner = ; } - const userId = MatrixClientPeg.get().getUserId(); + + let title; + let helpText; + let buttonText; + let goButtonFn; + + if (this.props.kind === KIND_DM) { + const userId = MatrixClientPeg.get().getUserId(); + + title = _t("Direct Messages"); + helpText = _t( + "If you can't find someone, ask them for their username, or share your " + + "username (%(userId)s) or profile link.", + {userId}, + {a: (sub) => {sub}}, + ); + buttonText = _t("Go"); + goButtonFn = this._startDm; + } else { // KIND_INVITE + title = _t("Invite to this room"); + helpText = _t( + "If you can't find someone, ask them for their username (e.g. @user:server.com) or " + + "share this room.", {}, + {a: (sub) => {sub}}, + ); + buttonText = _t("Invite"); + goButtonFn = this._inviteUsers; + } + return (
-

- {_t( - "If you can't find someone, ask them for their username, or share your " + - "username (%(userId)s) or profile link.", - {userId}, - {a: (sub) => {sub}}, - )} -

+

{helpText}

{this._renderEditor()}
- {_t("Go")} + {buttonText} {spinner}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b6f61570cd8..f8b17db7c57 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -372,7 +372,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "New DM invite dialog (under development)": "New DM invite dialog (under development)", + "New invite dialog": "New invite dialog", "Show a presence dot next to DMs in the room list": "Show a presence dot next to DMs in the room list", "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", @@ -1438,25 +1438,29 @@ "View Servers in Room": "View Servers in Room", "Toolbox": "Toolbox", "Developer Tools": "Developer Tools", + "An error has occurred.": "An error has occurred.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", + "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", + "Waiting for partner to confirm...": "Waiting for partner to confirm...", + "Incoming Verification Request": "Incoming Verification Request", + "Integrations are disabled": "Integrations are disabled", + "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", + "Integrations not allowed": "Integrations not allowed", + "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.", "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.", + "Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.", + "We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. 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", "Suggestions": "Suggestions", + "Recently Direct Messaged": "Recently Direct Messaged", "Show more": "Show more", "Direct Messages": "Direct Messages", "If you can't find someone, ask them for their username, or share your username (%(userId)s) or profile link.": "If you can't find someone, ask them for their username, or share your username (%(userId)s) or profile link.", "Go": "Go", - "An error has occurred.": "An error has occurred.", - "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", - "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", - "Waiting for partner to confirm...": "Waiting for partner to confirm...", - "Incoming Verification Request": "Incoming Verification Request", - "Integrations are disabled": "Integrations are disabled", - "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", - "Integrations not allowed": "Integrations not allowed", - "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.", + "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room.", "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", "Start verification": "Start verification", From 1a961358f0fe7956cefc246281960c9761f500ae Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jan 2020 14:40:25 -0700 Subject: [PATCH 7/8] Don't show recents and suggestions for users already in the room --- src/components/views/dialogs/InviteDialog.js | 28 +++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 7448b1a5a35..e176d3b105f 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -300,12 +300,24 @@ export default class InviteDialog extends React.PureComponent { throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog"); } + let alreadyInvited = []; + if (props.roomId) { + const room = MatrixClientPeg.get().getRoom(props.roomId); + if (!room) throw new Error("Room ID given to InviteDialog does not look like a room"); + alreadyInvited = [ + ...room.getMembersWithMembership('invite'), + ...room.getMembersWithMembership('join'), + ...room.getMembersWithMembership('ban'), // so we don't try to invite them + ].map(m => m.userId); + } + + this.state = { targets: [], // array of Member objects (see interface above) filterText: "", - recents: this._buildRecents(), + recents: this._buildRecents(alreadyInvited), numRecentsShown: INITIAL_ROOMS_SHOWN, - suggestions: this._buildSuggestions(), + suggestions: this._buildSuggestions(alreadyInvited), numSuggestionsShown: INITIAL_ROOMS_SHOWN, serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions @@ -320,10 +332,13 @@ export default class InviteDialog extends React.PureComponent { this._editorRef = createRef(); } - _buildRecents(): {userId: string, user: RoomMember, lastActive: number} { + _buildRecents(excludedTargetIds: string[]): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); const recents = []; for (const userId in rooms) { + // Filter out user IDs that are already in the room / should be excluded + if (excludedTargetIds.includes(userId)) continue; + const room = rooms[userId]; const member = room.getMember(userId); if (!member) continue; // just skip people who don't have memberships for some reason @@ -342,7 +357,7 @@ export default class InviteDialog extends React.PureComponent { return recents; } - _buildSuggestions(): {userId: string, user: RoomMember} { + _buildSuggestions(excludedTargetIds: string[]): {userId: string, user: RoomMember} { const maxConsideredMembers = 200; const client = MatrixClientPeg.get(); const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']]; @@ -359,6 +374,11 @@ export default class InviteDialog extends React.PureComponent { const joinedMembers = room.getJoinedMembers().filter(u => !excludedUserIds.includes(u.userId)); for (const member of joinedMembers) { + // Filter out user IDs that are already in the room / should be excluded + if (excludedTargetIds.includes(member.userId)) { + continue; + } + if (!members[member.userId]) { members[member.userId] = { member: member, From e42663fc627187cee9a59e65cd8ff1abccb95bf3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 16 Jan 2020 14:45:17 -0700 Subject: [PATCH 8/8] Appease the linter --- src/components/views/dialogs/InviteDialog.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index e176d3b105f..1b7a50c084b 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -509,7 +509,9 @@ export default class InviteDialog extends React.PureComponent { console.error(err); this.setState({ busy: false, - errorText: _t("We couldn't invite those users. Please check the users you want to invite and try again."), + errorText: _t( + "We couldn't invite those users. Please check the users you want to invite and try again.", + ), }); }); };