Merge branch 'develop' into matthew/warn-unknown-devices

This commit is contained in:
Richard van der Hoff 2017-01-25 16:44:03 +00:00
commit a2dd1fa0a9
45 changed files with 2021 additions and 589 deletions

View file

@ -0,0 +1,72 @@
/*
Copyright 2017 Vector Creations 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 * as KeyCode from '../../../KeyCode';
/**
* Basic container for modal dialogs.
*
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
export default React.createClass({
displayName: 'BaseDialog',
propTypes: {
// onFinished callback to call when Escape is pressed
onFinished: React.PropTypes.func.isRequired,
// callback to call when Enter is pressed
onEnterPressed: React.PropTypes.func,
// CSS class to apply to dialog div
className: React.PropTypes.string,
// Title for the dialog.
// (could probably actually be something more complicated than a string if desired)
title: React.PropTypes.string.isRequired,
// children should be the content of the dialog
children: React.PropTypes.node,
},
_onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished();
} else if (e.keyCode === KeyCode.ENTER) {
if (this.props.onEnterPressed) {
e.stopPropagation();
e.preventDefault();
this.props.onEnterPressed(e);
}
}
},
render: function() {
return (
<div onKeyDown={this._onKeyDown} className={this.props.className}>
<div className='mx_Dialog_title'>
{ this.props.title }
</div>
{ this.props.children }
</div>
);
},
});

View file

@ -24,9 +24,19 @@ var DMRoomMap = require('../../../utils/DMRoomMap');
var rate_limited_func = require("../../../ratelimitedfunc");
var dis = require("../../../dispatcher");
var Modal = require('../../../Modal');
import AccessibleButton from '../elements/AccessibleButton';
const TRUNCATE_QUERY_LIST = 40;
/*
* 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: {
@ -57,7 +67,14 @@ module.exports = React.createClass({
getInitialState: function() {
return {
error: false,
// List of AddressTile.InviteAddressType objects represeting
// the list of addresses we're going to invite
inviteList: [],
// List of AddressTile.InviteAddressType objects represeting
// the set of autocompletion results for the current search
// query.
queryList: [],
};
},
@ -146,14 +163,38 @@ module.exports = React.createClass({
},
onQueryChanged: function(ev) {
var query = ev.target.value;
var queryList = [];
const query = ev.target.value;
let queryList = [];
// Only do search if there is something to search
if (query.length > 0) {
if (query.length > 0 && query != '@') {
// filter the known users list
queryList = this._userList.filter((user) => {
return this._matches(query, user);
}).map((user) => {
// Return objects, structure of which is defined
// by InviteAddressType
return {
addressType: 'mx',
address: user.userId,
displayName: user.displayName,
avatarMxc: user.avatarUrl,
isKnown: true,
}
});
// If the query isn't a user we know about, but is a
// valid address, add an entry for that
if (queryList.length == 0) {
const addrType = Invite.getAddressType(query);
if (addrType !== null) {
queryList.push({
addressType: addrType,
address: query,
isKnown: false,
});
}
}
}
this.setState({
@ -183,7 +224,7 @@ module.exports = React.createClass({
onSelected: function(index) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.state.queryList[index].userId);
inviteList.push(this.state.queryList[index]);
this.setState({
inviteList: inviteList,
queryList: [],
@ -218,10 +259,14 @@ module.exports = React.createClass({
return;
}
const addrTexts = addrs.map((addr) => {
return addr.address;
});
if (this.props.roomId) {
// Invite new user to a room
var self = this;
Invite.inviteMultipleToRoom(this.props.roomId, addrs)
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
.then(function(addrs) {
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
return self._showAnyInviteErrors(addrs, room);
@ -236,9 +281,9 @@ module.exports = React.createClass({
return null;
})
.done();
} else if (this._isDmChat(addrs)) {
} else if (this._isDmChat(addrTexts)) {
// Start the DM chat
createRoom({dmUserId: addrs[0]})
createRoom({dmUserId: addrTexts[0]})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -255,7 +300,7 @@ module.exports = React.createClass({
var room;
createRoom().then(function(roomId) {
room = MatrixClientPeg.get().getRoom(roomId);
return Invite.inviteMultipleToRoom(roomId, addrs);
return Invite.inviteMultipleToRoom(roomId, addrTexts);
})
.then(function(addrs) {
return self._showAnyInviteErrors(addrs, room);
@ -273,7 +318,7 @@ module.exports = React.createClass({
}
// Close - this will happen before the above, as that is async
this.props.onFinished(true, addrs);
this.props.onFinished(true, addrTexts);
},
_updateUserList: new rate_limited_func(function() {
@ -307,19 +352,27 @@ module.exports = React.createClass({
return true;
}
// split spaces in name and try matching constituent parts
var parts = name.split(" ");
for (var i = 0; i < parts.length; i++) {
if (parts[i].indexOf(query) === 0) {
return true;
}
// 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
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
if (expr.test(name)) {
return true;
}
return false;
},
_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
if (this.state.inviteList[i].toLowerCase() === uid) {
if (
this.state.inviteList[i].addressType == 'mx' &&
this.state.inviteList[i].address.toLowerCase() === uid
) {
return true;
}
}
@ -354,24 +407,37 @@ module.exports = React.createClass({
},
_addInputToList: function() {
const addrType = Invite.getAddressType(this.refs.textinput.value);
if (addrType !== null) {
const inviteList = this.state.inviteList.slice();
inviteList.push(this.refs.textinput.value.trim());
this.setState({
inviteList: inviteList,
queryList: [],
});
return inviteList;
} else {
const addressText = this.refs.textinput.value.trim();
const addrType = Invite.getAddressType(addressText);
const addrObj = {
addressType: addrType,
address: addressText,
isKnown: false,
};
if (addrType == null) {
this.setState({ error: true });
return null;
} else if (addrType == 'mx') {
const user = MatrixClientPeg.get().getUser(addrObj.address);
if (user) {
addrObj.displayName = user.displayName;
addrObj.avatarMxc = user.avatarUrl;
addrObj.isKnown = true;
}
}
const inviteList = this.state.inviteList.slice();
inviteList.push(addrObj);
this.setState({
inviteList: inviteList,
queryList: [],
});
return inviteList;
},
render: function() {
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var AddressSelector = sdk.getComponent("elements.AddressSelector");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
var query = [];
@ -404,11 +470,16 @@ module.exports = React.createClass({
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 {
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
Searching known users
</div>;
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
addressList={ this.state.queryList }
onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST } />
truncateAt={ TRUNCATE_QUERY_LIST }
header={ addressSelectorHeader }
/>
);
}
@ -417,9 +488,10 @@ module.exports = React.createClass({
<div className="mx_Dialog_title">
{this.props.title}
</div>
<div className="mx_ChatInviteDialog_cancel" onClick={this.onCancel} >
<AccessibleButton className="mx_ChatInviteDialog_cancel"
onClick={this.onCancel} >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</div>
</AccessibleButton>
<div className="mx_ChatInviteDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>

View file

@ -25,9 +25,10 @@ limitations under the License.
* });
*/
var React = require("react");
import React from 'react';
import sdk from '../../../index';
module.exports = React.createClass({
export default React.createClass({
displayName: 'ErrorDialog',
propTypes: {
title: React.PropTypes.string,
@ -49,20 +50,11 @@ module.exports = React.createClass({
};
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_ErrorDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title}>
<div className="mx_Dialog_content">
{this.props.description}
</div>
@ -71,7 +63,7 @@ module.exports = React.createClass({
{this.props.button}
</button>
</div>
</div>
</BaseDialog>
);
}
},
});

View file

@ -111,20 +111,9 @@ export default React.createClass({
});
},
_onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
if (!this.state.busy) {
this._onCancel();
}
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
if (this.state.submitButtonEnabled && !this.state.busy) {
this._onSubmit();
}
_onEnterPressed: function(e) {
if (this.state.submitButtonEnabled && !this.state.busy) {
this._onSubmit();
}
},
@ -171,6 +160,7 @@ export default React.createClass({
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let error = null;
if (this.state.errorText) {
@ -200,10 +190,11 @@ export default React.createClass({
);
return (
<div className="mx_InteractiveAuthDialog" onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_InteractiveAuthDialog"
onEnterPressed={this._onEnterPressed}
onFinished={this.props.onFinished}
title={this.props.title}
>
<div className="mx_Dialog_content">
<p>This operation requires additional authentication.</p>
{this._renderCurrentStage()}
@ -213,7 +204,7 @@ export default React.createClass({
{submitButton}
{cancelButton}
</div>
</div>
</BaseDialog>
);
},
});

View file

@ -1,61 +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.
*/
var React = require('react');
var dis = require("../../../dispatcher");
module.exports = React.createClass({
displayName: 'LogoutPrompt',
propTypes: {
onFinished: React.PropTypes.func,
},
logOut: function() {
dis.dispatch({action: 'logout'});
if (this.props.onFinished) {
this.props.onFinished();
}
},
cancelPrompt: function() {
if (this.props.onFinished) {
this.props.onFinished();
}
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.cancelPrompt();
}
},
render: function() {
return (
<div>
<div className="mx_Dialog_content">
Sign out?
</div>
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
<button onClick={this.cancelPrompt}>Cancel</button>
</div>
</div>
);
}
});

View file

@ -23,8 +23,9 @@ limitations under the License.
* });
*/
var React = require("react");
var dis = require("../../../dispatcher");
import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'NeedToRegisterDialog',
@ -54,11 +55,12 @@ module.exports = React.createClass({
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_NeedToRegisterDialog">
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_NeedToRegisterDialog"
onFinished={this.props.onFinished}
title={this.props.title}
>
<div className="mx_Dialog_content">
{this.props.description}
</div>
@ -70,7 +72,7 @@ module.exports = React.createClass({
Register
</button>
</div>
</div>
</BaseDialog>
);
}
},
});

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
import React from 'react';
import sdk from '../../../index';
module.exports = React.createClass({
export default React.createClass({
displayName: 'QuestionDialog',
propTypes: {
title: React.PropTypes.string,
@ -46,25 +47,13 @@ module.exports = React.createClass({
this.props.onFinished(false);
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
this.props.onFinished(true);
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_QuestionDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk }
title={this.props.title}
>
<div className="mx_Dialog_content">
{this.props.description}
</div>
@ -77,7 +66,7 @@ module.exports = React.createClass({
Cancel
</button>
</div>
</div>
</BaseDialog>
);
}
},
});

View file

@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
var sdk = require("../../../index.js");
var MatrixClientPeg = require("../../../MatrixClientPeg");
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
module.exports = React.createClass({
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetDisplayNameDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
@ -42,10 +47,6 @@ module.exports = React.createClass({
this.refs.input_value.select();
},
getValue: function() {
return this.state.value;
},
onValueChange: function(ev) {
this.setState({
value: ev.target.value
@ -54,16 +55,17 @@ module.exports = React.createClass({
onFormSubmit: function(ev) {
ev.preventDefault();
this.props.onFinished(true);
this.props.onFinished(true, this.state.value);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_SetDisplayNameDialog">
<div className="mx_Dialog_title">
Set a Display Name
</div>
<BaseDialog className="mx_SetDisplayNameDialog"
onFinished={this.props.onFinished}
title="Set a Display Name"
>
<div className="mx_Dialog_content">
Your display name is how you'll appear to others when you speak in rooms.<br/>
What would you like it to be?
@ -79,7 +81,7 @@ module.exports = React.createClass({
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</div>
</BaseDialog>
);
}
},
});

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
import React from 'react';
import sdk from '../../../index';
module.exports = React.createClass({
export default React.createClass({
displayName: 'TextInputDialog',
propTypes: {
title: React.PropTypes.string,
@ -27,7 +28,7 @@ module.exports = React.createClass({
value: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired
onFinished: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
@ -36,7 +37,7 @@ module.exports = React.createClass({
value: "",
description: "",
button: "OK",
focus: true
focus: true,
};
},
@ -55,25 +56,13 @@ module.exports = React.createClass({
this.props.onFinished(false);
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
this.props.onFinished(true, this.refs.textinput.value);
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_TextInputDialog">
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> {this.props.description} </label>
@ -90,7 +79,7 @@ module.exports = React.createClass({
{this.props.button}
</button>
</div>
</div>
</BaseDialog>
);
}
},
});