diff --git a/src/component-index.js b/src/component-index.js index 11c711d239..08477c676e 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -52,7 +52,6 @@ module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./ module.exports.components['views.dialogs.EncryptedEventDialog'] = require('./components/views/dialogs/EncryptedEventDialog'); 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'); @@ -91,7 +90,6 @@ module.exports.components['views.rooms.Autocomplete'] = require('./components/vi module.exports.components['views.rooms.AuxPanel'] = require('./components/views/rooms/AuxPanel'); module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile'); module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); -module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList'); module.exports.components['views.rooms.LinkPreviewWidget'] = require('./components/views/rooms/LinkPreviewWidget'); module.exports.components['views.rooms.MemberDeviceInfo'] = require('./components/views/rooms/MemberDeviceInfo'); module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo'); diff --git a/src/components/views/dialogs/MultiInviteDialog.js b/src/components/views/dialogs/MultiInviteDialog.js deleted file mode 100644 index a8d7aec495..0000000000 --- a/src/components/views/dialogs/MultiInviteDialog.js +++ /dev/null @@ -1,218 +0,0 @@ -/* -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._canceled = false; - - 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'; - } - } - } - - componentWillUnmount() { - this._canceled = true; - } - - _onCancel() { - this._canceled = true; - this.props.onFinished(false); - } - - _startInviting() { - this.setState({ - busy: true, - done: false, - }); - this._inviteMore(0); - } - - _inviteMore(nextIndex) { - if (this._canceled) { - return; - } - - 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 to 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(() => { - if (this._canceled) { return; } - - this.setState((s) => { - s.completionStates[nextIndex] = 'invited' - return s; - }); - 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.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() { - 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 - {Object.keys(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(); - 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, - inputs: React.PropTypes.array.isRequired, - roomId: React.PropTypes.string.isRequired, -}; diff --git a/src/components/views/rooms/InviteMemberList.js b/src/components/views/rooms/InviteMemberList.js deleted file mode 100644 index ee5eabbeab..0000000000 --- a/src/components/views/rooms/InviteMemberList.js +++ /dev/null @@ -1,132 +0,0 @@ -/* -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. -*/ -import React from 'react'; -import sdk from '../../../index'; -import Entities from '../../../Entities'; -import MatrixClientPeg from '../../../MatrixClientPeg'; -import rate_limited_func from '../../../ratelimitedfunc'; - -const INITIAL_SEARCH_RESULTS_COUNT = 10; - -module.exports = React.createClass({ - displayName: 'InviteMemberList', - - propTypes: { - roomId: React.PropTypes.string.isRequired, - onInvite: React.PropTypes.func.isRequired, // fn(inputText) - onThirdPartyInvite: React.PropTypes.func.isRequired, // fn(inputText) - onSearchQueryChanged: React.PropTypes.func // fn(inputText) - }, - - getDefaultProps: function() { - return { - onSearchQueryChanged: function() {} - }; - }, - - componentWillMount: function() { - var cli = MatrixClientPeg.get(); - cli.on("RoomState.members", this.onRoomStateMember); - - this._emailEntity = null; - - // we have to update the list whenever membership changes - // particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813 - this._updateList(); - }, - - componentDidMount: function() { - // initialise the email tile - this.onSearchQueryChanged(''); - }, - - componentWillUnmount: function() { - var cli = MatrixClientPeg.get(); - if (cli) { - cli.removeListener("RoomState.members", this.onRoomStateMember); - } - // cancel any pending calls to the rate_limited_funcs - this._updateList.cancelPendingCall(); - }, - - _updateList: new rate_limited_func(function() { - this._room = MatrixClientPeg.get().getRoom(this.props.roomId); - // Load the complete user list for inviting new users - if (this._room) { - this._userList = MatrixClientPeg.get().getUsers().filter((u) => { - return (!this._room.hasMembershipState(u.userId, "join") && - !this._room.hasMembershipState(u.userId, "invite")); - }); - } - }, 500), - - onRoomStateMember: function(ev, state, member) { - this._updateList(); - }, - - onInvite: function(ev) { - this.props.onInvite(this._input); - }, - - onThirdPartyInvite: function(ev) { - this.props.onThirdPartyInvite(this._input); - }, - - onSearchQueryChanged: function(input) { - this._input = input; - var EntityTile = sdk.getComponent("rooms.EntityTile"); - var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - - // var label = input; - // if (input[0] === "@") { - // label = input; - // } - // else { - // label = "Email: " + input; - // } - - // this._emailEntity = new Entities.newEntity( - // } - // className="mx_EntityTile_invitePlaceholder" - // presenceState="online" onClick={this.onThirdPartyInvite} name={"Invite by email"} - // />, - // function(query) { - // return true; // always show this - // } - // ); - - this.props.onSearchQueryChanged(input); - }, - - render: function() { - var SearchableEntityList = sdk.getComponent("rooms.SearchableEntityList"); - var entities = Entities.fromUsers(this._userList || [], true, this.props.onInvite); - - // Add an "Email: foo@bar.com" tile as the first tile - // if (this._emailEntity) { - // entities.unshift(this._emailEntity); - // } - - return ( - - ); - } -}); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index fa3a7504f3..deedded4fa 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -34,10 +34,6 @@ var SHARE_HISTORY_WARNING = turn off, 'Share message history with new users' in the settings for this room. -var shown_invite_warning_this_session = false; -// global promise so people can bulk invite and they all get resolved -var invite_defer = q.defer(); - module.exports = React.createClass({ displayName: 'MemberList', @@ -47,6 +43,7 @@ module.exports = React.createClass({ // ideally we'd size this to the page height, but // in practice I find that a little constraining truncateAt: INITIAL_LOAD_NUM_MEMBERS, + searchQuery: "", }; if (!this.props.roomId) return state; var cli = MatrixClientPeg.get(); @@ -160,143 +157,6 @@ module.exports = React.createClass({ }); }, 500), - onThirdPartyInvite: function(inputText) { - var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - Modal.createDialog(TextInputDialog, { - title: "Invite members by email", - description: "Please enter one or more email addresses", - value: inputText, - button: "Invite", - onFinished: (should_invite, addresses)=>{ - if (should_invite) { - // defer the actual invite to the next event loop to give this - // Modal a chance to unmount in case onInvite() triggers a new one - setTimeout(()=>{ - this.onInvite(addresses); - }, 0); - } - } - }); - }, - - _doInvite(address) { - Invite.inviteToRoom(this.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(() => { - this.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(); - this.setState({ - inviting: true - }); - }, - - onInvite: function(inputText) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - var self = this; - inputText = inputText.trim(); // react requires es5-shim so we know trim() exists - - if (MatrixClientPeg.get().isGuest()) { - Modal.createDialog(NeedToRegisterDialog, { - title: "Unable to Invite", - description: "Guest user can't invite new users. Please register to be able to invite new users into a room." - }); - return; - } - - // email addresses and user IDs do not allow space, comma, semicolon so split - // on them for bulk inviting. - // '+' 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; - } - } - - if (validInputs == 0) { - Modal.createDialog(ErrorDialog, { - title: "Invite Error", - description: "Malformed ID. Should be an email address or a Matrix ID like '@localpart:domain'" - }); - return; - } - - var inviteWarningDefer = q.defer(); - - var room = MatrixClientPeg.get().getRoom(this.props.roomId); - var history_visibility = room.currentState.getStateEvents('m.room.history_visibility', ''); - if (history_visibility) history_visibility = history_visibility.getContent().history_visibility; - - if (history_visibility == 'shared' && !shown_invite_warning_this_session) { - inviteWarningDefer = invite_defer; // whether we continue depends on this defer - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { - title: "Warning", - description: SHARE_HISTORY_WARNING, - button: "Invite", - onFinished: function(should_invite) { - if (should_invite) { - shown_invite_warning_this_session = true; - invite_defer.resolve(); - } else { - invite_defer.reject(null); - // reset the promise so we don't auto-reject all invites from - // now on. - invite_defer = q.defer(); - } - } - }); - } else { - inviteWarningDefer.resolve(); - } - - const promise = inviteWarningDefer.promise; - - if (inputs.length == 1) { - // for a single address, we just send the invite - promise.done(() => { - this._doInvite(inputs[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() { if (!this.props.roomId) return {}; var cli = MatrixClientPeg.get(); @@ -423,10 +283,8 @@ module.exports = React.createClass({ return userB.getLastActiveTs() - userA.getLastActiveTs(); }, - onSearchQueryChanged: function(input) { - this.setState({ - searchQuery: input - }); + onSearchQueryChanged: function(ev) { + this.setState({ searchQuery: ev.target.value }); }, makeMemberTiles: function(membership, query) { @@ -489,8 +347,6 @@ module.exports = React.createClass({ }, render: function() { - var InviteMemberList = sdk.getComponent("rooms.InviteMemberList"); - var invitedSection = null; var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery); if (invitedMemberTiles.length > 0) { @@ -504,35 +360,25 @@ module.exports = React.createClass({ ); } - var inviteMemberListSection; - if (this.state.inviting) { - var Loader = sdk.getComponent("elements.Spinner"); - inviteMemberListSection = ( - - ); - } - else { - inviteMemberListSection = ( - - ); - } - + var inputBox = ( +
    + +
    + ); var TruncatedList = sdk.getComponent("elements.TruncatedList"); return (
    - {inviteMemberListSection} - - - {this.makeMemberTiles('join', this.state.searchQuery)} - - {invitedSection} - + { inputBox } + + + {this.makeMemberTiles('join', this.state.searchQuery)} + + {invitedSection} +
    ); }