merge develop

pull/21833/head
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-gemini-scrollbar": "^2.0.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",
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",

View File

@ -152,6 +152,8 @@ class Register extends Signup {
} else {
if (error.errcode === 'M_USER_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) {
throw new Error("Authorisation failed!");
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {

View File

@ -69,7 +69,7 @@ var commands = {
}),
// 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) {
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) {
@ -89,7 +89,7 @@ var commands = {
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") {
var client = MatrixClientPeg.get();
var members = client.getRoom(room_id).currentState.members;
@ -316,7 +316,9 @@ var commands = {
};
// helpful aliases
commands.j = commands.join;
var aliases = {
j: "join"
}
module.exports = {
/**
@ -336,6 +338,9 @@ module.exports = {
var cmd = bits[1].substring(1).toLowerCase();
var args = bits[3];
if (cmd === "me") return null;
if (aliases[cmd]) {
cmd = aliases[cmd];
}
if (commands[cmd]) {
return commands[cmd].run(roomId, args);
}
@ -347,8 +352,12 @@ module.exports = {
},
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];
});
})
cmds.push(new Command("me", "<action>", function(){}));
return cmds;
}
};

View File

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

View File

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

View File

@ -159,6 +159,15 @@ module.exports = React.createClass({
case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
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:
console.error("Unknown error code: %s", errCode);
errMsg = "An unknown error occurred.";

View File

@ -17,8 +17,15 @@ limitations under the License.
'use strict';
var React = require('react');
var Velocity = require('velocity-animate');
require('velocity-ui-pack');
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.
*/
@ -50,52 +57,151 @@ module.exports = React.createClass({
email: this.props.defaultEmail,
username: this.props.defaultUsername,
password: null,
passwordConfirm: null
passwordConfirm: null,
fieldValid: {}
};
},
onSubmit: function(ev) {
ev.preventDefault();
var pwd1 = this.refs.password.value.trim();
var pwd2 = this.refs.passwordConfirm.value.trim()
// validate everything, in reverse order so
// 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 (!pwd1 || !pwd2) {
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
}
else if (pwd1 !== pwd2) {
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 (this.allFieldsValid()) {
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim(),
password: this.refs.password.value.trim(),
email: this.refs.email.value.trim()
});
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() {
var self = this;
var emailSection, registerButton;
if (this.props.showEmail) {
emailSection = (
<input className="mx_Login_field" type="text" ref="email"
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) {
@ -111,13 +217,19 @@ module.exports = React.createClass({
<br />
<input className="mx_Login_field" type="text" ref="username"
placeholder="User name" defaultValue={this.state.username}
style={this._styleField(FIELD_USERNAME)}
onBlur={function() {self.validateField(FIELD_USERNAME)}}
disabled={this.props.disableUsernameChanges} />
<br />
<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} />
<br />
<input className="mx_Login_field" type="password" ref="passwordConfirm"
placeholder="Confirm password"
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
defaultValue={this.state.passwordConfirm} />
<br />
{registerButton}

View File

@ -16,12 +16,18 @@ limitations under the License.
var React = require('react');
var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
var q = require('q');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar');
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({
displayName: 'MemberList',
@ -132,12 +138,41 @@ module.exports = React.createClass({
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) {
promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText);
promise = promise.then(function() {
MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText);
});
}
else {
promise = MatrixClientPeg.get().invite(this.props.roomId, inputText);
promise = promise.then(function() {
MatrixClientPeg.get().invite(self.props.roomId, inputText);
});
}
self.setState({
@ -152,11 +187,13 @@ module.exports = React.createClass({
inviting: false
});
}, function(err) {
console.error("Failed to invite: %s", JSON.stringify(err));
Modal.createDialog(ErrorDialog, {
title: "Server error whilst inviting",
description: err.message
});
if (err !== null) {
console.error("Failed to invite: %s", JSON.stringify(err));
Modal.createDialog(ErrorDialog, {
title: "Server error whilst inviting",
description: err.message
});
}
self.setState({
inviting: false
});

View File

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