diff --git a/package.json b/package.json index 9c2c645ea2..ac72744af4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Signup.js b/src/Signup.js index 42468959fe..fbc2a09634 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -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) { diff --git a/src/SlashCommands.js b/src/SlashCommands.js index e213f31a88..d4e7df3a16 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -69,7 +69,7 @@ var commands = { }), // Changes the colorscheme of your current room - tint: new Command("tint", " []", function(room_id, args) { + tint: new Command("tint", " []", 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", "", function(room_id, args) { + encrypt: new Command("encrypt", "", 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", "", function(){})); + + return cmds; } }; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5ce5529e9c..5604565dd7 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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 } }, diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 12e502026f..eda843eb8a 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar', } } if (!upload) { - upload = uploads[0]; + return
} var innerProgressStyle = { diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f89d65d740..7b2808c72a 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -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."; diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 534464a4ae..58d7ca3aab 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -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 = ( + 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({



{registerButton} diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 535c194ea1..3e3221992e 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -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 }); diff --git a/src/components/views/rooms/TabCompleteBar.js b/src/components/views/rooms/TabCompleteBar.js index c640d6aa5b..ea74706f29 100644 --- a/src/components/views/rooms/TabCompleteBar.js +++ b/src/components/views/rooms/TabCompleteBar.js @@ -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({
{this.props.entries.map(function(entry, i) { return ( -
+
{entry.getImageJsx()} {entry.getText()}