Merge pull request #469 from matrix-org/wmwragg/chat-multi-invite
Wmwragg/chat multi invitepull/21833/head
						commit
						8508e006f0
					
				| 
						 | 
					@ -15,6 +15,7 @@ limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import MatrixClientPeg from './MatrixClientPeg';
 | 
					import MatrixClientPeg from './MatrixClientPeg';
 | 
				
			||||||
 | 
					import MultiInviter from './utils/MultiInviter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emailRegex = /^\S+@\S+\.\S+$/;
 | 
					const emailRegex = /^\S+@\S+\.\S+$/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,3 +44,40 @@ export function inviteToRoom(roomId, addr) {
 | 
				
			||||||
        throw new Error('Unsupported address');
 | 
					        throw new Error('Unsupported address');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Invites multiple addresses to a room
 | 
				
			||||||
 | 
					 * Simpler interface to utils/MultiInviter but with
 | 
				
			||||||
 | 
					 * no option to cancel.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {roomId} The ID of the room to invite to
 | 
				
			||||||
 | 
					 * @param {array} Array of strings of addresses to invite. May be matrix IDs or 3pids.
 | 
				
			||||||
 | 
					 * @returns Promise
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function inviteMultipleToRoom(roomId, addrs) {
 | 
				
			||||||
 | 
					    this.inviter = new MultiInviter(roomId);
 | 
				
			||||||
 | 
					    return this.inviter.invite(addrs);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Checks is the supplied address is valid
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {addr} The mx userId or email address to check
 | 
				
			||||||
 | 
					 * @returns true, false, or null for unsure
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isValidAddress(addr) {
 | 
				
			||||||
 | 
					    // Check if the addr is a valid type
 | 
				
			||||||
 | 
					    var addrType = this.getAddressType(addr);
 | 
				
			||||||
 | 
					    if (addrType === "mx") {
 | 
				
			||||||
 | 
					        let user = MatrixClientPeg.get().getUser(addr);
 | 
				
			||||||
 | 
					        if (user) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else if (addrType === "email") {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,6 +57,7 @@ module.exports.components['views.dialogs.NeedToRegisterDialog'] = require('./com
 | 
				
			||||||
module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');
 | 
					module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');
 | 
				
			||||||
module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog');
 | 
					module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog');
 | 
				
			||||||
module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
 | 
					module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
 | 
				
			||||||
 | 
					module.exports.components['views.elements.AddressSelector'] = require('./components/views/elements/AddressSelector');
 | 
				
			||||||
module.exports.components['views.elements.AddressTile'] = require('./components/views/elements/AddressTile');
 | 
					module.exports.components['views.elements.AddressTile'] = require('./components/views/elements/AddressTile');
 | 
				
			||||||
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
 | 
					module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
 | 
				
			||||||
module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer');
 | 
					module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -385,6 +385,9 @@ module.exports = React.createClass({
 | 
				
			||||||
            case 'view_create_chat':
 | 
					            case 'view_create_chat':
 | 
				
			||||||
                this._createChat();
 | 
					                this._createChat();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'view_invite':
 | 
				
			||||||
 | 
					                this._invite(payload.roomId);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
            case 'notifier_enabled':
 | 
					            case 'notifier_enabled':
 | 
				
			||||||
                this.forceUpdate();
 | 
					                this.forceUpdate();
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
| 
						 | 
					@ -524,7 +527,17 @@ module.exports = React.createClass({
 | 
				
			||||||
    _createChat: function() {
 | 
					    _createChat: function() {
 | 
				
			||||||
        var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
 | 
					        var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
 | 
				
			||||||
        Modal.createDialog(ChatInviteDialog, {
 | 
					        Modal.createDialog(ChatInviteDialog, {
 | 
				
			||||||
            title: "Start a one to one chat",
 | 
					            title: "Start a new chat",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _invite: function(roomId) {
 | 
				
			||||||
 | 
					        var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
 | 
				
			||||||
 | 
					        Modal.createDialog(ChatInviteDialog, {
 | 
				
			||||||
 | 
					            title: "Invite new room members",
 | 
				
			||||||
 | 
					            button: "Send Invites",
 | 
				
			||||||
 | 
					            description: "Who would you like to add to this room?",
 | 
				
			||||||
 | 
					            roomId: roomId,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,7 @@ module.exports = React.createClass({
 | 
				
			||||||
        ]),
 | 
					        ]),
 | 
				
			||||||
        value: React.PropTypes.string,
 | 
					        value: React.PropTypes.string,
 | 
				
			||||||
        placeholder: React.PropTypes.string,
 | 
					        placeholder: React.PropTypes.string,
 | 
				
			||||||
 | 
					        roomId: React.PropTypes.string,
 | 
				
			||||||
        button: React.PropTypes.string,
 | 
					        button: React.PropTypes.string,
 | 
				
			||||||
        focus: React.PropTypes.bool,
 | 
					        focus: React.PropTypes.bool,
 | 
				
			||||||
        onFinished: React.PropTypes.func.isRequired
 | 
					        onFinished: React.PropTypes.func.isRequired
 | 
				
			||||||
| 
						 | 
					@ -55,11 +56,9 @@ module.exports = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getInitialState: function() {
 | 
					    getInitialState: function() {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            user: null,
 | 
					            error: false,
 | 
				
			||||||
 | 
					            inviteList: [],
 | 
				
			||||||
            queryList: [],
 | 
					            queryList: [],
 | 
				
			||||||
            addressSelected: false,
 | 
					 | 
				
			||||||
            selected: 0,
 | 
					 | 
				
			||||||
            hover: false,
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,44 +70,29 @@ module.exports = React.createClass({
 | 
				
			||||||
        this._updateUserList();
 | 
					        this._updateUserList();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    componentDidUpdate: function() {
 | 
					    onButtonClick: function() {
 | 
				
			||||||
        // As the user scrolls with the arrow keys keep the selected item
 | 
					        if (this.state.inviteList.length > 0) {
 | 
				
			||||||
        // at the top of the window.
 | 
					            if (this._isDmChat()) {
 | 
				
			||||||
        if (this.scrollElement && !this.state.hover) {
 | 
					                // Direct Message chat
 | 
				
			||||||
            var elementHeight = this.queryListElement.getBoundingClientRect().height;
 | 
					                var room = this._getDirectMessageRoom(this.state.inviteList[0]);
 | 
				
			||||||
            this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
 | 
					                if (room) {
 | 
				
			||||||
        }
 | 
					                    // A Direct Message room already exists for this user and you
 | 
				
			||||||
    },
 | 
					                    // so go straight to that room
 | 
				
			||||||
 | 
					                    dis.dispatch({
 | 
				
			||||||
    onStartChat: function() {
 | 
					                        action: 'view_room',
 | 
				
			||||||
        var addr;
 | 
					                        room_id: room.roomId,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
        // Either an address tile was created, or text input is being used
 | 
					                    this.props.onFinished(true, this.state.inviteList[0]);
 | 
				
			||||||
        if (this.state.user) {
 | 
					                } else {
 | 
				
			||||||
            addr = this.state.user.userId;
 | 
					                    this._startChat(this.state.inviteList);
 | 
				
			||||||
        } else {
 | 
					                }
 | 
				
			||||||
            addr = this.refs.textinput.value;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Check if the addr is a valid type
 | 
					 | 
				
			||||||
        if (Invite.getAddressType(addr) === "mx") {
 | 
					 | 
				
			||||||
            var room = this._getDirectMessageRoom(addr);
 | 
					 | 
				
			||||||
            if (room) {
 | 
					 | 
				
			||||||
                // A Direct Message room already exists for this user and you
 | 
					 | 
				
			||||||
                // so go straight to that room
 | 
					 | 
				
			||||||
                dis.dispatch({
 | 
					 | 
				
			||||||
                    action: 'view_room',
 | 
					 | 
				
			||||||
                    room_id: room.roomId,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
                this.props.onFinished(true, addr);
 | 
					 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                this._startChat(addr);
 | 
					                // Multi invite chat
 | 
				
			||||||
 | 
					                this._startChat(this.state.inviteList);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else if (Invite.getAddressType(addr) === "email") {
 | 
					 | 
				
			||||||
            this._startChat(addr);
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // Nothing to do, so focus back on the textinput
 | 
					            // No addresses supplied
 | 
				
			||||||
            this.refs.textinput.focus();
 | 
					            this.setState({ error: true });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -124,31 +108,28 @@ module.exports = React.createClass({
 | 
				
			||||||
        } else if (e.keyCode === 38) { // up arrow
 | 
					        } else if (e.keyCode === 38) { // up arrow
 | 
				
			||||||
            e.stopPropagation();
 | 
					            e.stopPropagation();
 | 
				
			||||||
            e.preventDefault();
 | 
					            e.preventDefault();
 | 
				
			||||||
            if (this.state.selected > 0) {
 | 
					            this.addressSelector.onKeyUpArrow();
 | 
				
			||||||
                this.setState({
 | 
					 | 
				
			||||||
                    selected: this.state.selected - 1,
 | 
					 | 
				
			||||||
                    hover : false,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else if (e.keyCode === 40) { // down arrow
 | 
					        } else if (e.keyCode === 40) { // down arrow
 | 
				
			||||||
            e.stopPropagation();
 | 
					            e.stopPropagation();
 | 
				
			||||||
            e.preventDefault();
 | 
					            e.preventDefault();
 | 
				
			||||||
            if (this.state.selected < this._maxSelected(this.state.queryList)) {
 | 
					            this.addressSelector.onKeyDownArrow();
 | 
				
			||||||
                this.setState({
 | 
					 | 
				
			||||||
                    selected: this.state.selected + 1,
 | 
					 | 
				
			||||||
                    hover : false,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else if (e.keyCode === 13) { // enter
 | 
					        } else if (e.keyCode === 13) { // enter
 | 
				
			||||||
            e.stopPropagation();
 | 
					            e.stopPropagation();
 | 
				
			||||||
            e.preventDefault();
 | 
					            e.preventDefault();
 | 
				
			||||||
            if (this.state.queryList.length > 0) {
 | 
					            this.addressSelector.onKeyReturn();
 | 
				
			||||||
 | 
					        } else if (e.keyCode === 32 || e.keyCode === 188) { // space or comma
 | 
				
			||||||
 | 
					            e.stopPropagation();
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					            var check = Invite.isValidAddress(this.refs.textinput.value);
 | 
				
			||||||
 | 
					            if (check === true || check === null) {
 | 
				
			||||||
 | 
					                var inviteList = this.state.inviteList.slice();
 | 
				
			||||||
 | 
					                inviteList.push(this.refs.textinput.value);
 | 
				
			||||||
                this.setState({
 | 
					                this.setState({
 | 
				
			||||||
                    user: this.state.queryList[this.state.selected],
 | 
					                    inviteList: inviteList,
 | 
				
			||||||
                    addressSelected: true,
 | 
					 | 
				
			||||||
                    queryList: [],
 | 
					                    queryList: [],
 | 
				
			||||||
                    hover : false,
 | 
					 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.setState({ error: true });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -164,80 +145,38 @@ module.exports = React.createClass({
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Make sure the selected item isn't outside the list bounds
 | 
					 | 
				
			||||||
        var selected = this.state.selected;
 | 
					 | 
				
			||||||
        var maxSelected = this._maxSelected(queryList);
 | 
					 | 
				
			||||||
        if (selected > maxSelected) {
 | 
					 | 
				
			||||||
            selected = maxSelected;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.setState({
 | 
					        this.setState({
 | 
				
			||||||
            queryList: queryList,
 | 
					            queryList: queryList,
 | 
				
			||||||
            selected: selected,
 | 
					            error: false,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onDismissed: function() {
 | 
					    onDismissed: function(index) {
 | 
				
			||||||
        this.setState({
 | 
					        var self = this;
 | 
				
			||||||
            user: null,
 | 
					        return function() {
 | 
				
			||||||
            addressSelected: false,
 | 
					            var inviteList = self.state.inviteList.slice();
 | 
				
			||||||
            selected: 0,
 | 
					            inviteList.splice(index, 1);
 | 
				
			||||||
            queryList: [],
 | 
					            self.setState({
 | 
				
			||||||
        });
 | 
					                inviteList: inviteList,
 | 
				
			||||||
 | 
					                queryList: [],
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onClick: function(index) {
 | 
					    onClick: function(index) {
 | 
				
			||||||
        var self = this;
 | 
					        var self = this;
 | 
				
			||||||
        return function() {
 | 
					        return function() {
 | 
				
			||||||
            self.setState({
 | 
					            self.onSelected(index);
 | 
				
			||||||
                user: self.state.queryList[index],
 | 
					 | 
				
			||||||
                addressSelected: true,
 | 
					 | 
				
			||||||
                queryList: [],
 | 
					 | 
				
			||||||
                hover: false,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onMouseEnter: function(index) {
 | 
					    onSelected: function(index) {
 | 
				
			||||||
        var self = this;
 | 
					        var inviteList = this.state.inviteList.slice();
 | 
				
			||||||
        return function() {
 | 
					        inviteList.push(this.state.queryList[index].userId);
 | 
				
			||||||
            self.setState({
 | 
					        this.setState({
 | 
				
			||||||
                selected: index,
 | 
					            inviteList: inviteList,
 | 
				
			||||||
                hover: true,
 | 
					            queryList: [],
 | 
				
			||||||
            });
 | 
					        });
 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onMouseLeave: function() {
 | 
					 | 
				
			||||||
        this.setState({ hover : false });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    createQueryListTiles: function() {
 | 
					 | 
				
			||||||
        var self = this;
 | 
					 | 
				
			||||||
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
 | 
					 | 
				
			||||||
        var AddressTile = sdk.getComponent("elements.AddressTile");
 | 
					 | 
				
			||||||
        var maxSelected = this._maxSelected(this.state.queryList);
 | 
					 | 
				
			||||||
        var queryList = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Only create the query elements if there are queries
 | 
					 | 
				
			||||||
        if (this.state.queryList.length > 0) {
 | 
					 | 
				
			||||||
            for (var i = 0; i <= maxSelected; i++) {
 | 
					 | 
				
			||||||
                var classes = classNames({
 | 
					 | 
				
			||||||
                    "mx_ChatInviteDialog_queryListElement": true,
 | 
					 | 
				
			||||||
                    "mx_ChatInviteDialog_selected": this.state.selected === i,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // NOTE: Defaulting to "vector" as the network, until the network backend stuff is done.
 | 
					 | 
				
			||||||
                // Saving the queryListElement so we can use it to work out, in the componentDidUpdate
 | 
					 | 
				
			||||||
                // method, how far to scroll when using the arrow keys
 | 
					 | 
				
			||||||
                queryList.push(
 | 
					 | 
				
			||||||
                    <div className={classes} onClick={this.onClick(i)} onMouseEnter={this.onMouseEnter(i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.queryListElement = ref; }} >
 | 
					 | 
				
			||||||
                        <AddressTile user={this.state.queryList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return queryList;
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _getDirectMessageRoom: function(addr) {
 | 
					    _getDirectMessageRoom: function(addr) {
 | 
				
			||||||
| 
						 | 
					@ -258,21 +197,50 @@ module.exports = React.createClass({
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _startChat: function(addr) {
 | 
					    _startChat: function(addrs) {
 | 
				
			||||||
        // Start the chat
 | 
					        if (this.props.roomId) {
 | 
				
			||||||
        createRoom({dmUserId: addr})
 | 
					            // Invite new user to a room
 | 
				
			||||||
        .catch(function(err) {
 | 
					            Invite.inviteMultipleToRoom(this.props.roomId, addrs)
 | 
				
			||||||
            var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
					            .catch(function(err) {
 | 
				
			||||||
            Modal.createDialog(ErrorDialog, {
 | 
					                var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
				
			||||||
                title: "Failure to invite user",
 | 
					                Modal.createDialog(ErrorDialog, {
 | 
				
			||||||
                description: err.toString()
 | 
					                    title: "Failure to invite user",
 | 
				
			||||||
            });
 | 
					                    description: err.toString()
 | 
				
			||||||
            return null;
 | 
					                });
 | 
				
			||||||
        })
 | 
					                return null;
 | 
				
			||||||
        .done();
 | 
					            })
 | 
				
			||||||
 | 
					            .done();
 | 
				
			||||||
 | 
					        } else if (this._isDmChat()) {
 | 
				
			||||||
 | 
					            // Start the DM chat
 | 
				
			||||||
 | 
					            createRoom({dmUserId: addrs[0]})
 | 
				
			||||||
 | 
					            .catch(function(err) {
 | 
				
			||||||
 | 
					                var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
				
			||||||
 | 
					                Modal.createDialog(ErrorDialog, {
 | 
				
			||||||
 | 
					                    title: "Failure to invite user",
 | 
				
			||||||
 | 
					                    description: err.toString()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                return null;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .done();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Start multi user chat
 | 
				
			||||||
 | 
					            var self = this;
 | 
				
			||||||
 | 
					            createRoom().then(function(roomId) {
 | 
				
			||||||
 | 
					                return Invite.inviteMultipleToRoom(roomId, addrs);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(function(err) {
 | 
				
			||||||
 | 
					                var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
				
			||||||
 | 
					                Modal.createDialog(ErrorDialog, {
 | 
				
			||||||
 | 
					                    title: "Failure to invite user",
 | 
				
			||||||
 | 
					                    description: err.toString()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                return null;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .done();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Close - this will happen before the above, as that is async
 | 
					        // Close - this will happen before the above, as that is async
 | 
				
			||||||
        this.props.onFinished(true, addr);
 | 
					        this.props.onFinished(true, addrs);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _updateUserList: new rate_limited_func(function() {
 | 
					    _updateUserList: new rate_limited_func(function() {
 | 
				
			||||||
| 
						 | 
					@ -280,18 +248,17 @@ module.exports = React.createClass({
 | 
				
			||||||
        this._userList = MatrixClientPeg.get().getUsers();
 | 
					        this._userList = MatrixClientPeg.get().getUsers();
 | 
				
			||||||
    }, 500),
 | 
					    }, 500),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _maxSelected: function(list) {
 | 
					 | 
				
			||||||
        var listSize = list.length === 0 ? 0 : list.length - 1;
 | 
					 | 
				
			||||||
        var maxSelected = listSize > (TRUNCATE_QUERY_LIST - 1) ? (TRUNCATE_QUERY_LIST - 1) : listSize
 | 
					 | 
				
			||||||
        return maxSelected;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // This is the search algorithm for matching users
 | 
					    // This is the search algorithm for matching users
 | 
				
			||||||
    _matches: function(query, user) {
 | 
					    _matches: function(query, user) {
 | 
				
			||||||
        var name = user.displayName.toLowerCase();
 | 
					        var name = user.displayName.toLowerCase();
 | 
				
			||||||
        var uid = user.userId.toLowerCase();
 | 
					        var uid = user.userId.toLowerCase();
 | 
				
			||||||
        query = query.toLowerCase();
 | 
					        query = query.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // dount match any that are already on the invite list
 | 
				
			||||||
 | 
					        if (this._isOnInviteList(uid)) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // direct prefix matches
 | 
					        // direct prefix matches
 | 
				
			||||||
        if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) {
 | 
					        if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) {
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
| 
						 | 
					@ -312,37 +279,63 @@ module.exports = React.createClass({
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _isOnInviteList: function(uid) {
 | 
				
			||||||
 | 
					        for (let i = 0; i < this.state.inviteList.length; i++) {
 | 
				
			||||||
 | 
					            if (this.state.inviteList[i].toLowerCase() === uid) {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _isDmChat: function() {
 | 
				
			||||||
 | 
					        if (this.state.inviteList.length === 1 && Invite.getAddressType(this.state.inviteList[0]) === "mx" && !this.props.roomId) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render: function() {
 | 
					    render: function() {
 | 
				
			||||||
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
 | 
					        var TintableSvg = sdk.getComponent("elements.TintableSvg");
 | 
				
			||||||
 | 
					        var AddressSelector = sdk.getComponent("elements.AddressSelector");
 | 
				
			||||||
        this.scrollElement = null;
 | 
					        this.scrollElement = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var query;
 | 
					        var query = [];
 | 
				
			||||||
        if (this.state.addressSelected) {
 | 
					        // create the invite list
 | 
				
			||||||
 | 
					        if (this.state.inviteList.length > 0) {
 | 
				
			||||||
            var AddressTile = sdk.getComponent("elements.AddressTile");
 | 
					            var AddressTile = sdk.getComponent("elements.AddressTile");
 | 
				
			||||||
            query = (
 | 
					            for (let i = 0; i < this.state.inviteList.length; i++) {
 | 
				
			||||||
                <AddressTile user={this.state.user} canDismiss={true} onDismissed={this.onDismissed} />
 | 
					                query.push(
 | 
				
			||||||
            );
 | 
					                    <AddressTile key={i} address={this.state.inviteList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />
 | 
				
			||||||
        } else {
 | 
					                );
 | 
				
			||||||
            query = (
 | 
					            }
 | 
				
			||||||
                <textarea rows="1"
 | 
					 | 
				
			||||||
                    id="textinput"
 | 
					 | 
				
			||||||
                    ref="textinput"
 | 
					 | 
				
			||||||
                    className="mx_ChatInviteDialog_input"
 | 
					 | 
				
			||||||
                    onChange={this.onQueryChanged}
 | 
					 | 
				
			||||||
                    placeholder={this.props.placeholder}
 | 
					 | 
				
			||||||
                    defaultValue={this.props.value}
 | 
					 | 
				
			||||||
                    autoFocus={this.props.focus}>
 | 
					 | 
				
			||||||
                </textarea>
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var queryList;
 | 
					        // Add the query at the end
 | 
				
			||||||
        var queryListElements = this.createQueryListTiles();
 | 
					        query.push(
 | 
				
			||||||
        if (queryListElements.length > 0) {
 | 
					            <textarea key={this.state.inviteList.length}
 | 
				
			||||||
            queryList = (
 | 
					                rows="1"
 | 
				
			||||||
                <div className="mx_ChatInviteDialog_queryList" ref={(ref) => {this.scrollElement = ref}}>
 | 
					                id="textinput"
 | 
				
			||||||
                    { queryListElements }
 | 
					                ref="textinput"
 | 
				
			||||||
                </div>
 | 
					                className="mx_ChatInviteDialog_input"
 | 
				
			||||||
 | 
					                onChange={this.onQueryChanged}
 | 
				
			||||||
 | 
					                placeholder={this.props.placeholder}
 | 
				
			||||||
 | 
					                defaultValue={this.props.value}
 | 
				
			||||||
 | 
					                autoFocus={this.props.focus}>
 | 
				
			||||||
 | 
					            </textarea>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var error;
 | 
				
			||||||
 | 
					        var addressSelector;
 | 
				
			||||||
 | 
					        if (this.state.error) {
 | 
				
			||||||
 | 
					            error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            addressSelector = (
 | 
				
			||||||
 | 
					                <AddressSelector ref={(ref) => {this.addressSelector = ref}}
 | 
				
			||||||
 | 
					                    addressList={ this.state.queryList }
 | 
				
			||||||
 | 
					                    onSelected={ this.onSelected }
 | 
				
			||||||
 | 
					                    truncateAt={ TRUNCATE_QUERY_LIST } />
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -359,10 +352,11 @@ module.exports = React.createClass({
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className="mx_Dialog_content">
 | 
					                <div className="mx_Dialog_content">
 | 
				
			||||||
                    <div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
 | 
					                    <div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
 | 
				
			||||||
                    { queryList }
 | 
					                    { error }
 | 
				
			||||||
 | 
					                    { addressSelector }
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className="mx_Dialog_buttons">
 | 
					                <div className="mx_Dialog_buttons">
 | 
				
			||||||
                    <button className="mx_Dialog_primary" onClick={this.onStartChat}>
 | 
					                    <button className="mx_Dialog_primary" onClick={this.onButtonClick}>
 | 
				
			||||||
                        {this.props.button}
 | 
					                        {this.props.button}
 | 
				
			||||||
                    </button>
 | 
					                    </button>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,154 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2015, 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.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var React = require("react");
 | 
				
			||||||
 | 
					var sdk = require("../../../index");
 | 
				
			||||||
 | 
					var classNames = require('classnames');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = React.createClass({
 | 
				
			||||||
 | 
					    displayName: 'AddressSelector',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    propTypes: {
 | 
				
			||||||
 | 
					        onSelected: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					        addressList: React.PropTypes.array.isRequired,
 | 
				
			||||||
 | 
					        truncateAt: React.PropTypes.number.isRequired,
 | 
				
			||||||
 | 
					        selected: React.PropTypes.number,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getInitialState: function() {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            selected: this.props.selected === undefined ? 0 : this.props.selected,
 | 
				
			||||||
 | 
					            hover: false,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    componentWillReceiveProps: function(props) {
 | 
				
			||||||
 | 
					        // Make sure the selected item isn't outside the list bounds
 | 
				
			||||||
 | 
					        var selected = this.state.selected;
 | 
				
			||||||
 | 
					        var maxSelected = this._maxSelected(props.addressList);
 | 
				
			||||||
 | 
					        if (selected > maxSelected) {
 | 
				
			||||||
 | 
					            this.setState({ selected: maxSelected });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    componentDidUpdate: function() {
 | 
				
			||||||
 | 
					        // As the user scrolls with the arrow keys keep the selected item
 | 
				
			||||||
 | 
					        // at the top of the window.
 | 
				
			||||||
 | 
					        if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
 | 
				
			||||||
 | 
					            var elementHeight = this.addressListElement.getBoundingClientRect().height;
 | 
				
			||||||
 | 
					            this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onKeyUpArrow: function() {
 | 
				
			||||||
 | 
					        if (this.state.selected > 0) {
 | 
				
			||||||
 | 
					            this.setState({
 | 
				
			||||||
 | 
					                selected: this.state.selected - 1,
 | 
				
			||||||
 | 
					                hover : false,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onKeyDownArrow: function() {
 | 
				
			||||||
 | 
					        if (this.state.selected < this._maxSelected(this.props.addressList)) {
 | 
				
			||||||
 | 
					            this.setState({
 | 
				
			||||||
 | 
					                selected: this.state.selected + 1,
 | 
				
			||||||
 | 
					                hover : false,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onKeyReturn: function() {
 | 
				
			||||||
 | 
					        this.selectAddress(this.state.selected);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onClick: function(index) {
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					        return function() {
 | 
				
			||||||
 | 
					            self.selectAddress(index);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onMouseEnter: function(index) {
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					        return function() {
 | 
				
			||||||
 | 
					            self.setState({
 | 
				
			||||||
 | 
					                selected: index,
 | 
				
			||||||
 | 
					                hover: true,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onMouseLeave: function() {
 | 
				
			||||||
 | 
					        this.setState({ hover : false });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    selectAddress: function(index) {
 | 
				
			||||||
 | 
					        // Only try to select an address if one exists
 | 
				
			||||||
 | 
					        if (this.props.addressList.length !== 0) {
 | 
				
			||||||
 | 
					            this.props.onSelected(index);
 | 
				
			||||||
 | 
					            this.setState({ hover: false });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    createAddressListTiles: function() {
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					        var AddressTile = sdk.getComponent("elements.AddressTile");
 | 
				
			||||||
 | 
					        var maxSelected = this._maxSelected(this.props.addressList);
 | 
				
			||||||
 | 
					        var addressList = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Only create the address elements if there are address
 | 
				
			||||||
 | 
					        if (this.props.addressList.length > 0) {
 | 
				
			||||||
 | 
					            for (var i = 0; i <= maxSelected; i++) {
 | 
				
			||||||
 | 
					                var classes = classNames({
 | 
				
			||||||
 | 
					                    "mx_AddressSelector_addressListElement": true,
 | 
				
			||||||
 | 
					                    "mx_AddressSelector_selected": this.state.selected === i,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // NOTE: Defaulting to "vector" as the network, until the network backend stuff is done.
 | 
				
			||||||
 | 
					                // Saving the addressListElement so we can use it to work out, in the componentDidUpdate
 | 
				
			||||||
 | 
					                // method, how far to scroll when using the arrow keys
 | 
				
			||||||
 | 
					                addressList.push(
 | 
				
			||||||
 | 
					                    <div className={classes} onClick={this.onClick(i)} onMouseEnter={this.onMouseEnter(i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
 | 
				
			||||||
 | 
					                        <AddressTile address={this.props.addressList[i].userId} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return addressList;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _maxSelected: function(list) {
 | 
				
			||||||
 | 
					        var listSize = list.length === 0 ? 0 : list.length - 1;
 | 
				
			||||||
 | 
					        var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize
 | 
				
			||||||
 | 
					        return maxSelected;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render: function() {
 | 
				
			||||||
 | 
					        var classes = classNames({
 | 
				
			||||||
 | 
					            "mx_AddressSelector": true,
 | 
				
			||||||
 | 
					            "mx_AddressSelector_empty": this.props.addressList.length === 0,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <div className={classes} ref={(ref) => {this.scrollElement = ref}}>
 | 
				
			||||||
 | 
					                { this.createAddressListTiles() }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -19,13 +19,15 @@ limitations under the License.
 | 
				
			||||||
var React = require('react');
 | 
					var React = require('react');
 | 
				
			||||||
var classNames = require('classnames');
 | 
					var classNames = require('classnames');
 | 
				
			||||||
var sdk = require("../../../index");
 | 
					var sdk = require("../../../index");
 | 
				
			||||||
 | 
					var Invite = require("../../../Invite");
 | 
				
			||||||
 | 
					var MatrixClientPeg = require("../../../MatrixClientPeg");
 | 
				
			||||||
var Avatar = require('../../../Avatar');
 | 
					var Avatar = require('../../../Avatar');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = React.createClass({
 | 
					module.exports = React.createClass({
 | 
				
			||||||
    displayName: 'AddressTile',
 | 
					    displayName: 'AddressTile',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    propTypes: {
 | 
					    propTypes: {
 | 
				
			||||||
        user: React.PropTypes.object.isRequired,
 | 
					        address: React.PropTypes.string.isRequired,
 | 
				
			||||||
        canDismiss: React.PropTypes.bool,
 | 
					        canDismiss: React.PropTypes.bool,
 | 
				
			||||||
        onDismissed: React.PropTypes.func,
 | 
					        onDismissed: React.PropTypes.func,
 | 
				
			||||||
        justified: React.PropTypes.bool,
 | 
					        justified: React.PropTypes.bool,
 | 
				
			||||||
| 
						 | 
					@ -44,11 +46,30 @@ module.exports = React.createClass({
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render: function() {
 | 
					    render: function() {
 | 
				
			||||||
 | 
					        var userId, name, imgUrl, email;
 | 
				
			||||||
        var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
 | 
					        var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
 | 
				
			||||||
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
 | 
					        var TintableSvg = sdk.getComponent("elements.TintableSvg");
 | 
				
			||||||
        var userId = this.props.user.userId;
 | 
					
 | 
				
			||||||
        var name = this.props.user.displayName || userId;
 | 
					        // Check if the addr is a valid type
 | 
				
			||||||
        var imgUrl = Avatar.avatarUrlForUser(this.props.user, 25, 25, "crop");
 | 
					        var addrType = Invite.getAddressType(this.props.address);
 | 
				
			||||||
 | 
					        if (addrType === "mx") {
 | 
				
			||||||
 | 
					            let user = MatrixClientPeg.get().getUser(this.props.address);
 | 
				
			||||||
 | 
					            if (user) {
 | 
				
			||||||
 | 
					                userId = user.userId;
 | 
				
			||||||
 | 
					                name = user.displayName || userId;
 | 
				
			||||||
 | 
					                imgUrl = Avatar.avatarUrlForUser(user, 25, 25, "crop");
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                name=this.props.address;
 | 
				
			||||||
 | 
					                imgUrl = "img/icon-mx-user.svg";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if (addrType === "email") {
 | 
				
			||||||
 | 
					            email = this.props.address;
 | 
				
			||||||
 | 
					            name="email";
 | 
				
			||||||
 | 
					            imgUrl = "img/icon-email-user.svg";
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            name="Unknown";
 | 
				
			||||||
 | 
					            imgUrl = "img/avatar-error.svg";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var network;
 | 
					        var network;
 | 
				
			||||||
        if (this.props.networkUrl !== "") {
 | 
					        if (this.props.networkUrl !== "") {
 | 
				
			||||||
| 
						 | 
					@ -59,6 +80,60 @@ module.exports = React.createClass({
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var info;
 | 
				
			||||||
 | 
					        var error = false;
 | 
				
			||||||
 | 
					        if (addrType === "mx" && userId) {
 | 
				
			||||||
 | 
					            var nameClasses = classNames({
 | 
				
			||||||
 | 
					                "mx_AddressTile_name": true,
 | 
				
			||||||
 | 
					                "mx_AddressTile_justified": this.props.justified,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var idClasses = classNames({
 | 
				
			||||||
 | 
					                "mx_AddressTile_id": true,
 | 
				
			||||||
 | 
					                "mx_AddressTile_justified": this.props.justified,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            info = (
 | 
				
			||||||
 | 
					                <div className="mx_AddressTile_mx">
 | 
				
			||||||
 | 
					                    <div className={nameClasses}>{ name }</div>
 | 
				
			||||||
 | 
					                    <div className={idClasses}>{ userId }</div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        } else if (addrType === "mx") {
 | 
				
			||||||
 | 
					            var unknownMxClasses = classNames({
 | 
				
			||||||
 | 
					                "mx_AddressTile_unknownMx": true,
 | 
				
			||||||
 | 
					                "mx_AddressTile_justified": this.props.justified,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            info = (
 | 
				
			||||||
 | 
					                <div className={unknownMxClasses}>{ this.props.address }</div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        } else if (email)  {
 | 
				
			||||||
 | 
					            var emailClasses = classNames({
 | 
				
			||||||
 | 
					                "mx_AddressTile_email": true,
 | 
				
			||||||
 | 
					                "mx_AddressTile_justified": this.props.justified,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            info = (
 | 
				
			||||||
 | 
					                <div className={emailClasses}>{ email }</div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            error = true;
 | 
				
			||||||
 | 
					            var unknownClasses = classNames({
 | 
				
			||||||
 | 
					                "mx_AddressTile_unknown": true,
 | 
				
			||||||
 | 
					                "mx_AddressTile_justified": this.props.justified,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            info = (
 | 
				
			||||||
 | 
					                <div className={unknownClasses}>Unknown Address</div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var classes = classNames({
 | 
				
			||||||
 | 
					            "mx_AddressTile": true,
 | 
				
			||||||
 | 
					            "mx_AddressTile_error": error,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var dismiss;
 | 
					        var dismiss;
 | 
				
			||||||
        if (this.props.canDismiss) {
 | 
					        if (this.props.canDismiss) {
 | 
				
			||||||
            dismiss = (
 | 
					            dismiss = (
 | 
				
			||||||
| 
						 | 
					@ -68,24 +143,13 @@ module.exports = React.createClass({
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var nameClasses = classNames({
 | 
					 | 
				
			||||||
            "mx_AddressTile_name": true,
 | 
					 | 
				
			||||||
            "mx_AddressTile_justified": this.props.justified,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        var idClasses = classNames({
 | 
					 | 
				
			||||||
            "mx_AddressTile_id": true,
 | 
					 | 
				
			||||||
            "mx_AddressTile_justified": this.props.justified,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            <div className="mx_AddressTile">
 | 
					            <div className={classes}>
 | 
				
			||||||
                { network }
 | 
					                { network }
 | 
				
			||||||
                <div className="mx_AddressTile_avatar">
 | 
					                <div className="mx_AddressTile_avatar">
 | 
				
			||||||
                    <BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
 | 
					                    <BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className={nameClasses}>{ name }</div>
 | 
					                { info }
 | 
				
			||||||
                <div className={idClasses}>{ userId }</div>
 | 
					 | 
				
			||||||
                { dismiss }
 | 
					                { dismiss }
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,144 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					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 {getAddressType, inviteToRoom} from '../Invite';
 | 
				
			||||||
 | 
					import q from 'q';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Invites multiple addresses to a room, handling rate limiting from the server
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default class MultiInviter {
 | 
				
			||||||
 | 
					    constructor(roomId) {
 | 
				
			||||||
 | 
					        this.roomId = roomId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.canceled = false;
 | 
				
			||||||
 | 
					        this.addrs = [];
 | 
				
			||||||
 | 
					        this.busy = false;
 | 
				
			||||||
 | 
					        this.completionStates = {}; // State of each address (invited or error)
 | 
				
			||||||
 | 
					        this.errorTexts = {}; // Textual error per address
 | 
				
			||||||
 | 
					        this.deferred = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Invite users to this room. This may only be called once per
 | 
				
			||||||
 | 
					     * instance of the class.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * The promise is given progress when each address completes, with an
 | 
				
			||||||
 | 
					     * object argument with each completed address with value either
 | 
				
			||||||
 | 
					     * 'invited' or 'error'.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {array} addresses Array of addresses to invite
 | 
				
			||||||
 | 
					     * @returns {Promise} Resolved when all invitations in the queue are complete
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    invite(addrs) {
 | 
				
			||||||
 | 
					        if (this.addrs.length > 0) {
 | 
				
			||||||
 | 
					            throw new Error("Already inviting/invited");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.addrs.push(...addrs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const addr of this.addrs) {
 | 
				
			||||||
 | 
					            if (getAddressType(addr) === null) {
 | 
				
			||||||
 | 
					                this.completionStates[addr] = 'error';
 | 
				
			||||||
 | 
					                this.errorTexts[addr] = 'Unrecognised address';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.deferred = q.defer();
 | 
				
			||||||
 | 
					        this._inviteMore(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.deferred.promise;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Stops inviting. Causes promises returned by invite() to be rejected.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    cancel() {
 | 
				
			||||||
 | 
					        if (!this.busy) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this._canceled = true;
 | 
				
			||||||
 | 
					        this.deferred.reject(new Error('canceled'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getCompletionState(addr) {
 | 
				
			||||||
 | 
					        return this.completionStates[addr];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getErrorText(addr) {
 | 
				
			||||||
 | 
					        return this.errorTexts[addr];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _inviteMore(nextIndex) {
 | 
				
			||||||
 | 
					        if (this._canceled) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (nextIndex == this.addrs.length) {
 | 
				
			||||||
 | 
					            this.busy = false;
 | 
				
			||||||
 | 
					            this.deferred.resolve(this.completionStates);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const addr = this.addrs[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 to do so again)
 | 
				
			||||||
 | 
					        if (getAddressType(addr) === 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.completionStates[addr] == 'invited') {
 | 
				
			||||||
 | 
					            this._inviteMore(nextIndex + 1);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        inviteToRoom(this.roomId, addr).then(() => {
 | 
				
			||||||
 | 
					            if (this._canceled) { return; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.completionStates[addr] = 'invited';
 | 
				
			||||||
 | 
					            this.deferred.notify(this.completionStates);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this._inviteMore(nextIndex + 1);
 | 
				
			||||||
 | 
					        }, (err) => {
 | 
				
			||||||
 | 
					            if (this._canceled) { return; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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.completionStates[addr] = 'error';
 | 
				
			||||||
 | 
					            this.errorTexts[addr] = errorText;
 | 
				
			||||||
 | 
					            this.busy = !fatal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!fatal) {
 | 
				
			||||||
 | 
					                this.deferred.notify(this.completionStates);
 | 
				
			||||||
 | 
					                this._inviteMore(nextIndex + 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue