From 5333114d7bd6e83f813b71be9a55bb883976dc8c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 21:43:21 -0700 Subject: [PATCH] Give a route for retrying invites for users which may not exist Fixes https://github.com/vector-im/riot-web/issues/7922 This supports the current style of errors (M_NOT_FOUND) as well as the errors presented by MSC1797: https://github.com/matrix-org/matrix-doc/pull/1797 --- src/components/structures/UserSettings.js | 1 + .../views/dialogs/RetryInvitesDialog.js | 78 ++++++++ src/i18n/strings/en_EN.json | 6 + src/settings/Settings.js | 5 + src/utils/MultiInviter.js | 168 ++++++++++++------ 5 files changed, 207 insertions(+), 51 deletions(-) create mode 100644 src/components/views/dialogs/RetryInvitesDialog.js diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b9dbe345c5..6ba7bcc4dc 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -86,6 +86,7 @@ const SIMPLE_SETTINGS = [ { id: "pinMentionedRooms" }, { id: "pinUnreadRooms" }, { id: "showDeveloperTools" }, + { id: "alwaysRetryInvites" }, ]; // These settings must be defined in SettingsStore diff --git a/src/components/views/dialogs/RetryInvitesDialog.js b/src/components/views/dialogs/RetryInvitesDialog.js new file mode 100644 index 0000000000..24647ae4a0 --- /dev/null +++ b/src/components/views/dialogs/RetryInvitesDialog.js @@ -0,0 +1,78 @@ +/* +Copyright 2019 New Vector Ltd + +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 React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; + +export default React.createClass({ + propTypes: { + failedInvites: PropTypes.object.isRequired, // { address: { errcode, errorText } } + onTryAgain: PropTypes.func.isRequired, + onGiveUp: PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, + }, + + _onTryAgainClicked: function() { + this.props.onTryAgain(); + this.props.onFinished(true); + }, + + _onTryAgainNeverWarnClicked: function() { + SettingsStore.setValue("alwaysRetryInvites", null, SettingLevel.ACCOUNT, true); + this.props.onTryAgain(); + this.props.onFinished(true); + }, + + _onGiveUpClicked: function() { + this.props.onGiveUp(); + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + const errorList = Object.keys(this.props.failedInvites) + .map(address =>

{address}: {this.props.failedInvites[address].errorText}

); + + return ( + +
+ { errorList } +
+ +
+ + + +
+
+ ); + }, +}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef659bf566..816506f6c3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -222,8 +222,10 @@ "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", + "Unrecognised address": "Unrecognised address", "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", "User %(user_id)s does not exist": "User %(user_id)s does not exist", + "User %(user_id)s may or may not exist": "User %(user_id)s may or may not exist", "Unknown server error": "Unknown server error", "Use a few words, avoid common phrases": "Use a few words, avoid common phrases", "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", @@ -291,6 +293,7 @@ "Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Show empty room list headings": "Show empty room list headings", + "Always retry invites for unknown users": "Always retry invites for unknown users", "Show developer tools": "Show developer tools", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", @@ -965,6 +968,9 @@ "Clear cache and resync": "Clear cache and resync", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "Updating Riot": "Updating Riot", + "Failed to invite the following users": "Failed to invite the following users", + "Try again and never warn me again": "Try again and never warn me again", + "Try again": "Try again", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 1cac8559d1..507bcf49b8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -317,6 +317,11 @@ export const SETTINGS = { displayName: _td('Show empty room list headings'), default: true, }, + "alwaysRetryInvites": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Always retry invites for unknown users'), + default: false, + }, "showDeveloperTools": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Show developer tools'), diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index ad10f28edf..0d7a8837b8 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -15,11 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; import GroupStore from '../stores/GroupStore'; import Promise from 'bluebird'; import {_t} from "../languageHandler"; +import sdk from "../index"; +import Modal from "../Modal"; +import SettingsStore from "../settings/SettingsStore"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -41,7 +45,7 @@ export default class MultiInviter { this.addrs = []; this.busy = false; this.completionStates = {}; // State of each address (invited or error) - this.errorTexts = {}; // Textual error per address + this.errors = {}; // { address: {errorText, errcode} } this.deferred = null; } @@ -61,7 +65,10 @@ export default class MultiInviter { for (const addr of this.addrs) { if (getAddressType(addr) === null) { this.completionStates[addr] = 'error'; - this.errorTexts[addr] = 'Unrecognised address'; + this.errors[addr] = { + errcode: 'M_INVALID', + errorText: _t('Unrecognised address'), + }; } } this.deferred = Promise.defer(); @@ -85,18 +92,23 @@ export default class MultiInviter { } getErrorText(addr) { - return this.errorTexts[addr]; + return this.errors[addr] ? this.errors[addr].errorText : null; } - async _inviteToRoom(roomId, addr) { + async _inviteToRoom(roomId, addr, ignoreProfile) { const addrType = getAddressType(addr); if (addrType === 'email') { return MatrixClientPeg.get().inviteByEmail(roomId, addr); } else if (addrType === 'mx-user-id') { - const profile = await MatrixClientPeg.get().getProfileInfo(addr); - if (!profile) { - return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."}); + if (!ignoreProfile && !SettingsStore.getValue("alwaysRetryInvites", this.roomId)) { + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + return Promise.reject({ + errcode: "M_NOT_FOUND", + error: "User does not have a profile or does not exist.", + }); + } } return MatrixClientPeg.get().invite(roomId, addr); @@ -105,19 +117,113 @@ export default class MultiInviter { } } + _doInvite(address, ignoreProfile) { + return new Promise((resolve, reject) => { + let doInvite; + if (this.groupId !== null) { + doInvite = GroupStore.inviteUserToGroup(this.groupId, address); + } else { + doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile); + } - _inviteMore(nextIndex) { + doInvite.then(() => { + if (this._canceled) { + return; + } + + this.completionStates[address] = 'invited'; + delete this.errors[address]; + + resolve(); + }).catch((err) => { + if (this._canceled) { + return; + } + + let errorText; + let fatal = false; + if (err.errcode === 'M_FORBIDDEN') { + fatal = true; + errorText = _t('You do not have permission to invite people to this room.'); + } else if (err.errcode === 'M_LIMIT_EXCEEDED') { + // we're being throttled so wait a bit & try again + setTimeout(() => { + this._doInvite(address, ignoreProfile).then(resolve, reject); + }, 5000); + return; + } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { + errorText = _t("User %(user_id)s does not exist", {user_id: address}); + } else if (err.errcode === 'M_PROFILE_UNKNOWN') { + errorText = _t("User %(user_id)s may or may not exist", {user_id: address}); + } else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) { + // Invite without the profile check + console.warn(`User ${address} does not have a profile - trying invite again`); + this._doInvite(address, true).then(resolve, reject); + } else { + errorText = _t('Unknown server error'); + } + + this.completionStates[address] = 'error'; + this.errors[address] = {errorText, errcode: err.errcode}; + + this.busy = !fatal; + this.fatal = fatal; + + if (fatal) { + reject(); + } else { + resolve(); + } + }); + }); + } + + _inviteMore(nextIndex, ignoreProfile) { if (this._canceled) { return; } if (nextIndex === this.addrs.length) { this.busy = false; + if (Object.keys(this.errors).length > 0 && !this.groupId) { + // There were problems inviting some people - see if we can invite them + // without caring if they exist or not. + const reinviteErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNKNOWN', 'M_PROFILE_NOT_FOUND']; + const reinvitableUsers = Object.keys(this.errors).filter(a => reinviteErrors.includes(this.errors[a].errcode)); + + if (reinvitableUsers.length > 0) { + const retryInvites = () => { + const promises = reinvitableUsers.map(u => this._doInvite(u, true)); + Promise.all(promises).then(() => this.deferred.resolve(this.completionStates)); + }; + + if (SettingsStore.getValue("alwaysRetryInvites", this.roomId)) { + retryInvites(); + return; + } + + const RetryInvitesDialog = sdk.getComponent("dialogs.RetryInvitesDialog"); + console.log("Showing failed to invite dialog..."); + Modal.createTrackedDialog('Failed to invite the following users to the room', '', RetryInvitesDialog, { + failedInvites: this.errors, + onTryAgain: () => retryInvites(), + onGiveUp: () => { + // Fake all the completion states because we already warned the user + for (const addr of Object.keys(this.completionStates)) { + this.completionStates[addr] = 'invited'; + } + this.deferred.resolve(this.completionStates); + }, + }); + return; + } + } this.deferred.resolve(this.completionStates); return; } const addr = this.addrs[nextIndex]; + console.log(`Inviting ${addr}`); // don't try to invite it if it's an invalid address // (it will already be marked as an error though, @@ -134,48 +240,8 @@ export default class MultiInviter { return; } - let doInvite; - if (this.groupId !== null) { - doInvite = GroupStore.inviteUserToGroup(this.groupId, addr); - } else { - doInvite = this._inviteToRoom(this.roomId, addr); - } - - doInvite.then(() => { - if (this._canceled) { return; } - - this.completionStates[addr] = 'invited'; - - this._inviteMore(nextIndex + 1); - }).catch((err) => { - if (this._canceled) { return; } - - let errorText; - let fatal = false; - if (err.errcode === 'M_FORBIDDEN') { - fatal = true; - errorText = _t('You do not have permission to invite people to this room.'); - } else if (err.errcode === 'M_LIMIT_EXCEEDED') { - // we're being throttled so wait a bit & try again - setTimeout(() => { - this._inviteMore(nextIndex); - }, 5000); - return; - } else if(err.errcode === "M_NOT_FOUND") { - errorText = _t("User %(user_id)s does not exist", {user_id: addr}); - } else { - errorText = _t('Unknown server error'); - } - this.completionStates[addr] = 'error'; - this.errorTexts[addr] = errorText; - this.busy = !fatal; - this.fatal = fatal; - - if (!fatal) { - this._inviteMore(nextIndex + 1); - } else { - this.deferred.resolve(this.completionStates); - } - }); + this._doInvite(addr, ignoreProfile).then(() => { + this._inviteMore(nextIndex + 1, ignoreProfile); + }).catch(() => this.deferred.resolve(this.completionStates)); } }