mirror of https://github.com/vector-im/riot-web
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/1797pull/21833/head
parent
c11d0bdf0c
commit
5333114d7b
|
@ -86,6 +86,7 @@ const SIMPLE_SETTINGS = [
|
||||||
{ id: "pinMentionedRooms" },
|
{ id: "pinMentionedRooms" },
|
||||||
{ id: "pinUnreadRooms" },
|
{ id: "pinUnreadRooms" },
|
||||||
{ id: "showDeveloperTools" },
|
{ id: "showDeveloperTools" },
|
||||||
|
{ id: "alwaysRetryInvites" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// These settings must be defined in SettingsStore
|
// These settings must be defined in SettingsStore
|
||||||
|
|
|
@ -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 => <p>{address}: {this.props.failedInvites[address].errorText}</p>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_RetryInvitesDialog'
|
||||||
|
onFinished={this._onGiveUpClicked}
|
||||||
|
title={_t('Failed to invite the following users')}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
|
>
|
||||||
|
<div id='mx_Dialog_content'>
|
||||||
|
{ errorList }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this._onGiveUpClicked}>
|
||||||
|
{ _t('Close') }
|
||||||
|
</button>
|
||||||
|
<button onClick={this._onTryAgainNeverWarnClicked}>
|
||||||
|
{ _t('Try again and never warn me again') }
|
||||||
|
</button>
|
||||||
|
<button onClick={this._onTryAgainClicked} autoFocus="true">
|
||||||
|
{ _t('Try again') }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -222,8 +222,10 @@
|
||||||
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
|
"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",
|
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
||||||
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
"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.",
|
"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 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",
|
"Unknown server error": "Unknown server error",
|
||||||
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
|
"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",
|
"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",
|
"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",
|
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
|
||||||
"Show empty room list headings": "Show empty room list headings",
|
"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",
|
"Show developer tools": "Show developer tools",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
|
@ -965,6 +968,9 @@
|
||||||
"Clear cache and resync": "Clear cache and resync",
|
"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!",
|
"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",
|
"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",
|
"Failed to upgrade room": "Failed to upgrade room",
|
||||||
"The room upgrade could not be completed": "The room upgrade could not be completed",
|
"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",
|
"Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s",
|
||||||
|
|
|
@ -317,6 +317,11 @@ export const SETTINGS = {
|
||||||
displayName: _td('Show empty room list headings'),
|
displayName: _td('Show empty room list headings'),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
"alwaysRetryInvites": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
displayName: _td('Always retry invites for unknown users'),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"showDeveloperTools": {
|
"showDeveloperTools": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Show developer tools'),
|
displayName: _td('Show developer tools'),
|
||||||
|
|
|
@ -15,11 +15,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import {getAddressType} from '../UserAddress';
|
import {getAddressType} from '../UserAddress';
|
||||||
import GroupStore from '../stores/GroupStore';
|
import GroupStore from '../stores/GroupStore';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import {_t} from "../languageHandler";
|
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
|
* 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.addrs = [];
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.completionStates = {}; // State of each address (invited or error)
|
this.completionStates = {}; // State of each address (invited or error)
|
||||||
this.errorTexts = {}; // Textual error per address
|
this.errors = {}; // { address: {errorText, errcode} }
|
||||||
this.deferred = null;
|
this.deferred = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +65,10 @@ export default class MultiInviter {
|
||||||
for (const addr of this.addrs) {
|
for (const addr of this.addrs) {
|
||||||
if (getAddressType(addr) === null) {
|
if (getAddressType(addr) === null) {
|
||||||
this.completionStates[addr] = 'error';
|
this.completionStates[addr] = 'error';
|
||||||
this.errorTexts[addr] = 'Unrecognised address';
|
this.errors[addr] = {
|
||||||
|
errcode: 'M_INVALID',
|
||||||
|
errorText: _t('Unrecognised address'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.deferred = Promise.defer();
|
this.deferred = Promise.defer();
|
||||||
|
@ -85,18 +92,23 @@ export default class MultiInviter {
|
||||||
}
|
}
|
||||||
|
|
||||||
getErrorText(addr) {
|
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);
|
const addrType = getAddressType(addr);
|
||||||
|
|
||||||
if (addrType === 'email') {
|
if (addrType === 'email') {
|
||||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||||
} else if (addrType === 'mx-user-id') {
|
} else if (addrType === 'mx-user-id') {
|
||||||
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
|
if (!ignoreProfile && !SettingsStore.getValue("alwaysRetryInvites", this.roomId)) {
|
||||||
if (!profile) {
|
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
|
||||||
return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."});
|
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);
|
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) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex === this.addrs.length) {
|
if (nextIndex === this.addrs.length) {
|
||||||
this.busy = false;
|
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);
|
this.deferred.resolve(this.completionStates);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addr = this.addrs[nextIndex];
|
const addr = this.addrs[nextIndex];
|
||||||
|
console.log(`Inviting ${addr}`);
|
||||||
|
|
||||||
// don't try to invite it if it's an invalid address
|
// don't try to invite it if it's an invalid address
|
||||||
// (it will already be marked as an error though,
|
// (it will already be marked as an error though,
|
||||||
|
@ -134,48 +240,8 @@ export default class MultiInviter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let doInvite;
|
this._doInvite(addr, ignoreProfile).then(() => {
|
||||||
if (this.groupId !== null) {
|
this._inviteMore(nextIndex + 1, ignoreProfile);
|
||||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, addr);
|
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue