From 54b3638a85abbfff0b8fb119cfc17a6429b0a9b1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 17:11:49 +0100 Subject: [PATCH 01/11] Better support for inviting multiple people Pasting a list of addresses into the box will now pop up a dialog to confirm and show you the success / failure state of each address. It will also not die if it gets rate limited. Fixes https://github.com/vector-im/vector-web/issues/1713 --- src/Invite.js | 45 ++++ src/component-index.js | 1 + .../views/dialogs/MultiInviteDialog.js | 199 ++++++++++++++++++ src/components/views/rooms/MemberList.js | 115 +++++----- 4 files changed, 303 insertions(+), 57 deletions(-) create mode 100644 src/Invite.js create mode 100644 src/components/views/dialogs/MultiInviteDialog.js diff --git a/src/Invite.js b/src/Invite.js new file mode 100644 index 00000000000..3b52d6a1f46 --- /dev/null +++ b/src/Invite.js @@ -0,0 +1,45 @@ +/* +Copyright 2016 OpenMarket 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 MatrixClientPeg from './MatrixClientPeg'; + +const emailRegex = /^\S+@\S+\.\S+$/; + +export function getAddressType(inputText) { + const isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText); + const isMatrixId = inputText[0] === '@' && inputText.indexOf(":") > 0; + + // sanity check the input for user IDs + if (isEmailAddress) { + return 'email'; + } else if (isMatrixId) { + return 'mx'; + } else { + return null; + } +} + +export function inviteToRoom(roomId, addr) { + const addrType = getAddressType(addr); + + if (addrType == 'email') { + return MatrixClientPeg.get().inviteByEmail(roomId, addr); + } else if (addrType == 'mx') { + return MatrixClientPeg.get().invite(roomId, addr); + } else { + throw new Error('Unsupported address'); + } +} diff --git a/src/component-index.js b/src/component-index.js index 97f8882b82b..d61192de332 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -48,6 +48,7 @@ module.exports.components['views.create_room.RoomAlias'] = require('./components module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog'); module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog'); module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); +module.exports.components['views.dialogs.MultiInviteDialog'] = require('./components/views/dialogs/MultiInviteDialog'); module.exports.components['views.dialogs.NeedToRegisterDialog'] = require('./components/views/dialogs/NeedToRegisterDialog'); module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog'); module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog'); diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js new file mode 100644 index 00000000000..0d487c32c4d --- /dev/null +++ b/src/components/views/dialogs/MultiInviteDialog.js @@ -0,0 +1,199 @@ +/* +Copyright 2016 OpenMarket 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 {getAddressType, inviteToRoom} from '../../../Invite'; +import sdk from '../../../index'; + +export default class MultiInviteDialog extends React.Component { + constructor(props, context) { + super(props, context); + + this._onCancel = this._onCancel.bind(this); + this._startInviting = this._startInviting.bind(this); + + this.state = { + busy: false, + completionStates: [], // State of each address (invited or error) + errorTexts: [], // Textual error per address + done: false, + }; + for (let i = 0; i < this.props.inputs.length; ++i) { + const input = this.props.inputs[i]; + if (getAddressType(input) === null) { + this.state.completionStates[i] = 'error'; + this.state.errorTexts[i] = 'Unrecognised address'; + } + } + } + + _onCancel() { + this.props.onFinished(false); + } + + _startInviting() { + this.setState({ + completionStates: [], + busy: true, + done: false, + }); + this._inviteMore(0); + } + + _inviteMore(nextIndex) { + if (nextIndex == this.props.inputs.length) { + this.setState({ + busy: false, + done: true, + }); + return; + } + + const input = this.props.inputs[nextIndex]; + + // don't try to invite it if it's an invalid address + // (it will already be marked as an error though, + // so no need top do so again + if (getAddressType(input) === null) { + this._inviteMore(nextIndex + 1); + return; + } + + // don't re-invite (there's no way in the UI to do this, but + // for sanity's sake) + if (this.state.completionStates[nextIndex] == 'invited') { + this._inviteMore(nextIndex + 1); + return; + } + + inviteToRoom(this.props.roomId, input).then(() => { + this.setState((s) => { + s.completionStates[nextIndex] = 'invited' + return s; + }); + this._inviteMore(nextIndex + 1); + }, (err) => { + let errorText; + let fatal = false; + if (err.errcode == 'M_FORBIDDEN') { + fatal = true; + errorText = '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 { + errorText = 'Unknown server error'; + } + this.setState((s) => { + s.completionStates[nextIndex] = 'error'; + s.errorTexts[nextIndex] = errorText; + s.busy = !fatal; + s.done = fatal; + return s; + }); + if (!fatal) { + this._inviteMore(nextIndex + 1); + } + }); + } + + _getProgressIndicator() { + const numErrors = this.state.completionStates.filter((s) => { + return s == 'error'; + }).length; + let errorText; + if (numErrors > 0) { + const plural = numErrors > 1 ? 's' : ''; + errorText = ({numErrors} error{plural}) + } + return + {this.state.completionStates.length} / {this.props.inputs.length} {errorText} + ; + } + + render() { + const Spinner = sdk.getComponent("elements.Spinner"); + const inviteTiles = []; + + for (let i = 0; i < this.props.inputs.length; ++i) { + const input = this.props.inputs[i]; + let statusClass = ''; + let statusElement; + if (this.state.completionStates[i] == 'error') { + statusClass = 'error'; + statusElement =

{this.state.errorTexts[i]}

; + } else if (this.state.completionStates[i] == 'invited') { + statusClass = 'invited'; + } + inviteTiles.push( +
  • +

    {input}

    + {statusElement} +
  • + ); + } + + let controls = []; + if (this.state.busy) { + controls.push(); + controls.push({this._getProgressIndicator()}); + } else if (this.state.done) { + controls.push( + + ); + controls.push({this._getProgressIndicator()}); + } else { + controls.push( + ); + controls.push(); + } + + return ( +
    +
    + Inviting {this.props.inputs.length} People +
    +
    +
      + {inviteTiles} +
    +
    +
    + {controls} +
    +
    + ); + } +} + +MultiInviteDialog.propTypes = { + onFinished: React.PropTypes.func.isRequired, +}; diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 5536aeddd68..a6ede6a61c5 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -24,6 +24,7 @@ var sdk = require('../../../index'); var GeminiScrollbar = require('react-gemini-scrollbar'); var rate_limited_func = require('../../../ratelimitedfunc'); var CallHandler = require("../../../CallHandler"); +var Invite = require("../../../Invite"); var INITIAL_LOAD_NUM_MEMBERS = 30; var SHARE_HISTORY_WARNING = @@ -191,22 +192,20 @@ module.exports = React.createClass({ // email addresses and user IDs do not allow space, comma, semicolon so split // on them for bulk inviting. - var separators =[ ";", " ", "," ]; - for (var i = 0; i < separators.length; i++) { - if (inputText.indexOf(separators[i]) >= 0) { - var inputs = inputText.split(separators[i]); - inputs.forEach(function(input) { - self.onInvite(input); - }); - return; + // '+' here will treat multiple consecutive separators as one separator, so + // ', ' separators will also do the right thing. + const inputs = inputText.split(/[, ;]+/).filter((x) => { + return x.trim().length > 0; + }); + + let validInputs = 0; + for (const input of inputs) { + if (Invite.getAddressType(input) != null) { + ++validInputs; } } - var isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText); - - // sanity check the input for user IDs - if (!isEmailAddress && (inputText[0] !== '@' || inputText.indexOf(":") === -1)) { - console.error("Bad ID to invite: %s", inputText); + if (validInputs == 0) { Modal.createDialog(ErrorDialog, { title: "Invite Error", description: "Malformed ID. Should be an email address or a Matrix ID like '@localpart:domain'" @@ -243,53 +242,55 @@ module.exports = React.createClass({ inviteWarningDefer.resolve(); } - var promise = inviteWarningDefer.promise; - if (isEmailAddress) { - promise = promise.then(function() { - return MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText); - }); - } - else { - promise = promise.then(function() { - return MatrixClientPeg.get().invite(self.props.roomId, inputText); - }); - } - - self.setState({ - inviting: true - }); - console.log( - "Invite %s to %s - isEmail=%s", inputText, this.props.roomId, isEmailAddress - ); - promise.then(function(res) { - console.log("Invited %s", inputText); - }, function(err) { - if (err !== null) { - console.error("Failed to invite: %s", JSON.stringify(err)); - if (err.errcode == 'M_FORBIDDEN') { - Modal.createDialog(ErrorDialog, { - title: "Unable to Invite", - description: "You do not have permission to invite people to this room." - }); - } else { - Modal.createDialog(ErrorDialog, { - title: "Server error whilst inviting", - description: err.message - }); + const promise = inviteWarningDefer.promise; + + if (inputs.length == 1) { + // for a single address, we just send the invite + promise.then(() => { + return Invite.inviteToRoom(self.props.roomId, inputs[0]); + }).catch((err) => { + if (err !== null) { + console.error("Failed to invite: %s", JSON.stringify(err)); + if (err.errcode == 'M_FORBIDDEN') { + Modal.createDialog(ErrorDialog, { + title: "Unable to Invite", + description: "You do not have permission to invite people to this room." + }); + } else { + Modal.createDialog(ErrorDialog, { + title: "Server error whilst inviting", + description: err.message + }); + } } - } - }).finally(function() { + self.setState({ + inviting: false + }); + }).finally(() => { + self.setState({ + inviting: false + }); + // XXX: hacky focus on the invite box + setTimeout(function() { + var inviteBox = document.getElementById("mx_SearchableEntityList_query"); + if (inviteBox) { + inviteBox.focus(); + } + }, 0); + }).done(); self.setState({ - inviting: false + inviting: true }); - // XXX: hacky focus on the invite box - setTimeout(function() { - var inviteBox = document.getElementById("mx_SearchableEntityList_query"); - if (inviteBox) { - inviteBox.focus(); - } - }, 0); - }); + } else { + // if there are several, display the confirmation/progress dialog + promise.done(() => { + const MultiInviteDialog = sdk.getComponent('views.dialogs.MultiInviteDialog'); + Modal.createDialog(MultiInviteDialog, { + roomId: this.props.roomId, + inputs: inputs, + }); + }); + } }, getMemberDict: function() { From 4a6a9bd95bed58f8f9bae4658da25ad36167badc Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 18:28:20 +0100 Subject: [PATCH 02/11] Remove redundant setState Duplicated in `finally` --- src/components/views/rooms/MemberList.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index a6ede6a61c5..51635b3c39e 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -263,9 +263,6 @@ module.exports = React.createClass({ }); } } - self.setState({ - inviting: false - }); }).finally(() => { self.setState({ inviting: false From 020e4e14dfdd1b854cf7fd0b896ad47d6a7ab39e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 18:32:42 +0100 Subject: [PATCH 03/11] Factor out single-invite --- src/components/views/rooms/MemberList.js | 64 +++++++++++++----------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 51635b3c39e..d64e1d8265f 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -176,6 +176,39 @@ module.exports = React.createClass({ }); }, + _doInvite(address) { + Invite.inviteToRoom(self.props.roomId, address).catch((err) => { + if (err !== null) { + console.error("Failed to invite: %s", JSON.stringify(err)); + if (err.errcode == 'M_FORBIDDEN') { + Modal.createDialog(ErrorDialog, { + title: "Unable to Invite", + description: "You do not have permission to invite people to this room." + }); + } else { + Modal.createDialog(ErrorDialog, { + title: "Server error whilst inviting", + description: err.message + }); + } + } + }).finally(() => { + self.setState({ + inviting: false + }); + // XXX: hacky focus on the invite box + setTimeout(function() { + var inviteBox = document.getElementById("mx_SearchableEntityList_query"); + if (inviteBox) { + inviteBox.focus(); + } + }, 0); + }).done(); + self.setState({ + inviting: true + }); + } + onInvite: function(inputText) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); @@ -247,36 +280,7 @@ module.exports = React.createClass({ if (inputs.length == 1) { // for a single address, we just send the invite promise.then(() => { - return Invite.inviteToRoom(self.props.roomId, inputs[0]); - }).catch((err) => { - if (err !== null) { - console.error("Failed to invite: %s", JSON.stringify(err)); - if (err.errcode == 'M_FORBIDDEN') { - Modal.createDialog(ErrorDialog, { - title: "Unable to Invite", - description: "You do not have permission to invite people to this room." - }); - } else { - Modal.createDialog(ErrorDialog, { - title: "Server error whilst inviting", - description: err.message - }); - } - } - }).finally(() => { - self.setState({ - inviting: false - }); - // XXX: hacky focus on the invite box - setTimeout(function() { - var inviteBox = document.getElementById("mx_SearchableEntityList_query"); - if (inviteBox) { - inviteBox.focus(); - } - }, 0); - }).done(); - self.setState({ - inviting: true + this.doInvite(inputs[0]); }); } else { // if there are several, display the confirmation/progress dialog From b9f0b72ae1fcc991e18f9e8e0e116cf15779c269 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 18:35:59 +0100 Subject: [PATCH 04/11] Missing proptypes --- src/components/views/dialogs/MultiInviteDialog.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js index 0d487c32c4d..ac0466c7a00 100644 --- a/src/components/views/dialogs/MultiInviteDialog.js +++ b/src/components/views/dialogs/MultiInviteDialog.js @@ -196,4 +196,6 @@ export default class MultiInviteDialog extends React.Component { MultiInviteDialog.propTypes = { onFinished: React.PropTypes.func.isRequired, + inputs: React.PropTypes.array.isRequired, + roomId: React.PropTypes.string.isRequired, }; From 6b3b2e30fe4085fe676a87c3b629b0bfd301437d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 18:36:33 +0100 Subject: [PATCH 05/11] Don't reset completionStates We now set them in the constructor --- src/components/views/dialogs/MultiInviteDialog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js index ac0466c7a00..d91bb0c6503 100644 --- a/src/components/views/dialogs/MultiInviteDialog.js +++ b/src/components/views/dialogs/MultiInviteDialog.js @@ -47,7 +47,6 @@ export default class MultiInviteDialog extends React.Component { _startInviting() { this.setState({ - completionStates: [], busy: true, done: false, }); From fa498eb8c6e4a19c34003bf90dc2518daf4a93c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 18:37:21 +0100 Subject: [PATCH 06/11] Comment typo --- src/components/views/dialogs/MultiInviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js index d91bb0c6503..3ed0bb89c94 100644 --- a/src/components/views/dialogs/MultiInviteDialog.js +++ b/src/components/views/dialogs/MultiInviteDialog.js @@ -66,7 +66,7 @@ export default class MultiInviteDialog extends React.Component { // don't try to invite it if it's an invalid address // (it will already be marked as an error though, - // so no need top do so again + // so no need to do so again) if (getAddressType(input) === null) { this._inviteMore(nextIndex + 1); return; From d6f7358f819c399b71b5fefa416beaa2441fc23c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 18:50:56 +0100 Subject: [PATCH 07/11] Make completionStates an object So that we can sensibly track the number completed by taking the length of it. --- src/components/views/dialogs/MultiInviteDialog.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js index 3ed0bb89c94..1e8b8bc96a6 100644 --- a/src/components/views/dialogs/MultiInviteDialog.js +++ b/src/components/views/dialogs/MultiInviteDialog.js @@ -28,8 +28,8 @@ export default class MultiInviteDialog extends React.Component { this.state = { busy: false, - completionStates: [], // State of each address (invited or error) - errorTexts: [], // Textual error per address + completionStates: {}, // State of each address (invited or error) + errorTexts: {}, // Textual error per address done: false, }; for (let i = 0; i < this.props.inputs.length; ++i) { @@ -114,16 +114,19 @@ export default class MultiInviteDialog extends React.Component { } _getProgressIndicator() { - const numErrors = this.state.completionStates.filter((s) => { - return s == 'error'; - }).length; + let numErrors = 0; + for (const k of Object.keys(this.state.completionStates)) { + if (this.state.completionStates[k] == 'error') { + ++numErrors; + } + } let errorText; if (numErrors > 0) { const plural = numErrors > 1 ? 's' : ''; errorText = ({numErrors} error{plural}) } return - {this.state.completionStates.length} / {this.props.inputs.length} {errorText} + {Object.keys(this.state.completionStates).length} / {this.props.inputs.length} {errorText} ; } From cab95f43ff4048ef8e37f91a86ee378df5d950f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Aug 2016 18:58:36 +0100 Subject: [PATCH 08/11] Show cancel button whilst inviting is in progress And make it actually cancel the process. --- src/components/views/dialogs/MultiInviteDialog.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js index 1e8b8bc96a6..62b66c6de94 100644 --- a/src/components/views/dialogs/MultiInviteDialog.js +++ b/src/components/views/dialogs/MultiInviteDialog.js @@ -25,6 +25,7 @@ export default class MultiInviteDialog extends React.Component { this._onCancel = this._onCancel.bind(this); this._startInviting = this._startInviting.bind(this); + this._canceled = false; this.state = { busy: false, @@ -41,7 +42,12 @@ export default class MultiInviteDialog extends React.Component { } } + componentWillUnmount() { + this._unmounted = true; + } + _onCancel() { + this._canceled = true; this.props.onFinished(false); } @@ -54,6 +60,10 @@ export default class MultiInviteDialog extends React.Component { } _inviteMore(nextIndex) { + if (this._canceled) { + return; + } + if (nextIndex == this.props.inputs.length) { this.setState({ busy: false, @@ -80,12 +90,16 @@ export default class MultiInviteDialog extends React.Component { } inviteToRoom(this.props.roomId, input).then(() => { + if (this._unmounted) { return; } + this.setState((s) => { s.completionStates[nextIndex] = 'invited' return s; }); this._inviteMore(nextIndex + 1); }, (err) => { + if (this._unmounted) { return; } + let errorText; let fatal = false; if (err.errcode == 'M_FORBIDDEN') { @@ -155,6 +169,7 @@ export default class MultiInviteDialog extends React.Component { let controls = []; if (this.state.busy) { controls.push(); + controls.push(); controls.push({this._getProgressIndicator()}); } else if (this.state.done) { controls.push( From a58a6275e595518f576335820cadc4691f560248 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 11 Aug 2016 10:06:22 +0100 Subject: [PATCH 09/11] Just use _canceled Instead of both that and _unmounted --- src/components/views/dialogs/MultiInviteDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js index 62b66c6de94..a8d7aec495a 100644 --- a/src/components/views/dialogs/MultiInviteDialog.js +++ b/src/components/views/dialogs/MultiInviteDialog.js @@ -43,7 +43,7 @@ export default class MultiInviteDialog extends React.Component { } componentWillUnmount() { - this._unmounted = true; + this._canceled = true; } _onCancel() { @@ -90,7 +90,7 @@ export default class MultiInviteDialog extends React.Component { } inviteToRoom(this.props.roomId, input).then(() => { - if (this._unmounted) { return; } + if (this._canceled) { return; } this.setState((s) => { s.completionStates[nextIndex] = 'invited' @@ -98,7 +98,7 @@ export default class MultiInviteDialog extends React.Component { }); this._inviteMore(nextIndex + 1); }, (err) => { - if (this._unmounted) { return; } + if (this._canceled) { return; } let errorText; let fatal = false; From 806477dccee4b768b793fcfe678f16a20ce82e2b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 11 Aug 2016 10:08:14 +0100 Subject: [PATCH 10/11] s/then/done/ --- src/components/views/rooms/MemberList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index d64e1d8265f..f7c80506d44 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -279,7 +279,7 @@ module.exports = React.createClass({ if (inputs.length == 1) { // for a single address, we just send the invite - promise.then(() => { + promise.done(() => { this.doInvite(inputs[0]); }); } else { From f42d619e7dae7a992a50d01bf0d0c48c997c5aa8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 11 Aug 2016 12:30:37 +0100 Subject: [PATCH 11/11] Missing comma --- src/components/views/rooms/MemberList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index f7c80506d44..2451026bcfe 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -207,7 +207,7 @@ module.exports = React.createClass({ self.setState({ inviting: true }); - } + }, onInvite: function(inputText) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");