diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 55eaf75e4b6..498dfb88186 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -33,7 +33,6 @@ src/components/views/create_room/CreateRoomButton.js src/components/views/create_room/Presets.js src/components/views/create_room/RoomAlias.js src/components/views/dialogs/ChatCreateOrReuseDialog.js -src/components/views/dialogs/ChatInviteDialog.js src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/InteractiveAuthDialog.js src/components/views/dialogs/SetMxIdDialog.js @@ -114,7 +113,6 @@ src/components/views/settings/EnableNotificationsButton.js src/ContentMessages.js src/HtmlUtils.js src/ImageUtils.js -src/Invite.js src/languageHandler.js src/linkify-matrix.js src/Login.js diff --git a/src/Invite.js b/src/Invite.js index 0e8aca2cb55..b8e33d318ac 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,24 +17,11 @@ limitations under the License. import MatrixClientPeg from './MatrixClientPeg'; import MultiInviter from './utils/MultiInviter'; - -const emailRegex = /^\S+@\S+\.\S+$/; - -const mxidRegex = /^@\S+:\S+$/ - -export function getAddressType(inputText) { - const isEmailAddress = emailRegex.test(inputText); - const isMatrixId = mxidRegex.test(inputText); - - // sanity check the input for user IDs - if (isEmailAddress) { - return 'email'; - } else if (isMatrixId) { - return 'mx'; - } else { - return null; - } -} +import Modal from './Modal'; +import { getAddressType } from './UserAddress'; +import createRoom from './createRoom'; +import sdk from './'; +import { _t } from './languageHandler'; export function inviteToRoom(roomId, addr) { const addrType = getAddressType(addr); @@ -52,12 +40,116 @@ export function inviteToRoom(roomId, addr) { * 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 + * @param {string} roomId The ID of the room to invite to + * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. + * @returns {Promise} Promise */ export function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); return inviter.invite(addrs); } +export function showStartChatInviteDialog() { + const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog"); + Modal.createTrackedDialog('Start a chat', '', UserPickerDialog, { + title: _t('Start a chat'), + description: _t("Who would you like to communicate with?"), + placeholder: _t("Email, name or matrix ID"), + button: _t("Start Chat"), + onFinished: _onStartChatFinished, + }); +} + +export function showRoomInviteDialog(roomId) { + const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog"); + Modal.createTrackedDialog('Chat Invite', '', UserPickerDialog, { + title: _t('Invite new room members'), + description: _t('Who would you like to add to this room?'), + button: _t('Send Invites'), + placeholder: _t("Email, name or matrix ID"), + onFinished: (shouldInvite, addrs) => { + _onRoomInviteFinished(roomId, shouldInvite, addrs); + }, + }); +} + +function _onStartChatFinished(shouldInvite, addrs) { + if (!shouldInvite) return; + + const addrTexts = addrs.map((addr) => addr.address); + + if (_isDmChat(addrTexts)) { + // Start a new DM chat + createRoom({dmUserId: addrTexts[0]}).catch((err) => { + console.error(err.stack); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { + title: _t("Failed to invite user"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + } else { + // Start multi user chat + let room; + createRoom().then((roomId) => { + room = MatrixClientPeg.get().getRoom(roomId); + return inviteMultipleToRoom(roomId, addrTexts); + }).then((addrs) => { + return _showAnyInviteErrors(addrs, room); + }).catch((err) => { + console.error(err.stack); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t("Failed to invite"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + } +} + +function _onRoomInviteFinished(roomId, shouldInvite, addrs) { + if (!shouldInvite) return; + + const addrTexts = addrs.map((addr) => addr.address); + + // Invite new users to a room + inviteMultipleToRoom(roomId, addrTexts).then((addrs) => { + const room = MatrixClientPeg.get().getRoom(roomId); + return _showAnyInviteErrors(addrs, room); + }).catch((err) => { + console.error(err.stack); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t("Failed to invite"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); +} + +function _isDmChat(addrTexts) { + if (addrTexts.length === 1 && getAddressType(addrTexts[0])) { + return true; + } else { + return false; + } +} + +function _showAnyInviteErrors(addrs, room) { + // Show user any errors + const errorList = []; + for (const addr of Object.keys(addrs)) { + if (addrs[addr] === "error") { + errorList.push(addr); + } + } + + if (errorList.length > 0) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { + title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), + description: errorList.join(", "), + }); + } + return addrs; +} + diff --git a/src/UserAddress.js b/src/UserAddress.js new file mode 100644 index 00000000000..9eee48629d1 --- /dev/null +++ b/src/UserAddress.js @@ -0,0 +1,54 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const emailRegex = /^\S+@\S+\.\S+$/; + +const mxidRegex = /^@\S+:\S+$/; + +import PropTypes from 'prop-types'; +export const addressTypes = [ + 'mx', 'email', +]; + +// PropType definition for an object describing +// an address that can be invited to a room (which +// could be a third party identifier or a matrix ID) +// along with some additional information about the +// address / target. +export const UserAddressType = PropTypes.shape({ + addressType: PropTypes.oneOf(addressTypes).isRequired, + address: PropTypes.string.isRequired, + displayName: PropTypes.string, + avatarMxc: PropTypes.string, + // true if the address is known to be a valid address (eg. is a real + // user we've seen) or false otherwise (eg. is just an address the + // user has entered) + isKnown: PropTypes.bool, +}); + +export function getAddressType(inputText) { + const isEmailAddress = emailRegex.test(inputText); + const isMatrixId = mxidRegex.test(inputText); + + // sanity check the input for user IDs + if (isEmailAddress) { + return 'email'; + } else if (isMatrixId) { + return 'mx'; + } else { + return null; + } +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8cb111bf820..4d671d9cad6 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,6 +32,7 @@ import dis from "../../dispatcher"; import Modal from "../../Modal"; import Tinter from "../../Tinter"; import sdk from '../../index'; +import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite'; import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; import * as Lifecycle from '../../Lifecycle'; @@ -512,7 +514,7 @@ module.exports = React.createClass({ this._createChat(); break; case 'view_invite': - this._invite(payload.roomId); + showRoomInviteDialog(payload.roomId); break; case 'notifier_enabled': this.forceUpdate(); @@ -766,13 +768,7 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_set_mxid'}); return; } - const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); - Modal.createTrackedDialog('Start a chat', '', ChatInviteDialog, { - title: _t('Start a chat'), - description: _t("Who would you like to communicate with?"), - placeholder: _t("Email, name or matrix ID"), - button: _t("Start Chat"), - }); + showStartChatInviteDialog(); }, _createRoom: function() { @@ -857,17 +853,6 @@ module.exports = React.createClass({ }).close; }, - _invite: function(roomId) { - const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); - Modal.createTrackedDialog('Chat Invite', '', ChatInviteDialog, { - title: _t('Invite new room members'), - description: _t('Who would you like to add to this room?'), - button: _t('Send Invites'), - placeholder: _t("Email, name or matrix ID"), - roomId: roomId, - }); - }, - _leaveRoom: function(roomId) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/UserPickerDialog.js similarity index 59% rename from src/components/views/dialogs/ChatInviteDialog.js rename to src/components/views/dialogs/UserPickerDialog.js index 728860edec0..bde1ab0910e 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/UserPickerDialog.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,40 +16,37 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; -import { getAddressType, inviteMultipleToRoom } from '../../../Invite'; -import createRoom from '../../../createRoom'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import DMRoomMap from '../../../utils/DMRoomMap'; -import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; import Promise from 'bluebird'; -import dis from '../../../dispatcher'; +import { addressTypes, getAddressType } from '../../../UserAddress.js'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; module.exports = React.createClass({ - displayName: "ChatInviteDialog", + displayName: "UserPickerDialog", + propTypes: { - title: React.PropTypes.string.isRequired, - description: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.string, - ]), - value: React.PropTypes.string, - placeholder: React.PropTypes.string, - roomId: React.PropTypes.string, - button: React.PropTypes.string, - focus: React.PropTypes.bool, - onFinished: React.PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.node, + value: PropTypes.string, + placeholder: PropTypes.string, + roomId: PropTypes.string, + button: PropTypes.string, + focus: PropTypes.bool, + validAddressTypes: PropTypes.arrayOf(PropTypes.oneOfType(addressTypes)), + onFinished: PropTypes.func.isRequired, }, getDefaultProps: function() { return { value: "", focus: true, + validAddressTypes: addressTypes, }; }, @@ -56,9 +54,9 @@ module.exports = React.createClass({ return { error: false, - // List of AddressTile.InviteAddressType objects representing + // List of UserAddressType objects representing // the list of addresses we're going to invite - inviteList: [], + userList: [], // Whether a search is ongoing busy: false, @@ -68,7 +66,7 @@ module.exports = React.createClass({ serverSupportsUserDirectory: true, // The query being searched for query: "", - // List of AddressTile.InviteAddressType objects representing + // List of UserAddressType objects representing // the set of auto-completion results for the current search // query. queryList: [], @@ -83,57 +81,14 @@ module.exports = React.createClass({ }, onButtonClick: function() { - let inviteList = this.state.inviteList.slice(); + let userList = this.state.userList.slice(); // Check the text input field to see if user has an unconverted address - // If there is and it's valid add it to the local inviteList + // If there is and it's valid add it to the local userList if (this.refs.textinput.value !== '') { - inviteList = this._addInputToList(); - if (inviteList === null) return; - } - - const addrTexts = inviteList.map(addr => addr.address); - if (inviteList.length > 0) { - if (this._isDmChat(addrTexts)) { - const userId = inviteList[0].address; - // Direct Message chat - const rooms = this._getDirectMessageRooms(userId); - if (rooms.length > 0) { - // A Direct Message room already exists for this user, so select a - // room from a list that is similar to the one in MemberInfo panel - const ChatCreateOrReuseDialog = sdk.getComponent( - "views.dialogs.ChatCreateOrReuseDialog", - ); - const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, { - userId: userId, - onFinished: (success) => { - this.props.onFinished(success); - }, - onNewDMClick: () => { - dis.dispatch({ - action: 'start_chat', - user_id: userId, - }); - close(true); - }, - onExistingRoomSelected: (roomId) => { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); - close(true); - }, - }).close; - } else { - this._startChat(inviteList); - } - } else { - // Multi invite chat - this._startChat(inviteList); - } - } else { - // No addresses supplied - this.setState({ error: true }); + userList = this._addInputToList(); + if (userList === null) return; } + this.props.onFinished(true, userList); }, onCancel: function() { @@ -157,10 +112,10 @@ module.exports = React.createClass({ e.stopPropagation(); e.preventDefault(); if (this.addressSelector) this.addressSelector.chooseSelection(); - } else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace + } else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace e.stopPropagation(); e.preventDefault(); - this.onDismissed(this.state.inviteList.length - 1)(); + this.onDismissed(this.state.userList.length - 1)(); } else if (e.keyCode === 13) { // enter e.stopPropagation(); e.preventDefault(); @@ -201,12 +156,11 @@ module.exports = React.createClass({ }, onDismissed: function(index) { - var self = this; return () => { - var inviteList = self.state.inviteList.slice(); - inviteList.splice(index, 1); - self.setState({ - inviteList: inviteList, + const userList = this.state.userList.slice(); + userList.splice(index, 1); + this.setState({ + userList: userList, queryList: [], query: "", }); @@ -215,17 +169,16 @@ module.exports = React.createClass({ }, onClick: function(index) { - var self = this; - return function() { - self.onSelected(index); + return () => { + this.onSelected(index); }; }, onSelected: function(index) { - var inviteList = this.state.inviteList.slice(); - inviteList.push(this.state.queryList[index]); + const userList = this.state.userList.slice(); + userList.push(this.state.queryList[index]); this.setState({ - inviteList: inviteList, + userList: userList, queryList: [], query: "", }); @@ -297,7 +250,7 @@ module.exports = React.createClass({ return; } // Return objects, structure of which is defined - // by InviteAddressType + // by UserAddressType queryList.push({ addressType: 'mx', address: user.user_id, @@ -311,7 +264,7 @@ module.exports = React.createClass({ // This is important, otherwise there's no way to invite // a perfectly valid address if there are close matches. const addrType = getAddressType(query); - if (addrType !== null) { + if (this.props.validAddressTypes.includes(addrType)) { queryList.unshift({ addressType: addrType, address: query, @@ -330,132 +283,6 @@ module.exports = React.createClass({ }); }, - _getDirectMessageRooms: function(addr) { - const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); - const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); - const rooms = []; - dmRooms.forEach(dmRoom => { - let room = MatrixClientPeg.get().getRoom(dmRoom); - if (room) { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - if (me.membership == 'join') { - rooms.push(room); - } - } - }); - return rooms; - }, - - _startChat: function(addrs) { - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'view_set_mxid'}); - return; - } - - const addrTexts = addrs.map((addr) => { - return addr.address; - }); - - if (this.props.roomId) { - // Invite new user to a room - var self = this; - inviteMultipleToRoom(this.props.roomId, addrTexts) - .then(function(addrs) { - var room = MatrixClientPeg.get().getRoom(self.props.roomId); - return self._showAnyInviteErrors(addrs, room); - }) - .catch(function(err) { - console.error(err.stack); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { - title: _t("Failed to invite"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - return null; - }) - .done(); - } else if (this._isDmChat(addrTexts)) { - // Start the DM chat - createRoom({dmUserId: addrTexts[0]}) - .catch(function(err) { - console.error(err.stack); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { - title: _t("Failed to invite user"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - return null; - }) - .done(); - } else { - // Start multi user chat - var self = this; - var room; - createRoom().then(function(roomId) { - room = MatrixClientPeg.get().getRoom(roomId); - return inviteMultipleToRoom(roomId, addrTexts); - }) - .then(function(addrs) { - return self._showAnyInviteErrors(addrs, room); - }) - .catch(function(err) { - console.error(err.stack); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { - title: _t("Failed to invite"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - return null; - }) - .done(); - } - - // Close - this will happen before the above, as that is async - this.props.onFinished(true, addrTexts); - }, - - _isOnInviteList: function(uid) { - for (let i = 0; i < this.state.inviteList.length; i++) { - if ( - this.state.inviteList[i].addressType == 'mx' && - this.state.inviteList[i].address.toLowerCase() === uid - ) { - return true; - } - } - return false; - }, - - _isDmChat: function(addrTexts) { - if (addrTexts.length === 1 && - getAddressType(addrTexts[0]) === "mx" && - !this.props.roomId - ) { - return true; - } else { - return false; - } - }, - - _showAnyInviteErrors: function(addrs, room) { - // Show user any errors - var errorList = []; - for (var addr in addrs) { - if (addrs.hasOwnProperty(addr) && addrs[addr] === "error") { - errorList.push(addr); - } - } - - if (errorList.length > 0) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { - title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description: errorList.join(", "), - }); - } - return addrs; - }, - _addInputToList: function() { const addressText = this.refs.textinput.value.trim(); const addrType = getAddressType(addressText); @@ -476,15 +303,15 @@ module.exports = React.createClass({ } } - const inviteList = this.state.inviteList.slice(); - inviteList.push(addrObj); + const userList = this.state.userList.slice(); + userList.push(addrObj); this.setState({ - inviteList: inviteList, + userList: userList, queryList: [], query: "", }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); - return inviteList; + return userList; }, _lookupThreepid: function(medium, address) { @@ -495,7 +322,7 @@ module.exports = React.createClass({ // not like they leak. this._cancelThreepidLookup = function() { cancelled = true; - } + }; // wait a bit to let the user finish typing return Promise.delay(500).then(() => { @@ -511,7 +338,7 @@ module.exports = React.createClass({ if (cancelled) return null; this.setState({ queryList: [{ - // an InviteAddressType + // a UserAddressType addressType: medium, address: address, displayName: res.displayname, @@ -527,20 +354,20 @@ module.exports = React.createClass({ const AddressSelector = sdk.getComponent("elements.AddressSelector"); this.scrollElement = null; - var query = []; + const query = []; // create the invite list - if (this.state.inviteList.length > 0) { - var AddressTile = sdk.getComponent("elements.AddressTile"); - for (let i = 0; i < this.state.inviteList.length; i++) { + if (this.state.userList.length > 0) { + const AddressTile = sdk.getComponent("elements.AddressTile"); + for (let i = 0; i < this.state.userList.length; i++) { query.push( - + , ); } } // Add the query at the end query.push( -