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?")}
+
+ { errorList }
+
+
+
+
+
+
+
+
+
+ );
+ },
+});
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));
}
}