From 84a7fc16401eea018b74a8ee0ad6452c72bfc394 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 16:29:01 +0000 Subject: [PATCH 01/11] Tweak how command aliases are set This prevents multiple commands of the same name being returned in getCommandList() --- src/SlashCommands.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 83b4b52ffc..938bf062cb 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -318,7 +318,9 @@ var commands = { }; // helpful aliases -commands.j = commands.join; +var aliases = { + j: "join" +} module.exports = { /** @@ -338,6 +340,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); } From 42dc1be3410ee42466064b44e2f1286d53603757 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 16:29:30 +0000 Subject: [PATCH 02/11] fix descriptions a bit and sort the slash commands when tab-completing --- src/SlashCommands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 938bf062cb..ca3a010791 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; @@ -354,7 +354,7 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).map(function(cmdKey) { + return Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; }); } From 4430e16707e669eb7d29beb629cdc0d74f25ba50 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 16:29:41 +0000 Subject: [PATCH 03/11] apply CSS to slashcommand autocompletes --- src/components/views/rooms/TabCompleteBar.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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()} From d33d60691203efe54b7ff903db5776174c7d70c2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jan 2016 16:31:42 +0000 Subject: [PATCH 04/11] Only show uploads that are going to the current room Fixes #506 --- src/components/structures/UploadBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = { From dcfcc51f4c705a918e0b8e0545056feee5f01cbf Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jan 2016 17:28:53 +0000 Subject: [PATCH 05/11] Catch new invalid user name error added in https://github.com/matrix-org/synapse/pull/499 and https://github.com/matrix-org/matrix-doc/pull/263 --- src/Signup.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Signup.js b/src/Signup.js index 42468959fe..2d823b62ae 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_USER_NAME') { + 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) { From 66bc30c0bc28de4c038a933e014ec5002adf9d5d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 17:33:52 +0000 Subject: [PATCH 06/11] Add /me to the list --- src/SlashCommands.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index ca3a010791..4eb2adad5d 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -354,8 +354,12 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).sort().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; } }; From 51ce76aeab75d760c1a26badf901ab818dd741a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 10:08:16 +0000 Subject: [PATCH 07/11] M_INVALID_USERNAME to be consistent with param name --- src/Signup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Signup.js b/src/Signup.js index 2d823b62ae..fbc2a09634 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -152,7 +152,7 @@ class Register extends Signup { } else { if (error.errcode === 'M_USER_IN_USE') { throw new Error("Username in use"); - } else if (error.errcode == 'M_INVALID_USER_NAME') { + } 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!"); From 02e41450b4cf220a9d0ed4be9a7b6da41b462362 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 13:31:41 +0000 Subject: [PATCH 08/11] Do (more) client side validation of registration parameters. --- package.json | 3 +- .../structures/login/Registration.js | 9 ++ .../views/login/RegistrationForm.js | 133 +++++++++++++++--- 3 files changed, 124 insertions(+), 21 deletions(-) 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/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..d59f6556d7 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,31 +57,14 @@ 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() - - 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, @@ -89,13 +79,110 @@ module.exports = React.createClass({ } }, + 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_MISSING" + ); + } + break; + case FIELD_PASSWORD_CONFIRM: + if (pwd1 == '') { + this.markFieldValid( + field_id, false, + "RegistrationForm.ERR_PASSWORD_MISSING" + ); + } else if (pwd1 != pwd2) { + this.markFieldValid( + field_id, false, + "RegistrationForm.ERR_PASSWORD_LENGTH" + ); + } else { + this.markFieldValid(field_id, true); + } + 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 +198,19 @@ module.exports = React.createClass({



{registerButton} From 2638ee974ea70268599fb06720291908efeb09ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 15:18:55 +0000 Subject: [PATCH 09/11] Add warning on inviting a user if sharing history with new users. Fixes https://github.com/vector-im/vector-web/issues/60 --- src/components/views/rooms/MemberList.js | 53 ++++++++++++++++++++---- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index eac5466e88..f4073035af 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 }); From ecfdd3593c23c07c37b7dcf1e99d623dcc3aae51 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 16:17:27 +0000 Subject: [PATCH 10/11] Validate everything on form submit. Don't use pwd1 where we didn't define it & fix some error codes. --- .../views/login/RegistrationForm.js | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index d59f6556d7..58d7ca3aab 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -65,20 +65,46 @@ module.exports = React.createClass({ onSubmit: function(ev) { ev.preventDefault(); - var promise = this.props.onRegisterClick({ - username: this.refs.username.value.trim(), - password: pwd1, - email: this.refs.email.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); - 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() @@ -120,25 +146,18 @@ module.exports = React.createClass({ this.markFieldValid( field_id, false, - "RegistrationForm.ERR_PASSWORD_MISSING" - ); - } - break; - case FIELD_PASSWORD_CONFIRM: - if (pwd1 == '') { - this.markFieldValid( - field_id, false, - "RegistrationForm.ERR_PASSWORD_MISSING" - ); - } else if (pwd1 != pwd2) { - 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; } }, From a90f63f72ccc324daa687eb6d92b1bf843948cb2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 18:49:12 +0000 Subject: [PATCH 11/11] null check --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d0bfdf77a8..b20f615a3f 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.getEventReadUpTo(MatrixClientPeg.get().credentials.userId), + readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null, readMarkerGhostEventId: undefined } },