element-web/src/components/views/dialogs/ChatInviteDialog.js

446 lines
15 KiB
JavaScript
Raw Normal View History

/*
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");
2016-09-05 19:28:08 +03:00
var MatrixClientPeg = require("../../../MatrixClientPeg");
var DMRoomMap = require('../../../utils/DMRoomMap');
2016-09-05 19:28:08 +03:00
var rate_limited_func = require("../../../ratelimitedfunc");
var dis = require("../../../dispatcher");
var Modal = require('../../../Modal');
2016-09-06 17:46:58 +03:00
const TRUNCATE_QUERY_LIST = 40;
2016-09-05 19:28:08 +03:00
/*
* Escapes a string so it can be used in a RegExp
* Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ]
* From http://stackoverflow.com/a/6969486
*/
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
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,
roomId: 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: "",
2017-01-12 16:46:12 +03:00
placeholder: "Email, name or matrix ID",
button: "Start Chat",
focus: true
};
},
2016-09-05 19:28:08 +03:00
getInitialState: function() {
return {
2016-09-13 13:02:59 +03:00
error: false,
inviteList: [],
queryList: [],
2016-09-05 19:28:08 +03:00
};
},
componentDidMount: function() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
}
2016-09-05 19:28:08 +03:00
this._updateUserList();
},
onButtonClick: function() {
var inviteList = this.state.inviteList.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
var check = Invite.isValidAddress(this.refs.textinput.value);
if (check === true || check === null) {
inviteList.push(this.refs.textinput.value);
} else if (this.refs.textinput.value.length > 0) {
this.setState({ error: true });
return;
}
if (inviteList.length > 0) {
if (this._isDmChat(inviteList)) {
2016-09-13 18:06:04 +03:00
// Direct Message chat
var room = this._getDirectMessageRoom(inviteList[0]);
2016-09-13 18:06:04 +03:00
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, inviteList[0]);
2016-09-13 18:06:04 +03:00
} else {
this._startChat(inviteList);
2016-09-13 18:06:04 +03:00
}
} else {
2016-09-13 18:06:04 +03:00
// Multi invite chat
this._startChat(inviteList);
}
} else {
2016-09-13 18:06:04 +03:00
// No addresses supplied
this.setState({ error: true });
}
},
2016-09-05 19:28:08 +03:00
onCancel: function() {
this.props.onFinished(false);
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
2016-09-06 15:07:06 +03:00
} else if (e.keyCode === 38) { // up arrow
2016-09-05 19:28:08 +03:00
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeyUp();
2016-09-06 15:07:06 +03:00
} else if (e.keyCode === 40) { // down arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeyDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
2016-09-06 15:07:06 +03:00
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeySelect();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
this.onDismissed(this.state.inviteList.length - 1)();
} else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
this.onButtonClick();
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
e.stopPropagation();
e.preventDefault();
2016-09-13 19:09:40 +03:00
var check = Invite.isValidAddress(this.refs.textinput.value);
if (check === true || check === null) {
2016-09-13 13:02:59 +03:00
var inviteList = this.state.inviteList.slice();
2016-09-17 18:14:02 +03:00
inviteList.push(this.refs.textinput.value.trim());
2016-09-13 13:02:59 +03:00
this.setState({
inviteList: inviteList,
queryList: [],
});
} else {
this.setState({ error: true });
}
2016-09-05 19:28:08 +03:00
}
},
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);
});
}
2016-09-06 15:07:06 +03:00
2016-09-13 13:02:59 +03:00
this.setState({
queryList: queryList,
error: false,
});
2016-09-05 19:28:08 +03:00
},
onDismissed: function(index) {
var self = this;
return function() {
var inviteList = self.state.inviteList.slice();
inviteList.splice(index, 1);
self.setState({
inviteList: inviteList,
queryList: [],
});
}
2016-09-05 19:28:08 +03:00
},
onClick: function(index) {
var self = this;
return function() {
self.onSelected(index);
};
},
onSelected: function(index) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.state.queryList[index].userId);
this.setState({
inviteList: inviteList,
queryList: [],
});
},
_getDirectMessageRoom: function(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
var dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
if (dmRooms.length > 0) {
// Cycle through all the DM rooms and find the first non forgotten or parted room
for (let i = 0; i < dmRooms.length; i++) {
let room = MatrixClientPeg.get().getRoom(dmRooms[i]);
if (room) {
2016-09-12 18:05:51 +03:00
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me.membership == 'join') {
return room;
}
}
}
}
return null;
},
2016-09-13 18:06:04 +03:00
_startChat: function(addrs) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't invite users. Please register."
});
return;
}
if (this.props.roomId) {
2016-09-13 18:06:04 +03:00
// Invite new user to a room
var self = this;
2016-09-13 18:06:04 +03:00
Invite.inviteMultipleToRoom(this.props.roomId, addrs)
.then(function(addrs) {
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
return self._showAnyInviteErrors(addrs, room);
})
2016-09-13 18:06:04 +03:00
.catch(function(err) {
console.error(err.stack);
2016-09-13 18:06:04 +03:00
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failure to invite",
2016-09-13 18:06:04 +03:00
description: err.toString()
});
return null;
})
.done();
} else if (this._isDmChat(addrs)) {
2016-09-13 18:06:04 +03:00
// Start the DM chat
createRoom({dmUserId: addrs[0]})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failure to invite user",
description: err.toString()
});
return null;
})
.done();
} else {
2016-09-13 18:06:04 +03:00
// Start multi user chat
var self = this;
var room;
2016-09-13 18:06:04 +03:00
createRoom().then(function(roomId) {
room = MatrixClientPeg.get().getRoom(roomId);
return Invite.inviteMultipleToRoom(roomId, addrs);
2016-09-13 18:06:04 +03:00
})
.then(function(addrs) {
return self._showAnyInviteErrors(addrs, room);
})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failure to invite",
description: err.toString()
});
return null;
})
.done();
}
// Close - this will happen before the above, as that is async
2016-09-13 18:06:04 +03:00
this.props.onFinished(true, addrs);
},
2016-09-05 19:28:08 +03:00
_updateUserList: new rate_limited_func(function() {
// Get all the users
this._userList = MatrixClientPeg.get().getUsers();
}, 500),
2016-09-05 19:28:08 +03:00
// This is the search algorithm for matching users
_matches: function(query, user) {
var name = user.displayName.toLowerCase();
var uid = user.userId.toLowerCase();
query = query.toLowerCase();
2016-09-14 17:35:04 +03:00
// don't match any that are already on the invite list
2016-09-12 19:41:32 +03:00
if (this._isOnInviteList(uid)) {
return false;
}
2016-09-14 17:35:04 +03:00
// ignore current user
if (uid === MatrixClientPeg.get().credentials.userId) {
return false;
}
2016-09-05 19:28:08 +03:00
// direct prefix matches
if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) {
return true;
}
2016-09-05 19:28:08 +03:00
// strip @ on uid and try matching again
if (uid.length > 1 && uid[0] === "@" && uid.substring(1).indexOf(query) === 0) {
return true;
}
2016-09-05 19:28:08 +03:00
// Try to find the query following a "word boundary", except that
// this does avoids using \b because it only considers letters from
// the roman alphabet to be word characters.
// Instead, we look for the query following either:
// * The start of the string
// * Whitespace, or
// * A fixed number of punctuation characters
let expr = new RegExp("(?:^|[\\s\\(\)'\",\.-])" + escapeRegExp(query));
if (expr.test(name)) {
return true;
2016-09-05 19:28:08 +03:00
}
2016-09-05 19:28:08 +03:00
return false;
},
_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
2016-09-12 19:41:32 +03:00
if (this.state.inviteList[i].toLowerCase() === uid) {
return true;
}
}
return false;
},
_isDmChat: function(addrs) {
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
2016-09-13 18:06:04 +03:00
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.createDialog(ErrorDialog, {
title: "Failed to invite the following users to the " + room.name + " room:",
description: errorList.join(", "),
});
}
return addrs;
},
render: function() {
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var AddressSelector = sdk.getComponent("elements.AddressSelector");
2016-09-06 19:46:00 +03:00
this.scrollElement = null;
2016-09-05 19:28:08 +03:00
var query = [];
// create the invite list
if (this.state.inviteList.length > 0) {
2016-09-05 19:28:08 +03:00
var AddressTile = sdk.getComponent("elements.AddressTile");
for (let i = 0; i < this.state.inviteList.length; i++) {
query.push(
<AddressTile key={i} address={this.state.inviteList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />
);
}
2016-09-05 19:28:08 +03:00
}
2016-09-13 13:02:59 +03:00
// Add the query at the end
query.push(
<textarea key={this.state.inviteList.length}
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>
);
2016-09-05 19:28:08 +03:00
2016-09-13 13:02:59 +03:00
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 } />
);
}
return (
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<div className="mx_ChatInviteDialog_cancel" onClick={this.onCancel} >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</div>
<div className="mx_ChatInviteDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>
<div className="mx_Dialog_content">
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
2016-09-13 13:02:59 +03:00
{ error }
{ addressSelector }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
{this.props.button}
</button>
</div>
</div>
);
}
});