diff --git a/src/component-index.js b/src/component-index.js index 3871f60e15..4cf2ba4016 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -45,6 +45,7 @@ module.exports.components['views.avatars.RoomAvatar'] = require('./components/vi module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton'); module.exports.components['views.create_room.Presets'] = require('./components/views/create_room/Presets'); module.exports.components['views.create_room.RoomAlias'] = require('./components/views/create_room/RoomAlias'); +module.exports.components['views.dialogs.ChatInviteDialog'] = require('./components/views/dialogs/ChatInviteDialog'); module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog'); module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog'); module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); @@ -53,6 +54,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.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog'); module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog'); +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.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer'); module.exports.components['views.elements.EmojiText'] = require('./components/views/elements/EmojiText'); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 251f3f1dc8..c83da2b8f0 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -370,6 +370,9 @@ module.exports = React.createClass({ this._setPage(this.PageTypes.RoomDirectory); this.notifyNewScreen('directory'); break; + case 'view_create_chat': + this._createChat(); + break; case 'notifier_enabled': this.forceUpdate(); break; @@ -506,6 +509,13 @@ module.exports = React.createClass({ } }, + _createChat: function() { + var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); + Modal.createDialog(ChatInviteDialog, { + title: "Start a one to one chat", + }); + }, + // update scrollStateMap according to the current scroll state of the // room view. _updateScrollMap: function() { diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js new file mode 100644 index 0000000000..e322127135 --- /dev/null +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -0,0 +1,371 @@ +/* +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. +*/ + +var React = require("react"); +var classNames = require('classnames'); +var sdk = require("../../../index"); +var Invite = require("../../../Invite"); +var createRoom = require("../../../createRoom"); +var MatrixClientPeg = require("../../../MatrixClientPeg"); +var DMRoomMap = require('../../../utils/DMRoomMap'); +var rate_limited_func = require("../../../ratelimitedfunc"); +var dis = require("../../../dispatcher"); +var Modal = require('../../../Modal'); + +const TRUNCATE_QUERY_LIST = 40; + +module.exports = React.createClass({ + displayName: "ChatInviteDialog", + propTypes: { + title: React.PropTypes.string, + description: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.string, + ]), + value: React.PropTypes.string, + placeholder: React.PropTypes.string, + button: React.PropTypes.string, + focus: React.PropTypes.bool, + onFinished: React.PropTypes.func.isRequired + }, + + getDefaultProps: function() { + return { + title: "Start a chat", + description: "Who would you like to communicate with?", + value: "", + placeholder: "User ID, Name or email", + button: "Start Chat", + focus: true + }; + }, + + getInitialState: function() { + return { + user: null, + queryList: [], + addressSelected: false, + selected: 0, + hover: false, + }; + }, + + componentDidMount: function() { + if (this.props.focus) { + // Set the cursor at the end of the text input + this.refs.textinput.value = this.props.value; + } + this._updateUserList(); + }, + + componentDidUpdate: function() { + // As the user scrolls with the arrow keys keep the selected item + // at the top of the window. + if (this.scrollElement && !this.state.hover) { + var elementHeight = this.queryListElement.getBoundingClientRect().height; + this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight; + } + }, + + onStartChat: function() { + var addr; + + // Either an address tile was created, or text input is being used + if (this.state.user) { + addr = this.state.user.userId; + } 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 { + this._startChat(addr); + } + } else if (Invite.getAddressType(addr) === "email") { + this._startChat(addr); + } else { + // Nothing to do, so focus back on the textinput + this.refs.textinput.focus(); + } + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + onKeyDown: function(e) { + if (e.keyCode === 27) { // escape + e.stopPropagation(); + e.preventDefault(); + this.props.onFinished(false); + } else if (e.keyCode === 38) { // up arrow + e.stopPropagation(); + e.preventDefault(); + if (this.state.selected > 0) { + this.setState({ + selected: this.state.selected - 1, + hover : false, + }); + } + } else if (e.keyCode === 40) { // down arrow + e.stopPropagation(); + e.preventDefault(); + if (this.state.selected < this._maxSelected(this.state.queryList)) { + this.setState({ + selected: this.state.selected + 1, + hover : false, + }); + } + } else if (e.keyCode === 13) { // enter + e.stopPropagation(); + e.preventDefault(); + if (this.state.queryList.length > 0) { + this.setState({ + user: this.state.queryList[this.state.selected], + addressSelected: true, + queryList: [], + hover : false, + }); + } + } + }, + + onQueryChanged: function(ev) { + var query = ev.target.value; + var queryList = []; + + // Only do search if there is something to search + if (query.length > 0) { + queryList = this._userList.filter((user) => { + return this._matches(query, user); + }); + } + + // 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({ + queryList: queryList, + selected: selected, + }); + }, + + onDismissed: function() { + this.setState({ + user: null, + addressSelected: false, + selected: 0, + queryList: [], + }); + }, + + onClick: function(index) { + var self = this; + return function() { + self.setState({ + user: self.state.queryList[index], + addressSelected: true, + queryList: [], + hover: false, + }); + }; + }, + + onMouseEnter: function(index) { + var self = this; + return function() { + self.setState({ + selected: index, + hover: true, + }); + }; + }, + + 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( +