merge develop

This commit is contained in:
Matthew Hodgson 2016-01-17 23:31:21 +00:00
commit f3aeda7af9
9 changed files with 219 additions and 47 deletions

View file

@ -34,7 +34,8 @@
"react-dom": "^0.14.2", "react-dom": "^0.14.2",
"react-gemini-scrollbar": "^2.0.1", "react-gemini-scrollbar": "^2.0.1",
"sanitize-html": "^1.11.1", "sanitize-html": "^1.11.1",
"velocity-animate": "^1.2.3" "velocity-animate": "^1.2.3",
"velocity-ui-pack": "^1.2.2"
}, },
"//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder", "//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder",
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472", "//depsbuglink": "https://github.com/webpack/webpack/issues/1472",

View file

@ -152,6 +152,8 @@ class Register extends Signup {
} else { } else {
if (error.errcode === 'M_USER_IN_USE') { if (error.errcode === 'M_USER_IN_USE') {
throw new Error("Username in use"); throw new Error("Username in use");
} else if (error.errcode == 'M_INVALID_USERNAME') {
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
} else if (error.httpStatus == 401) { } else if (error.httpStatus == 401) {
throw new Error("Authorisation failed!"); throw new Error("Authorisation failed!");
} else if (error.httpStatus >= 400 && error.httpStatus < 500) { } else if (error.httpStatus >= 400 && error.httpStatus < 500) {

View file

@ -69,7 +69,7 @@ var commands = {
}), }),
// Changes the colorscheme of your current room // Changes the colorscheme of your current room
tint: new Command("tint", "<primaryColor> [<secondaryColor>]", function(room_id, args) { tint: new Command("tint", "<color1> [<color2>]", function(room_id, args) {
if (args) { if (args) {
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
if (matches) { if (matches) {
@ -89,7 +89,7 @@ var commands = {
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
encrypt: new Command("encrypt", "<on/off>", function(room_id, args) { encrypt: new Command("encrypt", "<on|off>", function(room_id, args) {
if (args == "on") { if (args == "on") {
var client = MatrixClientPeg.get(); var client = MatrixClientPeg.get();
var members = client.getRoom(room_id).currentState.members; var members = client.getRoom(room_id).currentState.members;
@ -316,7 +316,9 @@ var commands = {
}; };
// helpful aliases // helpful aliases
commands.j = commands.join; var aliases = {
j: "join"
}
module.exports = { module.exports = {
/** /**
@ -336,6 +338,9 @@ module.exports = {
var cmd = bits[1].substring(1).toLowerCase(); var cmd = bits[1].substring(1).toLowerCase();
var args = bits[3]; var args = bits[3];
if (cmd === "me") return null; if (cmd === "me") return null;
if (aliases[cmd]) {
cmd = aliases[cmd];
}
if (commands[cmd]) { if (commands[cmd]) {
return commands[cmd].run(roomId, args); return commands[cmd].run(roomId, args);
} }
@ -347,8 +352,12 @@ module.exports = {
}, },
getCommandList: function() { getCommandList: function() {
return Object.keys(commands).map(function(cmdKey) { // Return all the commands plus /me which isn't handled like normal commands
var cmds = Object.keys(commands).sort().map(function(cmdKey) {
return commands[cmdKey]; return commands[cmdKey];
}); })
cmds.push(new Command("me", "<action>", function(){}));
return cmds;
} }
}; };

View file

@ -79,7 +79,7 @@ module.exports = React.createClass({
hasUnsentMessages: this._hasUnsentMessages(room), hasUnsentMessages: this._hasUnsentMessages(room),
callState: null, callState: null,
guestsCanJoin: false, guestsCanJoin: false,
readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : undefined, readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null,
readMarkerGhostEventId: undefined readMarkerGhostEventId: undefined
} }
}, },

View file

@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
} }
} }
if (!upload) { if (!upload) {
upload = uploads[0]; return <div />
} }
var innerProgressStyle = { var innerProgressStyle = {

View file

@ -159,6 +159,15 @@ module.exports = React.createClass({
case "RegistrationForm.ERR_PASSWORD_LENGTH": case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`; errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
break; break;
case "RegistrationForm.ERR_EMAIL_INVALID":
errMsg = "This doesn't look like a valid email address";
break;
case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
break;
case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = "You need to enter a user name";
break;
default: default:
console.error("Unknown error code: %s", errCode); console.error("Unknown error code: %s", errCode);
errMsg = "An unknown error occurred."; errMsg = "An unknown error occurred.";

View file

@ -17,8 +17,15 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var Velocity = require('velocity-animate');
require('velocity-ui-pack');
var sdk = require('../../../index'); var sdk = require('../../../index');
var FIELD_EMAIL = 'field_email';
var FIELD_USERNAME = 'field_username';
var FIELD_PASSWORD = 'field_password';
var FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
/** /**
* A pure UI component which displays a registration form. * A pure UI component which displays a registration form.
*/ */
@ -50,52 +57,151 @@ module.exports = React.createClass({
email: this.props.defaultEmail, email: this.props.defaultEmail,
username: this.props.defaultUsername, username: this.props.defaultUsername,
password: null, password: null,
passwordConfirm: null passwordConfirm: null,
fieldValid: {}
}; };
}, },
onSubmit: function(ev) { onSubmit: function(ev) {
ev.preventDefault(); ev.preventDefault();
var pwd1 = this.refs.password.value.trim(); // validate everything, in reverse order so
var pwd2 = this.refs.passwordConfirm.value.trim() // the error that ends up being displayed
// is the one from the first invalid field.
// It's not super ideal that this just calls
// onError once for each invalid field.
this.validateField(FIELD_PASSWORD_CONFIRM);
this.validateField(FIELD_PASSWORD);
this.validateField(FIELD_USERNAME);
this.validateField(FIELD_EMAIL);
var errCode; if (this.allFieldsValid()) {
if (!pwd1 || !pwd2) { var promise = this.props.onRegisterClick({
errCode = "RegistrationForm.ERR_PASSWORD_MISSING"; username: this.refs.username.value.trim(),
} password: this.refs.password.value.trim(),
else if (pwd1 !== pwd2) { email: this.refs.email.value.trim()
errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH";
}
else if (pwd1.length < this.props.minPasswordLength) {
errCode = "RegistrationForm.ERR_PASSWORD_LENGTH";
}
if (errCode) {
this.props.onError(errCode);
return;
}
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim(),
password: pwd1,
email: this.refs.email.value.trim()
});
if (promise) {
ev.target.disabled = true;
promise.finally(function() {
ev.target.disabled = false;
}); });
if (promise) {
ev.target.disabled = true;
promise.finally(function() {
ev.target.disabled = false;
});
}
} }
}, },
/**
* Returns true if all fields were valid last time
* they were validated.
*/
allFieldsValid: function() {
var keys = Object.keys(this.state.fieldValid);
for (var i = 0; i < keys.length; ++i) {
if (this.state.fieldValid[keys[i]] == false) {
return false;
}
}
return true;
},
validateField: function(field_id) {
var pwd1 = this.refs.password.value.trim();
var pwd2 = this.refs.passwordConfirm.value.trim()
switch (field_id) {
case FIELD_EMAIL:
this.markFieldValid(
field_id,
this.refs.email.value == '' || !!this.refs.email.value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i),
"RegistrationForm.ERR_EMAIL_INVALID"
);
break;
case FIELD_USERNAME:
// XXX: SPEC-1
if (encodeURIComponent(this.refs.username.value) != this.refs.username.value) {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_USERNAME_INVALID"
);
} else if (this.refs.username.value == '') {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_USERNAME_BLANK"
);
} else {
this.markFieldValid(field_id, true);
}
break;
case FIELD_PASSWORD:
if (pwd1 == '') {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_PASSWORD_MISSING"
);
} else if (pwd1.length < this.props.minPasswordLength) {
this.markFieldValid(
field_id,
false,
"RegistrationForm.ERR_PASSWORD_LENGTH"
);
} else {
this.markFieldValid(field_id, true);
}
break;
case FIELD_PASSWORD_CONFIRM:
this.markFieldValid(
field_id, pwd1 == pwd2,
"RegistrationForm.ERR_PASSWORD_MISMATCH"
);
break;
}
},
markFieldValid: function(field_id, val, error_code) {
var fieldValid = this.state.fieldValid;
fieldValid[field_id] = val;
this.setState({fieldValid: fieldValid});
if (!val) {
Velocity(this.fieldElementById(field_id), "callout.shake", 300);
this.props.onError(error_code);
}
},
fieldElementById(field_id) {
switch (field_id) {
case FIELD_EMAIL:
return this.refs.email;
case FIELD_USERNAME:
return this.refs.username;
case FIELD_PASSWORD:
return this.refs.password;
case FIELD_PASSWORD_CONFIRM:
return this.refs.passwordConfirm;
}
},
_styleField: function(field_id, baseStyle) {
var style = baseStyle || {};
if (this.state.fieldValid[field_id] === false) {
style['borderColor'] = 'red';
}
return style;
},
render: function() { render: function() {
var self = this;
var emailSection, registerButton; var emailSection, registerButton;
if (this.props.showEmail) { if (this.props.showEmail) {
emailSection = ( emailSection = (
<input className="mx_Login_field" type="text" ref="email" <input className="mx_Login_field" type="text" ref="email"
autoFocus={true} placeholder="Email address" autoFocus={true} placeholder="Email address"
defaultValue={this.state.email} /> defaultValue={this.state.email}
style={this._styleField(FIELD_EMAIL)}
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
); );
} }
if (this.props.onRegisterClick) { if (this.props.onRegisterClick) {
@ -111,13 +217,19 @@ module.exports = React.createClass({
<br /> <br />
<input className="mx_Login_field" type="text" ref="username" <input className="mx_Login_field" type="text" ref="username"
placeholder="User name" defaultValue={this.state.username} placeholder="User name" defaultValue={this.state.username}
style={this._styleField(FIELD_USERNAME)}
onBlur={function() {self.validateField(FIELD_USERNAME)}}
disabled={this.props.disableUsernameChanges} /> disabled={this.props.disableUsernameChanges} />
<br /> <br />
<input className="mx_Login_field" type="password" ref="password" <input className="mx_Login_field" type="password" ref="password"
style={this._styleField(FIELD_PASSWORD)}
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
placeholder="Password" defaultValue={this.state.password} /> placeholder="Password" defaultValue={this.state.password} />
<br /> <br />
<input className="mx_Login_field" type="password" ref="passwordConfirm" <input className="mx_Login_field" type="password" ref="passwordConfirm"
placeholder="Confirm password" placeholder="Confirm password"
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
defaultValue={this.state.passwordConfirm} /> defaultValue={this.state.passwordConfirm} />
<br /> <br />
{registerButton} {registerButton}

View file

@ -16,12 +16,18 @@ limitations under the License.
var React = require('react'); var React = require('react');
var classNames = require('classnames'); var classNames = require('classnames');
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
var q = require('q');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var sdk = require('../../../index'); var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar'); var GeminiScrollbar = require('react-gemini-scrollbar');
var INITIAL_LOAD_NUM_MEMBERS = 50; var INITIAL_LOAD_NUM_MEMBERS = 50;
var SHARE_HISTORY_WARNING = "Newly invited users will see the history of this room. "+
"If you'd prefer invited users not to see messages that were sent before they joined, "+
"turn off, 'Share message history with new users' in the settings for this room.";
var shown_invite_warning_this_session = false;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberList', displayName: 'MemberList',
@ -132,12 +138,41 @@ module.exports = React.createClass({
return; return;
} }
var promise; var invite_defer = 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) {
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);
}
}
});
} else {
invite_defer.resolve();
}
var promise = invite_defer.promise;;
if (isEmailAddress) { if (isEmailAddress) {
promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText); promise = promise.then(function() {
MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText);
});
} }
else { else {
promise = MatrixClientPeg.get().invite(this.props.roomId, inputText); promise = promise.then(function() {
MatrixClientPeg.get().invite(self.props.roomId, inputText);
});
} }
self.setState({ self.setState({
@ -152,11 +187,13 @@ module.exports = React.createClass({
inviting: false inviting: false
}); });
}, function(err) { }, function(err) {
console.error("Failed to invite: %s", JSON.stringify(err)); if (err !== null) {
Modal.createDialog(ErrorDialog, { console.error("Failed to invite: %s", JSON.stringify(err));
title: "Server error whilst inviting", Modal.createDialog(ErrorDialog, {
description: err.message title: "Server error whilst inviting",
}); description: err.message
});
}
self.setState({ self.setState({
inviting: false inviting: false
}); });

View file

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'TabCompleteBar', displayName: 'TabCompleteBar',
@ -31,8 +32,9 @@ module.exports = React.createClass({
<div className="mx_TabCompleteBar"> <div className="mx_TabCompleteBar">
{this.props.entries.map(function(entry, i) { {this.props.entries.map(function(entry, i) {
return ( return (
<div key={entry.getKey() || i + ""} className="mx_TabCompleteBar_item" <div key={entry.getKey() || i + ""}
onClick={entry.onClick.bind(entry)} > className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={entry.onClick.bind(entry)} >
{entry.getImageJsx()} {entry.getImageJsx()}
<span className="mx_TabCompleteBar_text"> <span className="mx_TabCompleteBar_text">
{entry.getText()} {entry.getText()}