diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b9dbe345c5..37f60c47bf 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: "alwaysInviteUnknownUsers" }, ]; // These settings must be defined in SettingsStore diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js new file mode 100644 index 0000000000..5c61c3a694 --- /dev/null +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -0,0 +1,81 @@ +/* +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: { + unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] + onInviteAnyways: PropTypes.func.isRequired, + onGiveUp: PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, + }, + + _onInviteClicked: function() { + this.props.onInviteAnyways(); + this.props.onFinished(true); + }, + + _onInviteNeverWarnClicked: function() { + SettingsStore.setValue("alwaysInviteUnknownUsers", null, SettingLevel.ACCOUNT, true); + this.props.onInviteAnyways(); + this.props.onFinished(true); + }, + + _onGiveUpClicked: function() { + this.props.onGiveUp(); + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + const errorList = this.props.unknownProfileUsers + .map(address =>
  • {address.userId}: {address.errorText}
  • ); + + return ( + +
    +

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

    + +
    + +
    + + + +
    +
    + ); + }, +}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef659bf566..4f8674db2f 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 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", @@ -881,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!", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 1cac8559d1..a007f78c1f 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, }, + "alwaysInviteUnknownUsers": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Always invite users which may not exist'), + 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..b5f4f960a9 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,28 @@ 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("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." + }; + } } return MatrixClientPeg.get().invite(roomId, addr); @@ -105,14 +122,109 @@ export default class MultiInviter { } } + _doInvite(address, ignoreProfile) { + return new Promise((resolve, reject) => { + console.log(`Inviting ${address}`); - _inviteMore(nextIndex) { + let doInvite; + if (this.groupId !== null) { + doInvite = GroupStore.inviteUserToGroup(this.groupId, address); + } else { + doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile); + } + + 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', '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_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 - inviting anyways automatically`); + 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 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 (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("alwaysInviteUnknownUsers", this.roomId)) { + inviteUnknowns(); + return; + } + + const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog"); + console.log("Showing failed to invite dialog..."); + 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 unknownProfileUsers) { + this.completionStates[addr] = 'invited'; + } + this.deferred.resolve(this.completionStates); + }, + }); + return; + } + } this.deferred.resolve(this.completionStates); return; } @@ -134,48 +246,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)); } }