From 5333114d7bd6e83f813b71be9a55bb883976dc8c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 21:43:21 -0700 Subject: [PATCH 1/3] 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)); } } From c351ee3d30d2fe6ae3fa8a023fa783791ee98162 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 21:54:07 -0700 Subject: [PATCH 2/3] Appease the linter --- src/components/views/dialogs/RetryInvitesDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/RetryInvitesDialog.js b/src/components/views/dialogs/RetryInvitesDialog.js index 24647ae4a0..f27b0bc08b 100644 --- a/src/components/views/dialogs/RetryInvitesDialog.js +++ b/src/components/views/dialogs/RetryInvitesDialog.js @@ -49,7 +49,7 @@ export default React.createClass({ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const errorList = Object.keys(this.props.failedInvites) - .map(address =>

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

); + .map(address =>

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

); return ( Date: Fri, 11 Jan 2019 15:46:03 -0700 Subject: [PATCH 3/3] Rephrase everything to be "invite anyways" rather than "retry" Also handle profile errors better --- ...itesDialog.js => AskInviteAnywayDialog.js} | 33 ++++++------ src/i18n/strings/en_EN.json | 9 ++-- src/settings/Settings.js | 4 +- src/utils/MultiInviter.js | 52 +++++++++++-------- 4 files changed, 54 insertions(+), 44 deletions(-) rename src/components/views/dialogs/{RetryInvitesDialog.js => AskInviteAnywayDialog.js} (63%) diff --git a/src/components/views/dialogs/RetryInvitesDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js similarity index 63% rename from src/components/views/dialogs/RetryInvitesDialog.js rename to src/components/views/dialogs/AskInviteAnywayDialog.js index f27b0bc08b..5c61c3a694 100644 --- a/src/components/views/dialogs/RetryInvitesDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -23,20 +23,20 @@ import SettingsStore from "../../../settings/SettingsStore"; export default React.createClass({ propTypes: { - failedInvites: PropTypes.object.isRequired, // { address: { errcode, errorText } } - onTryAgain: PropTypes.func.isRequired, + unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] + onInviteAnyways: PropTypes.func.isRequired, onGiveUp: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired, }, - _onTryAgainClicked: function() { - this.props.onTryAgain(); + _onInviteClicked: function() { + this.props.onInviteAnyways(); this.props.onFinished(true); }, - _onTryAgainNeverWarnClicked: function() { - SettingsStore.setValue("alwaysRetryInvites", null, SettingLevel.ACCOUNT, true); - this.props.onTryAgain(); + _onInviteNeverWarnClicked: function() { + SettingsStore.setValue("alwaysInviteUnknownUsers", null, SettingLevel.ACCOUNT, true); + this.props.onInviteAnyways(); this.props.onFinished(true); }, @@ -48,28 +48,31 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const errorList = Object.keys(this.props.failedInvites) - .map(address =>

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

); + const errorList = this.props.unknownProfileUsers + .map(address =>
  • {address.userId}: {address.errorText}
  • ); return (
    - { errorList } +

    {_t("The following users may not exist - would you like to invite them anyways?")}

    +
      + { errorList } +
    - -
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 816506f6c3..4f8674db2f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -293,7 +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", + "Always invite users which may not exist": "Always invite users which may not exist", "Show developer tools": "Show developer tools", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", @@ -884,6 +884,10 @@ "That doesn't look like a valid email address": "That doesn't look like a valid email address", "You have entered an invalid address.": "You have entered an invalid address.", "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", + "The following users may not exist": "The following users may not exist", + "The following users may not exist - would you like to invite them anyways?": "The following users may not exist - would you like to invite them anyways?", + "Invite anyways and never warn me again": "Invite anyways and never warn me again", + "Invite anyways": "Invite anyways", "Preparing to send logs": "Preparing to send logs", "Logs sent": "Logs sent", "Thank you!": "Thank you!", @@ -968,9 +972,6 @@ "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 507bcf49b8..a007f78c1f 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -317,9 +317,9 @@ export const SETTINGS = { displayName: _td('Show empty room list headings'), default: true, }, - "alwaysRetryInvites": { + "alwaysInviteUnknownUsers": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td('Always retry invites for unknown users'), + displayName: _td('Always invite users which may not exist'), default: false, }, "showDeveloperTools": { diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index 0d7a8837b8..b5f4f960a9 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -101,13 +101,18 @@ export default class MultiInviter { if (addrType === 'email') { return MatrixClientPeg.get().inviteByEmail(roomId, addr); } else if (addrType === 'mx-user-id') { - 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.", - }); + if (!ignoreProfile && !SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) { + try { + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + // noinspection ExceptionCaughtLocallyJS + throw new Error("User has no profile"); + } + } catch (e) { + throw { + errcode: "RIOT.USER_NOT_FOUND", + error: "User does not have a profile or does not exist." + }; } } @@ -119,6 +124,8 @@ export default class MultiInviter { _doInvite(address, ignoreProfile) { return new Promise((resolve, reject) => { + console.log(`Inviting ${address}`); + let doInvite; if (this.groupId !== null) { doInvite = GroupStore.inviteUserToGroup(this.groupId, address); @@ -151,13 +158,13 @@ export default class MultiInviter { this._doInvite(address, ignoreProfile).then(resolve, reject); }, 5000); return; - } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { + } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.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') { + } else if (err.errcode === 'M_PROFILE_UNDISCLOSED') { 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`); + console.warn(`User ${address} does not have a profile - inviting anyways automatically`); this._doInvite(address, true).then(resolve, reject); } else { errorText = _t('Unknown server error'); @@ -188,28 +195,28 @@ export default class MultiInviter { 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)); + const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND', 'RIOT.USER_NOT_FOUND']; + const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode)); - if (reinvitableUsers.length > 0) { - const retryInvites = () => { - const promises = reinvitableUsers.map(u => this._doInvite(u, true)); + if (unknownProfileUsers.length > 0) { + const inviteUnknowns = () => { + const promises = unknownProfileUsers.map(u => this._doInvite(u, true)); Promise.all(promises).then(() => this.deferred.resolve(this.completionStates)); }; - if (SettingsStore.getValue("alwaysRetryInvites", this.roomId)) { - retryInvites(); + if (SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) { + inviteUnknowns(); return; } - const RetryInvitesDialog = sdk.getComponent("dialogs.RetryInvitesDialog"); + const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog"); console.log("Showing failed to invite dialog..."); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', RetryInvitesDialog, { - failedInvites: this.errors, - onTryAgain: () => retryInvites(), + Modal.createTrackedDialog('Failed to invite the following users to the room', '', AskInviteAnywayDialog, { + unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}), + onInviteAnyways: () => inviteUnknowns(), onGiveUp: () => { // Fake all the completion states because we already warned the user - for (const addr of Object.keys(this.completionStates)) { + for (const addr of unknownProfileUsers) { this.completionStates[addr] = 'invited'; } this.deferred.resolve(this.completionStates); @@ -223,7 +230,6 @@ export default class MultiInviter { } 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,