From dfe745c9e606ee28b903319ab5559ac736009e2b Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 4 Jul 2016 17:15:15 +0100 Subject: [PATCH 01/73] Add a JS code style doc --- code_style.rst | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 code_style.rst diff --git a/code_style.rst b/code_style.rst new file mode 100644 index 0000000000..ff48bc381d --- /dev/null +++ b/code_style.rst @@ -0,0 +1,116 @@ +Matrix JavaScript/ECMAScript Style Guide +======================================== + +The intention of this guide is to make Matrix's JavaScript codebase clean, +consistent with other popular JavaScript styles and consistent with the rest of +the Matrix codebase. For reference, the Matrix Python style guide can be found +at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst + +General Style +------------- + +- 4 spaces to indent, for consistency with Matrix Python. +- Max line width: 79 chars (with flexibility to overflow by a "few chars" if + the overflowing content is not semantically significant and avoids an + explosion of vertical whitespace). +- No trailing whitespace at end of lines. +- Don't indent empty lines. +- One newline at the end of the file. +- Unix newlines, never `\r` +- Indent similar to our python code: break up long lines at logical boundaries, + more than one argument on a line is OK +- Use semicolons, for consistency with node. +- UpperCamelCase for class and type names +- lowerCamelCase for functions and variables. +- Single line ternary operators are fine. +- UPPER_CAMEL_CASE for constants +- Single quotes for strings, for consistency with most JavaScript styles:: + "bad" // Bad + 'good' // Good +- Use parentheses instead of '\\' for line continuation where ever possible +- Open braces on the same line (consistent with Node):: + if (x) { + console.log("I am a fish"); // Good + } + + if (x) + { + console.log("I am a fish"); // Bad + } +- Spaces after `if`, `for`, `else` etc, no space around the condition:: + if (x) { + console.log("I am a fish"); // Good + } + + if(x) { + console.log("I am a fish"); // Bad + } + + if ( x ) { + console.log("I am a fish"); // Bad + } +- Declare one variable per var statement (consistent with Node). Unless they + are simple and closely related. If you put the next declaration on a new line, + treat yourself to another `var`:: + var key = "foo", + comparator = function(x, y) { + return x - y; + }; // Bad + + var key = "foo"; + var comparator = function(x, y) { + return x - y; + }; // Good + + var x = 0, y = 0; // Fine + + var x = 0; + var y = 0; // Also fine +- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:: + if (x) return true; // Fine + + if (x) { + return true; // Also fine + } + + if (x) + return true; // Not fine +- Terminate all multi-line lists with commas:: + var mascots = [ + "Patrick", + "Shirley", + "Colin", + "Susan", + "Sir Arthur David" // Bad + ]; + + var mascots = [ + "Patrick", + "Shirley", + "Colin", + "Susan", + "Sir Arthur David", // Good + ]; +- Use `null`, `undefined` etc consistently with node: + Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. + When something is intentionally missing or removed, set it to null. + Don't set things to undefined. Reserve that value to mean "not yet set to anything." + Boolean objects are verboten. +- Use JSDoc + +ECMAScript +---------- +- Use `let` for variables and `const` for constants. This sounds obvious, but it isn't: the ES6 `const` keyword + could be used for assign-once variables, however this guide advises against doing so on the grounds that it + confuses them with constants. +- Be careful migrating files to newer syntax. + - Don't mix `require` and `import` in the same file. Either stick to the old style or change them all. + - Likewise, don't mix things like class properties and `MyClass.prototype.MY_CONSTANT = 42;` + - Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an + arrow function, they probably all should be. +- Apart from that, newer ES features should be used whenever the author deems them to be appropriate. +- Flow annotations are welcome and encouraged. + +React +----- +- Use ES6 classes, although bear in mind a lot of code uses createClass. From 56c73b68a9abec549b03232be8dd0e3d2e3f1f9a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 4 Jul 2016 17:23:38 +0100 Subject: [PATCH 02/73] Use markdown because the rst wasn't formatting and we use md for everything else in this repo, and in a document that talks about consistency... --- code_style.rst => code_style.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) rename code_style.rst => code_style.md (94%) diff --git a/code_style.rst b/code_style.md similarity index 94% rename from code_style.rst rename to code_style.md index ff48bc381d..eea23aed9c 100644 --- a/code_style.rst +++ b/code_style.md @@ -8,7 +8,6 @@ at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst General Style ------------- - - 4 spaces to indent, for consistency with Matrix Python. - Max line width: 79 chars (with flexibility to overflow by a "few chars" if the overflowing content is not semantically significant and avoids an @@ -28,7 +27,8 @@ General Style "bad" // Bad 'good' // Good - Use parentheses instead of '\\' for line continuation where ever possible -- Open braces on the same line (consistent with Node):: +- Open braces on the same line (consistent with Node): + ``` if (x) { console.log("I am a fish"); // Good } @@ -37,7 +37,9 @@ General Style { console.log("I am a fish"); // Bad } -- Spaces after `if`, `for`, `else` etc, no space around the condition:: + ``` +- Spaces after `if`, `for`, `else` etc, no space around the condition: + ``` if (x) { console.log("I am a fish"); // Good } @@ -49,9 +51,11 @@ General Style if ( x ) { console.log("I am a fish"); // Bad } + ``` - Declare one variable per var statement (consistent with Node). Unless they are simple and closely related. If you put the next declaration on a new line, - treat yourself to another `var`:: + treat yourself to another `var`: + ``` var key = "foo", comparator = function(x, y) { return x - y; @@ -66,7 +70,9 @@ General Style var x = 0; var y = 0; // Also fine -- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:: + ``` +- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: + ``` if (x) return true; // Fine if (x) { @@ -75,7 +81,9 @@ General Style if (x) return true; // Not fine -- Terminate all multi-line lists with commas:: + ``` +- Terminate all multi-line lists with commas: + ``` var mascots = [ "Patrick", "Shirley", @@ -91,6 +99,7 @@ General Style "Susan", "Sir Arthur David", // Good ]; + ``` - Use `null`, `undefined` etc consistently with node: Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. When something is intentionally missing or removed, set it to null. From e2c473b366e9494d4096a16a52f96df91f9dcc3d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 6 Jul 2016 15:22:06 +0100 Subject: [PATCH 03/73] Error on registration if email taken Use the new register-specific request token endpoint (https://github.com/matrix-org/matrix-js-sdk/pull/147) and catch the error that it gives if the email is already in use. Also add initial values to the registration form so we can reload it after the error without all the values disappearing, and split out the guest username parameter which was previously called defaultUsername. --- src/Signup.js | 5 +++- src/SignupStages.js | 6 ++--- .../structures/login/Registration.js | 14 ++++++++--- .../views/login/RegistrationForm.js | 24 +++++++++---------- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 4518955d95..5aadd94701 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -152,7 +152,10 @@ class Register extends Signup { console.log("Active flow => %s", JSON.stringify(flow)); var flowStage = self.firstUncompletedStage(flow); if (flowStage != self.activeStage) { - return self.startStage(flowStage); + return self.startStage(flowStage).catch(function(err) { + self.setStep('START'); + throw err; + }); } } } diff --git a/src/SignupStages.js b/src/SignupStages.js index 1c5c48ddd6..2b0d163a08 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -170,7 +170,7 @@ class EmailIdentityStage extends Stage { encodeURIComponent(this.signupInstance.getServerData().session); var self = this; - return this.client.requestEmailToken( + return this.client.requestRegisterEmailToken( this.signupInstance.email, this.clientSecret, 1, // TODO: Multiple send attempts? @@ -186,8 +186,8 @@ class EmailIdentityStage extends Stage { var e = { isFatal: true }; - if (error.errcode == 'THREEPID_IN_USE') { - e.message = "Email in use"; + if (error.errcode == 'M_THREEPID_IN_USE') { + e.message = "This email address is already registered"; } else { e.message = 'Unable to contact the given identity server'; } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 2f15a3b5df..4615031760 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -54,6 +54,9 @@ module.exports = React.createClass({ return { busy: false, errorText: null, + formVals: { + email: this.props.email, + }, }; }, @@ -108,7 +111,8 @@ module.exports = React.createClass({ var self = this; this.setState({ errorText: "", - busy: true + busy: true, + formVals: formVals, }); if (formVals.username !== this.props.username) { @@ -228,11 +232,15 @@ module.exports = React.createClass({ break; // NOP case "Register.START": case "Register.STEP_m.login.dummy": + // NB. Our 'username' prop is specifically for upgrading + // a guest account registerStep = ( diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index a172d77bb4..17827d5b46 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -37,6 +37,8 @@ module.exports = React.createClass({ propTypes: { defaultEmail: React.PropTypes.string, defaultUsername: React.PropTypes.string, + defaultPassword: React.PropTypes.string, + guestUsername: React.PropTypes.string, showEmail: React.PropTypes.bool, minPasswordLength: React.PropTypes.number, onError: React.PropTypes.func, @@ -55,10 +57,6 @@ module.exports = React.createClass({ getInitialState: function() { return { - email: this.props.defaultEmail, - username: null, - password: null, - passwordConfirm: null, fieldValid: {} }; }, @@ -103,7 +101,7 @@ module.exports = React.createClass({ _doSubmit: function() { var promise = this.props.onRegisterClick({ - username: this.refs.username.value.trim() || this.props.defaultUsername, + username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), email: this.refs.email.value.trim() }); @@ -144,7 +142,7 @@ module.exports = React.createClass({ break; case FIELD_USERNAME: // XXX: SPEC-1 - var username = this.refs.username.value.trim() || this.props.defaultUsername; + var username = this.refs.username.value.trim() || this.props.guestUsername; if (encodeURIComponent(username) != username) { this.markFieldValid( field_id, @@ -225,7 +223,7 @@ module.exports = React.createClass({ emailSection = ( ); @@ -237,8 +235,8 @@ module.exports = React.createClass({ } var placeholderUserName = "User name"; - if (this.props.defaultUsername) { - placeholderUserName += " (default: " + this.props.defaultUsername + ")" + if (this.props.guestUsername) { + placeholderUserName += " (default: " + this.props.guestUsername + ")" } return ( @@ -247,23 +245,23 @@ module.exports = React.createClass({ {emailSection}

- { this.props.defaultUsername ? + { this.props.guestUsername ?
Setting a user name will create a fresh account
: null } + placeholder="Password" defaultValue={this.props.defaultPassword} />
+ defaultValue={this.props.defaultPassword} />
{registerButton} From 5c879d786e1e8b51ea24b6f8f3a6278d79717800 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 11:23:08 +0100 Subject: [PATCH 04/73] Doc the default params / guestUsername props --- src/components/views/login/RegistrationForm.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 17827d5b46..ad3526c598 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -35,10 +35,16 @@ module.exports = React.createClass({ displayName: 'RegistrationForm', propTypes: { + // Values pre-filled in the input boxes when the component loads defaultEmail: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, + + // A username that will be used if no username is enetered. + // Specifying this param will also warn the user that enetering + // a different username will cause a fresh account to be generated. guestUsername: React.PropTypes.string, + showEmail: React.PropTypes.bool, minPasswordLength: React.PropTypes.number, onError: React.PropTypes.func, From a6b04c462e26ff32d58dfdfdfc9b103636afe1fa Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 11:26:35 +0100 Subject: [PATCH 05/73] Comment how we're remembering form vals --- src/components/structures/login/Registration.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 4615031760..5126965407 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -54,6 +54,13 @@ module.exports = React.createClass({ return { busy: false, errorText: null, + // We remember the values entered by the user because + // the registration form will be unmounted during the + // course of registration, but if there's an error we + // want to bring back the registration form with the + // values the user enetered still in it. We can keep + // them in this component's state since this component + // persist for the duration of the registration process. formVals: { email: this.props.email, }, From 04728ae03bc041a487bd22b8d943c8a983ff794b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 12:09:02 +0100 Subject: [PATCH 06/73] PR fixes + more general notes --- code_style.md | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/code_style.md b/code_style.md index eea23aed9c..4a42597b7a 100644 --- a/code_style.md +++ b/code_style.md @@ -6,6 +6,19 @@ consistent with other popular JavaScript styles and consistent with the rest of the Matrix codebase. For reference, the Matrix Python style guide can be found at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst +This document reflects how we would like Matrix JavaScript code to look, with +acknowledgement that a significant amount of code is written to older +standards. + +Write applications in modern ECMAScript and use a transpiler where necessary to +target older platforms. When writing library code, consider carefully whether +to write in ES5 to allow all JavaScript application to use the code directly or +writing in modern ECMAScript and using a transpile step to generate the file +that applications can then include. There are significant benefits in being +able to use modern ECMAScript, although the tooling for doing so can be awkward +for library code, especially with regard to translating source maps and line +number throgh from the original code to the final application. + General Style ------------- - 4 spaces to indent, for consistency with Matrix Python. @@ -23,10 +36,12 @@ General Style - lowerCamelCase for functions and variables. - Single line ternary operators are fine. - UPPER_CAMEL_CASE for constants -- Single quotes for strings, for consistency with most JavaScript styles:: +- Single quotes for strings by default, for consistency with most JavaScript styles: + ``` "bad" // Bad 'good' // Good -- Use parentheses instead of '\\' for line continuation where ever possible + ``` +- Use parentheses or `\`` instead of '\\' for line continuation where ever possible - Open braces on the same line (consistent with Node): ``` if (x) { @@ -82,7 +97,7 @@ General Style if (x) return true; // Not fine ``` -- Terminate all multi-line lists with commas: +- Terminate all multi-line lists with commas (if using a transpiler): ``` var mascots = [ "Patrick", @@ -103,6 +118,14 @@ General Style - Use `null`, `undefined` etc consistently with node: Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. When something is intentionally missing or removed, set it to null. + If returning a boolean, type coerce: + ``` + function hasThings() { + return !!length; // bad + return new Boolean(length); // REALLY bad + return Boolean(length); // good + } + ``` Don't set things to undefined. Reserve that value to mean "not yet set to anything." Boolean objects are verboten. - Use JSDoc @@ -123,3 +146,12 @@ ECMAScript React ----- - Use ES6 classes, although bear in mind a lot of code uses createClass. +- Pull out functions in props to the class, generally as specific event handlers: + ``` + // Bad + {doStuff();}}> // Equally bad + // Better + // Best + ``` +- Think about whether your component really needs state: are you duplicating + information in component state that could be derived from the model? From 345ed04ba9e362d56600776b9bc4595a2ffe2354 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 7 Jul 2016 13:03:27 +0100 Subject: [PATCH 07/73] Less enetering --- src/components/structures/login/Registration.js | 2 +- src/components/views/login/RegistrationForm.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 5126965407..423d62933f 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -58,7 +58,7 @@ module.exports = React.createClass({ // the registration form will be unmounted during the // course of registration, but if there's an error we // want to bring back the registration form with the - // values the user enetered still in it. We can keep + // values the user entered still in it. We can keep // them in this component's state since this component // persist for the duration of the registration process. formVals: { diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index ad3526c598..39c1acc625 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -40,8 +40,8 @@ module.exports = React.createClass({ defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, - // A username that will be used if no username is enetered. - // Specifying this param will also warn the user that enetering + // A username that will be used if no username is entered. + // Specifying this param will also warn the user that entering // a different username will cause a fresh account to be generated. guestUsername: React.PropTypes.string, From 6d403e792b343907dc8069514dddc0baecbf4f96 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:29:59 +0100 Subject: [PATCH 08/73] Add --stage 1 to babel cmdline So we can use trailing function commas. The trailing comma proposal is actually at stage 3 now and there's a babel 6 plugin for it, which we should use when we switch back to babel 6. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 13cabf32d9..cd81ad7c56 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ }, "scripts": { "reskindex": "reskindex -h header", - "build": "babel src -d lib --source-maps", - "start": "babel src -w -d lib --source-maps", + "build": "babel src -d lib --source-maps --stage 1", + "start": "babel src -w -d lib --source-maps --stage 1", "lint": "eslint src/", "lintall": "eslint src/ test/", "clean": "rimraf lib", From afa6acc20a75705c4f27fa9a17aa22329fd01d88 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:42:42 +0100 Subject: [PATCH 09/73] All the trailing commas --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index 4a42597b7a..c46592f244 100644 --- a/code_style.md +++ b/code_style.md @@ -97,7 +97,7 @@ General Style if (x) return true; // Not fine ``` -- Terminate all multi-line lists with commas (if using a transpiler): +- Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: ``` var mascots = [ "Patrick", From 1a3bc814e1cb81f2d1d32d73ad9ae11c14b94ccd Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:58:18 +0100 Subject: [PATCH 10/73] clarify event handlers --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index c46592f244..9078d8cdb2 100644 --- a/code_style.md +++ b/code_style.md @@ -151,7 +151,7 @@ React // Bad {doStuff();}}> // Equally bad // Better - // Best + // Best, if onFooClick would do anything other than directly calling doStuff ``` - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? From 0fdc2d817c6c8640048baa3b4b9d6bd71b448e0a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 15:59:34 +0100 Subject: [PATCH 11/73] mark as jsx --- code_style.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_style.md b/code_style.md index 9078d8cdb2..0ef16aedf5 100644 --- a/code_style.md +++ b/code_style.md @@ -147,7 +147,7 @@ React ----- - Use ES6 classes, although bear in mind a lot of code uses createClass. - Pull out functions in props to the class, generally as specific event handlers: - ``` + ```jsx // Bad {doStuff();}}> // Equally bad // Better From b78340ff51c40c6b7ec6de6ac9c9614897043c45 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 17:28:04 +0100 Subject: [PATCH 12/73] Use HS proxy API for requestToken on adding email So we report an error if the email is already taken. Also fix a bug where the spinner wouldn't disappear if adding an email failed (and don't include the raw errcode in the user-facing dialog) --- src/AddThreepid.js | 6 ++++-- src/components/structures/UserSettings.js | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 31805aad11..c32eb3aab1 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -38,11 +38,13 @@ class AddThreepid { */ addEmailAddress(emailAddress, bind) { this.bind = bind; - return MatrixClientPeg.get().requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + return MatrixClientPeg.get().request3pidAddEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; return res; }, function(err) { - if (err.httpStatus) { + if (err.errcode == 'M_THREEPID_IN_USE') { + err.message = "This email address is already in use"; + } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } throw err; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 7fcb81a60c..c54a10c7c0 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -214,9 +214,10 @@ module.exports = React.createClass({ onFinished: this.onEmailDialogFinished, }); }, (err) => { + this.setState({email_add_pending: false}); Modal.createDialog(ErrorDialog, { title: "Unable to add email address", - description: err.toString() + description: err.message }); }); ReactDOM.findDOMNode(this.refs.add_threepid_input).blur(); From 8e8e54a3bfec44b837c86ce4d91d73c8932ed533 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 17:53:06 +0100 Subject: [PATCH 13/73] Update function name as per js-sdk PR feedback --- src/AddThreepid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index c32eb3aab1..5593d46ff7 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -38,7 +38,7 @@ class AddThreepid { */ addEmailAddress(emailAddress, bind) { this.bind = bind; - return MatrixClientPeg.get().request3pidAddEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; return res; }, function(err) { From 30cfc6b605f2a09ad344c438d86dc99e2926857c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Jul 2016 18:06:50 +0100 Subject: [PATCH 14/73] Error if email already in use when resetting pw Use password reset specific requestToken --- src/PasswordReset.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PasswordReset.js b/src/PasswordReset.js index bbafa0ef33..a03a565459 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -48,11 +48,13 @@ class PasswordReset { */ resetPassword(emailAddress, newPassword) { this.password = newPassword; - return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; return res; }, function(err) { - if (err.httpStatus) { + if (err.errcode == 'M_THREEPID_NOT_FOUND') { + err.message = "This email address was not found"; + } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } throw err; From ffbe045fcc645bda4fcc0c0292c973774ef1ac1a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 10:10:51 +0100 Subject: [PATCH 15/73] Change to const-by-default --- code_style.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/code_style.md b/code_style.md index 0ef16aedf5..6ec2597aa2 100644 --- a/code_style.md +++ b/code_style.md @@ -132,9 +132,7 @@ General Style ECMAScript ---------- -- Use `let` for variables and `const` for constants. This sounds obvious, but it isn't: the ES6 `const` keyword - could be used for assign-once variables, however this guide advises against doing so on the grounds that it - confuses them with constants. +- Use `const` unless you need a re-assignable variable. This ensures things you don't want to be re-assigned can't be. - Be careful migrating files to newer syntax. - Don't mix `require` and `import` in the same file. Either stick to the old style or change them all. - Likewise, don't mix things like class properties and `MyClass.prototype.MY_CONSTANT = 42;` From 8d1d37c103bb4dd72eff856ad9cdac5824ca095c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:19:14 +0100 Subject: [PATCH 16/73] Try newline to fix code style formatting --- code_style.md | 1 + 1 file changed, 1 insertion(+) diff --git a/code_style.md b/code_style.md index 6ec2597aa2..70ae36faa7 100644 --- a/code_style.md +++ b/code_style.md @@ -37,6 +37,7 @@ General Style - Single line ternary operators are fine. - UPPER_CAMEL_CASE for constants - Single quotes for strings by default, for consistency with most JavaScript styles: + ``` "bad" // Bad 'good' // Good From a28a335df426dff470e833d5be6cdfbe5851fb76 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:21:21 +0100 Subject: [PATCH 17/73] More newlines for formatting --- code_style.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code_style.md b/code_style.md index 70ae36faa7..6466b512b4 100644 --- a/code_style.md +++ b/code_style.md @@ -44,6 +44,7 @@ General Style ``` - Use parentheses or `\`` instead of '\\' for line continuation where ever possible - Open braces on the same line (consistent with Node): + ``` if (x) { console.log("I am a fish"); // Good @@ -55,6 +56,7 @@ General Style } ``` - Spaces after `if`, `for`, `else` etc, no space around the condition: + ``` if (x) { console.log("I am a fish"); // Good @@ -71,6 +73,7 @@ General Style - Declare one variable per var statement (consistent with Node). Unless they are simple and closely related. If you put the next declaration on a new line, treat yourself to another `var`: + ``` var key = "foo", comparator = function(x, y) { @@ -88,6 +91,7 @@ General Style var y = 0; // Also fine ``` - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: + ``` if (x) return true; // Fine @@ -99,6 +103,7 @@ General Style return true; // Not fine ``` - Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: + ``` var mascots = [ "Patrick", @@ -120,6 +125,7 @@ General Style Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number. When something is intentionally missing or removed, set it to null. If returning a boolean, type coerce: + ``` function hasThings() { return !!length; // bad @@ -146,6 +152,7 @@ React ----- - Use ES6 classes, although bear in mind a lot of code uses createClass. - Pull out functions in props to the class, generally as specific event handlers: + ```jsx // Bad {doStuff();}}> // Equally bad From 513492e1bf83fc6cf551bc747294c41e0cc11442 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:27:17 +0100 Subject: [PATCH 18/73] Change line length as per PR discussion --- code_style.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code_style.md b/code_style.md index 6466b512b4..15404e1599 100644 --- a/code_style.md +++ b/code_style.md @@ -22,9 +22,8 @@ number throgh from the original code to the final application. General Style ------------- - 4 spaces to indent, for consistency with Matrix Python. -- Max line width: 79 chars (with flexibility to overflow by a "few chars" if - the overflowing content is not semantically significant and avoids an - explosion of vertical whitespace). +- 120 columns per line, but try to keep JavaScript code around the 80 column mark. + Inline JSX in particular can be nicer with more columns per line. - No trailing whitespace at end of lines. - Don't indent empty lines. - One newline at the end of the file. From e00f3d9334e6216685fff74a7e702d58dffec3ea Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Jul 2016 16:28:41 +0100 Subject: [PATCH 19/73] Mark code blocks as javascript --- code_style.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code_style.md b/code_style.md index 15404e1599..7b272e0656 100644 --- a/code_style.md +++ b/code_style.md @@ -37,14 +37,14 @@ General Style - UPPER_CAMEL_CASE for constants - Single quotes for strings by default, for consistency with most JavaScript styles: - ``` + ```javascript "bad" // Bad 'good' // Good ``` - Use parentheses or `\`` instead of '\\' for line continuation where ever possible - Open braces on the same line (consistent with Node): - ``` + ```javascript if (x) { console.log("I am a fish"); // Good } @@ -56,7 +56,7 @@ General Style ``` - Spaces after `if`, `for`, `else` etc, no space around the condition: - ``` + ```javascript if (x) { console.log("I am a fish"); // Good } @@ -73,7 +73,7 @@ General Style are simple and closely related. If you put the next declaration on a new line, treat yourself to another `var`: - ``` + ```javascript var key = "foo", comparator = function(x, y) { return x - y; @@ -91,7 +91,7 @@ General Style ``` - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: - ``` + ```javascript if (x) return true; // Fine if (x) { @@ -103,7 +103,7 @@ General Style ``` - Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing: - ``` + ```javascript var mascots = [ "Patrick", "Shirley", @@ -125,7 +125,7 @@ General Style When something is intentionally missing or removed, set it to null. If returning a boolean, type coerce: - ``` + ```javascript function hasThings() { return !!length; // bad return new Boolean(length); // REALLY bad From a8677b52adb6e30dfc3eb35d48e9eb758425b32e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 11 Jul 2016 18:26:16 +0100 Subject: [PATCH 20/73] major update to dev guidelines --- README.md | 180 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 113 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index ae1cd17c9a..dfc1a6e6ec 100644 --- a/README.md +++ b/README.md @@ -3,65 +3,85 @@ matrix-react-sdk This is a react-based SDK for inserting a Matrix chat/voip client into a web page. -This package provides the logic and 'controller' parts for the UI components. This -forms one part of a complete matrix client, but it not useable in isolation. It -must be used from a 'skin'. A skin provides: - * The HTML for the UI components (in the form of React `render` methods) - * The CSS for this HTML - * The containing application +This package provides the React components needed to build a Matrix web client +using React. It is not useable in isolation, and instead must must be used from +a 'skin'. A skin provides: + * Customised implementations of presentation components. + * Custom CSS + * The containing application * Zero or more 'modules' containing non-UI functionality -Skins are modules are exported from such a package in the `lib` directory. -`lib/skins` contains one directory per-skin, named after the skin, and the -`modules` directory contains modules as their javascript files. +**WARNING: As of July 2016, the skinning abstraction is broken due to rapid +development of `matrix-react-sdk` to meet the needs of Vector, the first app +to be built on top of the SDK** (https://github.com/vector-im/vector-web). +Right now `matrix-react-sdk` depends on some functionality from `vector-web` +(e.g. CSS), and `matrix-react-sdk` contains some Vector specific behaviour +(grep for 'vector'). This layering will be fixed asap once Vector development +has stabilised, but for now we do not advise trying to create new skins for +matrix-react-sdk until the layers are clearly separated again. -A basic skin is provided in the matrix-react-skin package. This also contains -a minimal application that instantiates the basic skin making a working matrix -client. +In the interim, `vector-im/vector-web` and `matrix-org/matrix-react-sdk` should +be considered as a single project (for instance, matrix-react-sdk bugs +are currently filed against vector-im/vector-web rather than this project). -You can use matrix-react-sdk directly, but to do this you would have to provide -'views' for each UI component. To get started quickly, use matrix-react-skin. +Developer Guide +=============== -How to customise the SDK -======================== +Platform Targets: + * Chrome, Firefox and Safari. + * Edge should also work, but we're not testing it proactively. + * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. + * Mobile Web is not currently a target platform - instead please use the native + iOS (https://github.com/matrix-org/matrix-ios-kit) and Android + (https://github.com/matrix-org/matrix-android-sdk) SDKs. -The SDK formerly used the 'atomic' design pattern as seen at http://patternlab.io to -encourage a very modular and reusable architecture, making it easy to -customise and use UI widgets independently of the rest of the SDK and your app. +All code lands on the `develop` branch - `master` is only used for stable releases. +**Please file PRs against `develop`!!** -So unfortunately at the moment this document does not describe how to customize your UI! +Please follow the standard Matrix contributor's guide: +https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst -###This is the old description for the atomic design pattern: +Please follow the Matrix JS/React code style as per: +https://github.com/matrix-org/matrix-react-sdk/tree/master/code_style.rst -In practice this means: +Whilst the layering separation between matrix-react-sdk and Vector is broken +(as of July 2016), code should be committed as follows: + * All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components + * Vector-specific components: https://github.com/vector-im/vector-web/tree/master/src/components + * In practice, `matrix-react-sdk` is still evolving so fast that the maintenance + burden of customising and overriding these components for Vector can seriously + impede development. So right now, there should be very few (if any) customisations for Vector. + * CSS for Matrix SDK components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk + * CSS for Vector-specific overrides and components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/vector-web - * The UI of the app is strictly split up into a hierarchy of components. - - * Each component has its own: - * View object defined as a React javascript class containing embedded - HTML expressed in React's JSX notation. - * CSS file, which defines the styling specific to that component. - - * Components are loosely grouped into the 5 levels outlined by atomic design: - * atoms: fundamental building blocks (e.g. a timestamp tag) - * molecules: "group of atoms which functions together as a unit" - (e.g. a message in a chat timeline) - * organisms: "groups of molecules (and atoms) which form a distinct section - of a UI" (e.g. a view of a chat room) - * templates: "a reusable configuration of organisms" - used to combine and - style organisms into a well-defined global look and feel - * pages: specific instances of templates. +React components in matrix-react-sdk are come in two different flavours: +'structures' and 'views'. Structures are stateful components which handle the +more complicated business logic of the app, delegating their actual presentation +rendering to stateless 'view' components. For instance, the RoomView component +that orchestrates the act of visualising the contents of a given Matrix chat room +tracks lots of state for its child components which it passes into them for visual +rendering via props. - Good separation between the components is maintained by adopting various best - practices that anyone working with the SDK needs to be be aware of and uphold: +Good separation between the components is maintained by adopting various best +practices that anyone working with the SDK needs to be be aware of and uphold: - * Views are named with upper camel case (e.g. molecules/MessageTile.js) + * Components are named with upper camel case (e.g. views/rooms/EventTile.js) - * The view's CSS file MUST have the same name (e.g. molecules/MessageTile.css) + * They are organised in a typically two-level hierarchy - first whether the + component is a view or a structure, and then a broad functional grouping + (e.g. 'rooms' here) + + * After creating a new component you must run `npm run reskindex` to regenerate + the `component-index.js` for the SDK (used in future for skinning) + + * The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css). + CSS for matrix-react-sdk currently resides in + https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk. * Per-view CSS is optional - it could choose to inherit all its styling from - the context of the rest of the app, although this is unusual for any but - the simplest atoms and molecules. + the context of the rest of the app, although this is unusual for any but + structural components (lacking presentation logic) and the simplest view + components. * The view MUST *only* refer to the CSS rules defined in its own CSS file. 'Stealing' styling information from other components (including parents) @@ -82,9 +102,10 @@ In practice this means: * We deliberately use vanilla CSS 3.0 to avoid adding any more magic dependencies into the mix than we already have. App developers are welcome - to use whatever floats their boat however. + to use whatever floats their boat however. In future we'll start using + css-next to pull in features like CSS variable support. - * The CSS for a component can however override the rules for child components. + * The CSS for a component can override the rules for child components. For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides *must* be scoped to the View's CSS class - i.e. don't just define @@ -98,30 +119,36 @@ In practice this means: generally not cool and stop the component from being reused easily in different places. - * We don't use the atomify library itself, as React already provides most - of the modularity requirements it brings to the table. +Originally `matrix-react-sdk` followed the Atomic design pattern as per +http://patternlab.io to try to encourage a modular architecture. However, we +found that the grouping of components into atoms/molecules/organisms +made them harder to find relative to a functional split, and didn't emphasise +the distinction between 'structural' and 'view' components, so we backed away +from it. -With all this in mind, here's how you go about skinning the react SDK UI -components to embed a Matrix client into your app: +Github Issues +============= - * Create a new NPM project. Be sure to directly depend on react, (otherwise - you can end up with two copies of react). - * Create an index.js file that sets up react. Add require statements for - React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the - SDK and call Render. This can be a skin provided by a separate package or - a skin in the same package. - * Add a way to build your project: we suggest copying the scripts block - from matrix-react-skin (which uses babel and webpack). You could use - different tools but remember that at least the skins and modules of - your project should end up in plain (ie. non ES6, non JSX) javascript in - the lib directory at the end of the build process, as well as any - packaging that you might do. - * Create an index.html file pulling in your compiled javascript and the - CSS bundle from the skin you use. For now, you'll also need to manually - import CSS from any skins that your skin inherts from. +All issues should be filed under https://github.com/vector-im/vector-web/issues +for now. + +OUTDATED: To Create Your Own Skin +================================= + +**This is ALL LIES currently, as skinning is currently broken - see the WARNING +section at the top of this readme.** + +Skins are modules are exported from such a package in the `lib` directory. +`lib/skins` contains one directory per-skin, named after the skin, and the +`modules` directory contains modules as their javascript files. + +A basic skin is provided in the matrix-react-skin package. This also contains +a minimal application that instantiates the basic skin making a working matrix +client. + +You can use matrix-react-sdk directly, but to do this you would have to provide +'views' for each UI component. To get started quickly, use matrix-react-skin. -To Create Your Own Skin -======================= To actually change the look of a skin, you can create a base skin (which does not use views from any other skin) or you can make a derived skin. Note that derived skins are currently experimental: for example, the CSS @@ -145,3 +172,22 @@ Now you have the basis of a skin, you need to generate a skindex.json file. The you add an npm script to run this, as in matrix-react-skin. For more specific detail on any of these steps, look at matrix-react-skin. + +Alternative instructions: + + * Create a new NPM project. Be sure to directly depend on react, (otherwise + you can end up with two copies of react). + * Create an index.js file that sets up react. Add require statements for + React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the + SDK and call Render. This can be a skin provided by a separate package or + a skin in the same package. + * Add a way to build your project: we suggest copying the scripts block + from matrix-react-skin (which uses babel and webpack). You could use + different tools but remember that at least the skins and modules of + your project should end up in plain (ie. non ES6, non JSX) javascript in + the lib directory at the end of the build process, as well as any + packaging that you might do. + * Create an index.html file pulling in your compiled javascript and the + CSS bundle from the skin you use. For now, you'll also need to manually + import CSS from any skins that your skin inherts from. + From ac58520b3bce6dc32f16b53204f61e6a811b1fef Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Jul 2016 18:30:37 +0100 Subject: [PATCH 21/73] Update tab completion list when we have a room Fixes https://github.com/vector-im/vector-web/issues/1737 --- src/components/structures/RoomView.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index dc4b21a300..cfd359ea01 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -204,6 +204,9 @@ module.exports = React.createClass({ user_is_in_room = this.state.room.hasMembershipState( MatrixClientPeg.get().credentials.userId, 'join' ); + + // update the tab complete list now we have a room + this._updateTabCompleteList(); } if (!user_is_in_room && this.state.roomId) { From 6b03f72474be475ff524531a176c15857c09ecb4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Jul 2016 11:56:01 +0100 Subject: [PATCH 22/73] Fix filtering user list by ID Fixes https://github.com/vector-im/vector-web/issues/1739 --- src/components/views/rooms/MemberList.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 20f60c80a8..e87d31cdb3 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -442,9 +442,16 @@ module.exports = React.createClass({ var memberList = self.state.members.filter(function(userId) { var m = self.memberDict[userId]; - if (query && m.name.toLowerCase().indexOf(query) === -1) { - return false; + + if (query) { + const matchesName = m.name.toLowerCase().indexOf(query) !== -1; + const matchesId = m.userId.toLowerCase().indexOf(query) !== -1; + + if (!matchesName && !matchesId) { + return false; + } } + return m.membership == membership; }).map(function(userId) { var m = self.memberDict[userId]; From 2d6d7345719988b3e8988c496b47dcddb4ed7d6d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 10:05:40 +0100 Subject: [PATCH 23/73] Listen for the new lastPreseceTs event This will catch all presence events as previously we were only listening for the actual presence string changing --- src/components/views/rooms/MemberList.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index e87d31cdb3..1a3f710063 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -64,7 +64,10 @@ module.exports = React.createClass({ cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomState.events", this.onRoomStateEvent); cli.on("Room", this.onRoom); // invites - cli.on("User.presence", this.onUserPresence); + // We listen for changes to the lastPresenceTs which is essentially + // listening for all presence events (we display most of not all of + // the information contained in presence events). + cli.on("User.lastPresenceTs", this.onUserLastPresenceTs); // cli.on("Room.timeline", this.onRoomTimeline); }, @@ -75,7 +78,7 @@ module.exports = React.createClass({ cli.removeListener("RoomMember.name", this.onRoomMemberName); cli.removeListener("RoomState.events", this.onRoomStateEvent); cli.removeListener("Room", this.onRoom); - cli.removeListener("User.presence", this.onUserPresence); + cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs); // cli.removeListener("Room.timeline", this.onRoomTimeline); } }, @@ -121,7 +124,7 @@ module.exports = React.createClass({ }, */ - onUserPresence(event, user) { + onUserLastPresenceTs(event, user) { // Attach a SINGLE listener for global presence changes then locate the // member tile and re-render it. This is more efficient than every tile // evar attaching their own listener. From 20210e71040906623648dc8e314708eacfe032c4 Mon Sep 17 00:00:00 2001 From: Kegsay Date: Thu, 14 Jul 2016 10:38:24 +0100 Subject: [PATCH 24/73] Log scrollTop and scrollHeight to try to debug flakey test Sometimes it fails because awaitScroll() on :277 isn't resolving because onScroll isn't firing. We need to know if this is because we aren't changing scrollTop --- test/components/structures/TimelinePanel-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index 045ccd70b7..d1c13ff8b8 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -271,6 +271,8 @@ describe('TimelinePanel', function() { // we should now be able to scroll down, and paginate in the other // direction. + console.log("scrollingDiv.scrollTop is " + scrollingDiv.scrollTop); + console.log("Going to set it to " + scrollingDiv.scrollHeight); scrollingDiv.scrollTop = scrollingDiv.scrollHeight; return awaitScroll(); }).then(() => { From a5272542ef43d218ab7b5b1b1b7274dc1b79f43b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 10:39:15 +0100 Subject: [PATCH 25/73] Lengthen timelinepanel test timeout again It's now hitting this timeout on the jenkins box instead of the browser disconnect timeout --- test/components/structures/TimelinePanel-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index d1c13ff8b8..cd9d86cd64 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -210,7 +210,7 @@ describe('TimelinePanel', function() { var N_EVENTS = 600; // sadly, loading all those events takes a while - this.timeout(N_EVENTS * 20); + this.timeout(N_EVENTS * 30); // client.getRoom is called a /lot/ in this test, so replace // sinon's spy with a fast noop. From 0dde891d4cc941aa16f8a8f61954e2e0f4e5837b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 11:25:45 +0100 Subject: [PATCH 26/73] Order tab complete by most recently spoke Fixes https://github.com/vector-im/vector-web/issues/1741 --- src/TabCompleteEntries.js | 20 +++++++++++++++++++- src/components/structures/RoomView.js | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index a23050063f..488aaa57a6 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -113,8 +113,26 @@ class MemberEntry extends Entry { } } -MemberEntry.fromMemberList = function(members) { +MemberEntry.fromMemberList = function(room, members) { + // build up a dict of when, in the history we have cached, + // each member last spoke + const lastSpoke = {}; + const timelineEvents = room.getLiveTimeline().getEvents(); + for (var i = timelineEvents.length - 1; i >= 0; --i) { + const ev = timelineEvents[i]; + lastSpoke[ev.sender.userId] = ev.getTs(); + } + return members.sort(function(a, b) { + const lastSpokeA = lastSpoke[a.userId] || 0; + const lastSpokeB = lastSpoke[b.userId] || 0; + + if (lastSpokeA != lastSpokeB) { + // B - A here because the highest value + // is most recent + return lastSpokeB - lastSpokeA; + } + var userA = a.user; var userB = b.user; if (userA && !userB) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cfd359ea01..64a29f9ffc 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -533,7 +533,7 @@ module.exports = React.createClass({ UserProvider.getInstance().setUserList(members); this.tabComplete.setCompletionList( - MemberEntry.fromMemberList(members).concat( + MemberEntry.fromMemberList(this.state.room, members).concat( CommandEntry.fromCommands(SlashCommands.getCommandList()) ) ); From 2ce521fe387428f105013da133c3bf57d291e03c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 11:40:17 +0100 Subject: [PATCH 27/73] Fix null error in TabComplete .sende ris sometimes null: use getSender() which isn't and returns the userId which is what we actually want --- src/TabCompleteEntries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 488aaa57a6..3cfe07c7c0 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -120,7 +120,7 @@ MemberEntry.fromMemberList = function(room, members) { const timelineEvents = room.getLiveTimeline().getEvents(); for (var i = timelineEvents.length - 1; i >= 0; --i) { const ev = timelineEvents[i]; - lastSpoke[ev.sender.userId] = ev.getTs(); + lastSpoke[ev.getSender()] = ev.getTs(); } return members.sort(function(a, b) { From f1d72296b76e87d28753281ef6d1a8d481ef8a4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 14:06:31 +0100 Subject: [PATCH 28/73] Fix last-spoke order Turns out this timeline is the other way around, so loop through the other way --- src/TabCompleteEntries.js | 3 +-- src/components/structures/RoomView.js | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 3cfe07c7c0..419b3d7942 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -118,8 +118,7 @@ MemberEntry.fromMemberList = function(room, members) { // each member last spoke const lastSpoke = {}; const timelineEvents = room.getLiveTimeline().getEvents(); - for (var i = timelineEvents.length - 1; i >= 0; --i) { - const ev = timelineEvents[i]; + for (const ev of room.getLiveTimeline().getEvents()) { lastSpoke[ev.getSender()] = ev.getTs(); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 64a29f9ffc..71edbf162d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -360,6 +360,10 @@ module.exports = React.createClass({ }); } } + + // update ther tab complete list as it depends on who most recently spoke, + // and that has probably just changed + this._updateTabCompleteList(); }, // called when state.room is first initialised (either at initial load, From cad057ef348d7029c075644a9dd2a3d611e42788 Mon Sep 17 00:00:00 2001 From: Stefan Pearson Date: Thu, 14 Jul 2016 17:12:17 +0100 Subject: [PATCH 29/73] amends react template and removes opening image in lightbox --- .../views/rooms/LinkPreviewWidget.js | 46 +++++-------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index ba438c1d12..3398b021fb 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -73,36 +73,13 @@ module.exports = React.createClass({ this.unmounted = true; }, - onImageClick: function(ev) { - var p = this.state.preview; - if (ev.button != 0 || ev.metaKey) return; - ev.preventDefault(); - var ImageView = sdk.getComponent("elements.ImageView"); - - var src = p["og:image"]; - if (src && src.startsWith("mxc://")) { - src = MatrixClientPeg.get().mxcUrlToHttp(src); - } - - var params = { - src: src, - width: p["og:image:width"], - height: p["og:image:height"], - name: p["og:title"] || p["og:description"] || this.props.link, - fileSize: p["matrix:image:size"], - link: this.props.link, - }; - - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); - }, - render: function() { var p = this.state.preview; if (!p) return
; // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? var image = p["og:image"]; - var imageMaxWidth = 100, imageMaxHeight = 100; + var imageMaxWidth = 600, imageMaxHeight = 400; if (image && image.startsWith("mxc://")) { image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); } @@ -114,23 +91,24 @@ module.exports = React.createClass({ var img; if (image) { - img =
- -
+ img = ( +
+ + + +
+ ); } return (
- { img }
-
{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }
-
- { p["og:description"] } -
+
{ p["og:site_name"] ? ("from " + p["og:site_name"]) : null }
- + { img } +
{ p["og:description"] } Read more
+
); } From a49a545161a696009e616c26f96ede57ae849fd8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 17:41:07 +0100 Subject: [PATCH 30/73] CSS classes to colour offline users differently So we can use the same 66% opacity as idle tiles for offline-with-last-active-time to reduce the visual jarring --- src/components/views/rooms/EntityTile.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 91874ed45a..eb351143e9 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -29,6 +29,23 @@ var PRESENCE_CLASS = { "unavailable": "mx_EntityTile_unavailable" }; + +function presence_class_for_member(presence_state, last_active_ago) { + // offline is split into two categories depending on whether we have + // a last_active_ago for them. + if (presence_state == 'offline') { + if (last_active_ago) { + return PRESENCE_CLASS['offline'] + '_beenactive'; + } else { + return PRESENCE_CLASS['offline'] + '_neveractive'; + } + } else if (presence_state) { + return PRESENCE_CLASS[presence_state]; + } else { + return PRESENCE_CLASS['offline']; + } +} + module.exports = React.createClass({ displayName: 'EntityTile', @@ -79,7 +96,10 @@ module.exports = React.createClass({ }, render: function() { - var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline"; + const presenceClass = presence_class_for_member( + this.props.presenceState, this.props.presenceLastActiveAgo + ); + var mainClassName = "mx_EntityTile "; mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : ""); var nameEl; From 7c1b4f4fc965deebde58031bc932fc9e4899b5a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:13:15 +0100 Subject: [PATCH 31/73] Obey my own code style --- src/components/views/rooms/EntityTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index eb351143e9..b406f4f06f 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -30,7 +30,7 @@ var PRESENCE_CLASS = { }; -function presence_class_for_member(presence_state, last_active_ago) { +function presenceClassForMember(presence_state, last_active_ago) { // offline is split into two categories depending on whether we have // a last_active_ago for them. if (presence_state == 'offline') { @@ -96,7 +96,7 @@ module.exports = React.createClass({ }, render: function() { - const presenceClass = presence_class_for_member( + const presenceClass = presenceClassForMember( this.props.presenceState, this.props.presenceLastActiveAgo ); From 2fd690ea2b14ef1881e203d8ad3db1a3f5d9f9fa Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:13:54 +0100 Subject: [PATCH 32/73] Oops, removed the pure offline class --- src/components/views/rooms/EntityTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index b406f4f06f..ef460fe74a 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -42,7 +42,7 @@ function presenceClassForMember(presence_state, last_active_ago) { } else if (presence_state) { return PRESENCE_CLASS[presence_state]; } else { - return PRESENCE_CLASS['offline']; + return PRESENCE_CLASS['offline'] + '_neveractive'; } } From 9fd0ea1e328fb066d567a468404920c9fc989286 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:18:44 +0100 Subject: [PATCH 33/73] More variable case --- src/components/views/rooms/EntityTile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index ef460fe74a..8a99b4c565 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -30,17 +30,17 @@ var PRESENCE_CLASS = { }; -function presenceClassForMember(presence_state, last_active_ago) { +function presenceClassForMember(presenceState, lastActiveAgo) { // offline is split into two categories depending on whether we have // a last_active_ago for them. - if (presence_state == 'offline') { - if (last_active_ago) { + if (presenceState == 'offline') { + if (lastActiveAgo) { return PRESENCE_CLASS['offline'] + '_beenactive'; } else { return PRESENCE_CLASS['offline'] + '_neveractive'; } - } else if (presence_state) { - return PRESENCE_CLASS[presence_state]; + } else if (presenceState) { + return PRESENCE_CLASS[presenceState]; } else { return PRESENCE_CLASS['offline'] + '_neveractive'; } From 743cb4b7772ede94153a5c9cf0b27d88b02703c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:37:57 +0100 Subject: [PATCH 34/73] Remove the member list loading hack Now the memberlist os a truncated list it at-best makes no difference and may be marginally slowing us down. --- src/components/views/rooms/MemberList.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 1a3f710063..dbbc9c6a7e 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -54,7 +54,7 @@ module.exports = React.createClass({ this.memberDict = this.getMemberDict(); - state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS); + state.members = this.roomMembers(); return state; }, @@ -83,19 +83,6 @@ module.exports = React.createClass({ } }, - componentDidMount: function() { - var self = this; - - // Lazy-load in more than the first N members - setTimeout(function() { - if (!self.isMounted()) return; - // lazy load to prevent it blocking the first render - self.setState({ - members: self.roomMembers() - }); - }, 50); - }, - /* onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) { // ignore anything but real-time updates at the end of the room: From 3abdb83d162485a76b84b578ae203e74f0b9433a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jul 2016 18:40:43 +0100 Subject: [PATCH 35/73] Remove now-unused limit param to roomMembers() --- src/components/views/rooms/MemberList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index dbbc9c6a7e..01a952f1d7 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -315,7 +315,7 @@ module.exports = React.createClass({ return all_members; }, - roomMembers: function(limit) { + roomMembers: function() { var all_members = this.memberDict || {}; var all_user_ids = Object.keys(all_members); var ConferenceHandler = CallHandler.getConferenceHandler(); @@ -324,7 +324,7 @@ module.exports = React.createClass({ var to_display = []; var count = 0; - for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) { + for (var i = 0; i < all_user_ids.length; ++i) { var user_id = all_user_ids[i]; var m = all_members[user_id]; From febd2110c53afb2f663064d9a1531c474cd04e33 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Fri, 15 Jul 2016 11:23:58 +0100 Subject: [PATCH 36/73] Updated all dialogs to define their primary button, I've selected the one that had been specified for focus, but the primary button isn't dependent on focus, it's a UX feature --- src/components/views/dialogs/ErrorDialog.js | 2 +- src/components/views/dialogs/LogoutPrompt.js | 2 +- src/components/views/dialogs/NeedToRegisterDialog.js | 2 +- src/components/views/dialogs/QuestionDialog.js | 2 +- src/components/views/dialogs/SetDisplayNameDialog.js | 2 +- src/components/views/dialogs/TextInputDialog.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index b3278dfcfe..cc400e30a6 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -59,7 +59,7 @@ module.exports = React.createClass({ {this.props.description}
-
diff --git a/src/components/views/dialogs/LogoutPrompt.js b/src/components/views/dialogs/LogoutPrompt.js index 67fedfe840..7c4ba18e82 100644 --- a/src/components/views/dialogs/LogoutPrompt.js +++ b/src/components/views/dialogs/LogoutPrompt.js @@ -46,7 +46,7 @@ module.exports = React.createClass({ Sign out?
- +
diff --git a/src/components/views/dialogs/NeedToRegisterDialog.js b/src/components/views/dialogs/NeedToRegisterDialog.js index d9133ee138..0080e0c643 100644 --- a/src/components/views/dialogs/NeedToRegisterDialog.js +++ b/src/components/views/dialogs/NeedToRegisterDialog.js @@ -63,7 +63,7 @@ module.exports = React.createClass({ {this.props.description}
-
- diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js index 81ceb21696..c1041cc218 100644 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ b/src/components/views/dialogs/SetDisplayNameDialog.js @@ -76,7 +76,7 @@ module.exports = React.createClass({ />
- +
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index fed7ff079a..6245b5786f 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -86,7 +86,7 @@ module.exports = React.createClass({ - From 41bff38713e4a6f536296a3a5af7d9394fff22b9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jul 2016 15:04:19 +0100 Subject: [PATCH 37/73] fix classes used for body spans, and only apply markdown-body to markdown(!) --- src/HtmlUtils.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 64d746f0a4..8b3a368f4d 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -223,8 +223,10 @@ module.exports = { let match = EMOJI_REGEX.exec(contentBodyTrimmed); let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length; - let className = classNames('markdown-body', { - 'emoji-body': emojiBody, + const className = classNames({ + 'mx_EventTile_body': true, + 'mx_EventTile_bigEmoji': emojiBody, + 'markdown-body': isHtml, }); return ; }, From d5bed78a54926b591422c60fdd3ff3b849c95689 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 16:10:27 +0100 Subject: [PATCH 38/73] Rejig tab complete to make it faster Now do a lot less when people speak. Also move more of the tab completion logic into TabComplete.js and out of RoomView. --- src/TabComplete.js | 77 +++++++++++++++++--- src/TabCompleteEntries.js | 40 +--------- src/components/structures/RoomStatusBar.js | 16 ++-- src/components/structures/RoomView.js | 66 ++++++++--------- src/components/views/rooms/TabCompleteBar.js | 8 +- 5 files changed, 113 insertions(+), 94 deletions(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 7da8bde76b..5b7be7c286 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -13,7 +13,10 @@ 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 Entry = require("./TabCompleteEntries").Entry; + +import { Entry, MemberEntry, CommandEntry } from './TabCompleteEntries'; +import SlashCommands from './SlashCommands'; +import MatrixClientPeg from './MatrixClientPeg'; const DELAY_TIME_MS = 1000; const KEY_TAB = 9; @@ -45,23 +48,34 @@ class TabComplete { this.isFirstWord = false; // true if you tab-complete on the first word this.enterTabCompleteTimerId = null; this.inPassiveMode = false; + this.memberTabOrder = {}; + this.memberOrderSeq = 0; } /** - * @param {Entry[]} completeList + * Call this when a a UI element representing a tab complete entry has been clicked + * @param {entry} The entry that was clicked */ - setCompletionList(completeList) { - this.list = completeList; + onEntryClick(entry) { if (this.opts.onClickCompletes) { - // assign onClick listeners for each entry to complete the text - this.list.forEach((l) => { - l.onClick = () => { - this.completeTo(l); - } - }); + this.completeTo(entry); } } + loadEntries(room) { + this._makeEntries(room); + this._initSorting(room); + this._sortEntries(); + } + + onMemberSpoke(member) { + if (this.memberTabOrder[member.userId] === undefined) { + this.list.push(new MemberEntry(member)); + } + this.memberTabOrder[member.userId] = this.memberOrderSeq++; + this._sortEntries(); + } + /** * @param {DOMElement} */ @@ -307,6 +321,49 @@ class TabComplete { this.opts.onStateChange(this.completing); } } + + _sortEntries() { + // largest comes first + const KIND_ORDER = { + command: 1, + member: 2, + }; + + this.list.sort((a, b) => { + const kindOrderDifference = KIND_ORDER[b.kind] - KIND_ORDER[a.kind]; + if (kindOrderDifference != 0) { + return kindOrderDifference; + } + + if (a.kind == 'member') { + return this.memberTabOrder[b.member.userId] - this.memberTabOrder[a.member.userId]; + } + + // anything else we have no ordering for + return 0; + }); + } + + _makeEntries(room) { + const myUserId = MatrixClientPeg.get().credentials.userId; + + const members = room.getJoinedMembers().filter(function(member) { + if (member.userId !== myUserId) return true; + }); + + this.list = MemberEntry.fromMemberList(members).concat( + CommandEntry.fromCommands(SlashCommands.getCommandList()) + ); + } + + _initSorting(room) { + this.memberTabOrder = {}; + this.memberOrderSeq = 0; + + for (const ev of room.getLiveTimeline().getEvents()) { + this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++; + } + } }; module.exports = TabComplete; diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 419b3d7942..4a28103210 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -69,6 +69,7 @@ class Entry { class CommandEntry extends Entry { constructor(cmd, cmdWithArgs) { super(cmdWithArgs); + this.kind = 'command'; this.cmd = cmd; } @@ -95,6 +96,7 @@ class MemberEntry extends Entry { constructor(member) { super(member.name || member.userId); this.member = member; + this.kind = 'member'; } getImageJsx() { @@ -113,42 +115,8 @@ class MemberEntry extends Entry { } } -MemberEntry.fromMemberList = function(room, members) { - // build up a dict of when, in the history we have cached, - // each member last spoke - const lastSpoke = {}; - const timelineEvents = room.getLiveTimeline().getEvents(); - for (const ev of room.getLiveTimeline().getEvents()) { - lastSpoke[ev.getSender()] = ev.getTs(); - } - - return members.sort(function(a, b) { - const lastSpokeA = lastSpoke[a.userId] || 0; - const lastSpokeB = lastSpoke[b.userId] || 0; - - if (lastSpokeA != lastSpokeB) { - // B - A here because the highest value - // is most recent - return lastSpokeB - lastSpokeA; - } - - var userA = a.user; - var userB = b.user; - if (userA && !userB) { - return -1; // a comes first - } - else if (!userA && userB) { - return 1; // b comes first - } - else if (!userA && !userB) { - return 0; // don't care - } - else { // both User objects exist - var lastActiveAgoA = userA.lastActiveAgo || Number.MAX_SAFE_INTEGER; - var lastActiveAgoB = userB.lastActiveAgo || Number.MAX_SAFE_INTEGER; - return lastActiveAgoA - lastActiveAgoB; - } - }).map(function(m) { +MemberEntry.fromMemberList = function(members) { + return members.map(function(m) { return new MemberEntry(m); }); } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 92f50dcb02..4309b1e849 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -26,9 +26,9 @@ module.exports = React.createClass({ propTypes: { // the room this statusbar is representing. room: React.PropTypes.object.isRequired, - - // a list of TabCompleteEntries.Entry objects - tabCompleteEntries: React.PropTypes.array, + + // a TabComplete object + tabComplete: React.PropTypes.object, // the number of messages which have arrived since we've been scrolled up numUnreadMessages: React.PropTypes.number, @@ -208,11 +208,11 @@ module.exports = React.createClass({ ); } - if (this.props.tabCompleteEntries) { + if (this.props.tabComplete.isTabCompleting()) { return (
- + ); - }, + }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 71edbf162d..f7f7ceb12c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -31,10 +31,7 @@ var Modal = require("../../Modal"); var sdk = require('../../index'); var CallHandler = require('../../CallHandler'); var TabComplete = require("../../TabComplete"); -var MemberEntry = require("../../TabCompleteEntries").MemberEntry; -var CommandEntry = require("../../TabCompleteEntries").CommandEntry; var Resend = require("../../Resend"); -var SlashCommands = require("../../SlashCommands"); var dis = require("../../dispatcher"); var Tinter = require("../../Tinter"); var rate_limited_func = require('../../ratelimitedfunc'); @@ -136,12 +133,6 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room", this.onRoom); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); - this.tabComplete = new TabComplete({ allowLooping: false, autoEnterTabComplete: true, @@ -151,6 +142,12 @@ module.exports = React.createClass({ } }); + this.dispatcherRef = dis.register(this.onAction); + MatrixClientPeg.get().on("Room", this.onRoom); + MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); + MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + if (this.props.roomAddress[0] == '#') { // we always look up the alias from the directory server: // we want the room that the given alias is pointing to @@ -205,8 +202,13 @@ module.exports = React.createClass({ MatrixClientPeg.get().credentials.userId, 'join' ); - // update the tab complete list now we have a room - this._updateTabCompleteList(); + this.tabComplete.loadEntries(this.state.room); + + const myUserId = MatrixClientPeg.get().credentials.userId; + const members = this.state.room.getJoinedMembers().filter(function(member) { + if (member.userId !== myUserId) return true; + }); + UserProvider.getInstance().setUserList(members); } if (!user_is_in_room && this.state.roomId) { @@ -363,7 +365,15 @@ module.exports = React.createClass({ // update ther tab complete list as it depends on who most recently spoke, // and that has probably just changed - this._updateTabCompleteList(); + if (ev.sender) { + this.tabComplete.onMemberSpoke(ev.sender); + + const myUserId = MatrixClientPeg.get().credentials.userId; + const members = this.state.room.getJoinedMembers().filter(function(member) { + if (member.userId !== myUserId) return true; + }); + UserProvider.getInstance().setUserList(members); + } }, // called when state.room is first initialised (either at initial load, @@ -441,7 +451,13 @@ module.exports = React.createClass({ } // a member state changed in this room, refresh the tab complete list - this._updateTabCompleteList(); + this.tabComplete.loadEntries(this.state.room); + + const myUserId = MatrixClientPeg.get().credentials.userId; + const members = this.state.room.getJoinedMembers().filter(function(member) { + if (member.userId !== myUserId) return true; + }); + UserProvider.getInstance().setUserList(members); // if we are now a member of the room, where we were not before, that // means we have finished joining a room we were previously peeking @@ -506,8 +522,6 @@ module.exports = React.createClass({ window.addEventListener('resize', this.onResize); this.onResize(); - this._updateTabCompleteList(); - // XXX: EVIL HACK to autofocus inviting on empty rooms. // We use the setTimeout to avoid racing with focus_composer. if (this.state.room && @@ -525,24 +539,6 @@ module.exports = React.createClass({ } }, - _updateTabCompleteList: function() { - var cli = MatrixClientPeg.get(); - - if (!this.state.room) { - return; - } - var members = this.state.room.getJoinedMembers().filter(function(member) { - if (member.userId !== cli.credentials.userId) return true; - }); - - UserProvider.getInstance().setUserList(members); - this.tabComplete.setCompletionList( - MemberEntry.fromMemberList(this.state.room, members).concat( - CommandEntry.fromCommands(SlashCommands.getCommandList()) - ) - ); - }, - componentDidUpdate: function() { if (this.refs.roomView) { var roomView = ReactDOM.findDOMNode(this.refs.roomView); @@ -1380,12 +1376,10 @@ module.exports = React.createClass({ statusBar = } else if (!this.state.searchResults) { var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); - var tabEntries = this.tabComplete.isTabCompleting() ? - this.tabComplete.peek(6) : null; statusBar = - {this.props.entries.map(function(entry, i) { + {this.props.tabComplete.peek(6).map((entry, i) => { return (
+ className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") } + onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} > {entry.getImageJsx()} {entry.getText()} From 7d712d06a1086ed0588f04b7c6e281becf9e683d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 16:14:05 +0100 Subject: [PATCH 39/73] Move code to make diff less confusing --- src/components/structures/RoomView.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f7f7ceb12c..9c41a993e3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -133,6 +133,12 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this.dispatcherRef = dis.register(this.onAction); + MatrixClientPeg.get().on("Room", this.onRoom); + MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); + MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + this.tabComplete = new TabComplete({ allowLooping: false, autoEnterTabComplete: true, @@ -142,12 +148,6 @@ module.exports = React.createClass({ } }); - this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room", this.onRoom); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); - if (this.props.roomAddress[0] == '#') { // we always look up the alias from the directory server: // we want the room that the given alias is pointing to From a61168d943099fec0f56bf10e3d31e75269ebe5e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jul 2016 16:54:56 +0100 Subject: [PATCH 40/73] Revert "Amends react template and removes opening image in lightbox" --- .../views/rooms/LinkPreviewWidget.js | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 3398b021fb..ba438c1d12 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -73,13 +73,36 @@ module.exports = React.createClass({ this.unmounted = true; }, + onImageClick: function(ev) { + var p = this.state.preview; + if (ev.button != 0 || ev.metaKey) return; + ev.preventDefault(); + var ImageView = sdk.getComponent("elements.ImageView"); + + var src = p["og:image"]; + if (src && src.startsWith("mxc://")) { + src = MatrixClientPeg.get().mxcUrlToHttp(src); + } + + var params = { + src: src, + width: p["og:image:width"], + height: p["og:image:height"], + name: p["og:title"] || p["og:description"] || this.props.link, + fileSize: p["matrix:image:size"], + link: this.props.link, + }; + + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); + }, + render: function() { var p = this.state.preview; if (!p) return
; // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? var image = p["og:image"]; - var imageMaxWidth = 600, imageMaxHeight = 400; + var imageMaxWidth = 100, imageMaxHeight = 100; if (image && image.startsWith("mxc://")) { image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); } @@ -91,24 +114,23 @@ module.exports = React.createClass({ var img; if (image) { - img = ( - - ); + img =
+ +
} return (
+ { img }
-
{ p["og:site_name"] ? ("from " + p["og:site_name"]) : null }
+
{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }
+
+ { p["og:description"] } +
- { img } -
{ p["og:description"] } Read more
- +
); } From 327015ba0f271bed0dac88d5a8ff7759c4847344 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 17:03:53 +0100 Subject: [PATCH 41/73] Tidy up autocomplete updating ..into a function --- src/components/structures/RoomView.js | 30 +++++++++++---------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9c41a993e3..75614471a0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -202,13 +202,8 @@ module.exports = React.createClass({ MatrixClientPeg.get().credentials.userId, 'join' ); + this._updateAutoComplete(); this.tabComplete.loadEntries(this.state.room); - - const myUserId = MatrixClientPeg.get().credentials.userId; - const members = this.state.room.getJoinedMembers().filter(function(member) { - if (member.userId !== myUserId) return true; - }); - UserProvider.getInstance().setUserList(members); } if (!user_is_in_room && this.state.roomId) { @@ -367,12 +362,8 @@ module.exports = React.createClass({ // and that has probably just changed if (ev.sender) { this.tabComplete.onMemberSpoke(ev.sender); - - const myUserId = MatrixClientPeg.get().credentials.userId; - const members = this.state.room.getJoinedMembers().filter(function(member) { - if (member.userId !== myUserId) return true; - }); - UserProvider.getInstance().setUserList(members); + // nb. we don't need to update the new autocomplete here since + // its results are currently ordered purely by search score. } }, @@ -452,12 +443,7 @@ module.exports = React.createClass({ // a member state changed in this room, refresh the tab complete list this.tabComplete.loadEntries(this.state.room); - - const myUserId = MatrixClientPeg.get().credentials.userId; - const members = this.state.room.getJoinedMembers().filter(function(member) { - if (member.userId !== myUserId) return true; - }); - UserProvider.getInstance().setUserList(members); + this._updateAutoComplete(); // if we are now a member of the room, where we were not before, that // means we have finished joining a room we were previously peeking @@ -1263,6 +1249,14 @@ module.exports = React.createClass({ } }, + _updateAutoComplete: function() { + const myUserId = MatrixClientPeg.get().credentials.userId; + const members = this.state.room.getJoinedMembers().filter(function(member) { + if (member.userId !== myUserId) return true; + }); + UserProvider.getInstance().setUserList(members); + }, + render: function() { var RoomHeader = sdk.getComponent('rooms.RoomHeader'); var MessageComposer = sdk.getComponent('rooms.MessageComposer'); From ccf8e269cde30f331804e66d51f8c1daf127b068 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 17:15:51 +0100 Subject: [PATCH 42/73] Comments & required props --- src/TabComplete.js | 5 +++++ src/components/structures/RoomStatusBar.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 5b7be7c286..0ec0b77802 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -48,7 +48,12 @@ class TabComplete { this.isFirstWord = false; // true if you tab-complete on the first word this.enterTabCompleteTimerId = null; this.inPassiveMode = false; + + // Map tracking ordering of the room members. + // userId: integer, highest comes first. this.memberTabOrder = {}; + + // monotonically increasing counter used for tracking ordering of members this.memberOrderSeq = 0; } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 4309b1e849..9a0d3dbbdd 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -28,7 +28,7 @@ module.exports = React.createClass({ room: React.PropTypes.object.isRequired, // a TabComplete object - tabComplete: React.PropTypes.object, + tabComplete: React.PropTypes.object.isRequired, // the number of messages which have arrived since we've been scrolled up numUnreadMessages: React.PropTypes.number, From 5c566cae5c01578aa0c164a52903368c7c4460b6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jul 2016 18:10:56 +0100 Subject: [PATCH 43/73] typo --- 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 75614471a0..abcccc48b8 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -358,7 +358,7 @@ module.exports = React.createClass({ } } - // update ther tab complete list as it depends on who most recently spoke, + // update the tab complete list as it depends on who most recently spoke, // and that has probably just changed if (ev.sender) { this.tabComplete.onMemberSpoke(ev.sender); From bfe50c209c2a44087b4ffdd17a7f32f4c48edc3c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 16 Jul 2016 22:49:06 +0100 Subject: [PATCH 44/73] fix typo in comment --- src/components/views/rooms/MemberInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 0e14776e82..e60a717c3e 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -366,7 +366,7 @@ module.exports = React.createClass({ var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); currentMembers = currentRoom.getJoinedMembers(); } - // if we're currently in a 1:1 with this user, start a new chat + // if we're currently not in a 1:1 with this user, start a new chat if (currentMembers && currentMembers.length === 2 && userIds.indexOf(currentMembers[0].userId) !== -1 && userIds.indexOf(currentMembers[1].userId) !== -1) From 2bc9dd43077ca10e1821e452241de86d228f86a8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 18:32:48 +0100 Subject: [PATCH 45/73] hopefully fix vector-im/vector-web#1813 --- .../views/rooms/InviteMemberList.js | 32 +++++++++++++++---- src/createRoom.js | 4 +++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/InviteMemberList.js b/src/components/views/rooms/InviteMemberList.js index 5246e2e54d..c1c83e533b 100644 --- a/src/components/views/rooms/InviteMemberList.js +++ b/src/components/views/rooms/InviteMemberList.js @@ -37,12 +37,31 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this._room = MatrixClientPeg.get().getRoom(this.props.roomId); + var cli = MatrixClientPeg.get(); + cli.on("RoomState.members", this.onRoomStateMember); + this._emailEntity = null; + + // we have to update the list whenever membership changes + // particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813 + this._updateList(); + }, + + componentDidMount: function() { + // initialise the email tile + this.onSearchQueryChanged(''); + }, + + componentWillUnmount: function() { + var cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener("RoomState.members", this.onRoomStateMember); + } + }, + + _updateList: function() { + this._room = MatrixClientPeg.get().getRoom(this.props.roomId); // Load the complete user list for inviting new users - // TODO: Keep this list bleeding-edge up-to-date. Practically speaking, - // it will do for now not being updated as random new users join different - // rooms as this list will be reloaded every room swap. if (this._room) { this._userList = MatrixClientPeg.get().getUsers().filter((u) => { return !this._room.hasMembershipState(u.userId, "join"); @@ -50,9 +69,8 @@ module.exports = React.createClass({ } }, - componentDidMount: function() { - // initialise the email tile - this.onSearchQueryChanged(''); + onRoomStateMember: function(ev, state, member) { + this._updateList(); }, onInvite: function(ev) { diff --git a/src/createRoom.js b/src/createRoom.js index 658561e78a..3a56dc1be3 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -69,6 +69,10 @@ function createRoom(opts) { return client.createRoom(createOpts).finally(function() { modal.close(); }).then(function(res) { + // NB createRoom doesn't block on the client seeing the echo that the + // room has been created, so we race here with the client knowing that + // the room exists, causing things like + // https://github.com/vector-im/vector-web/issues/1813 dis.dispatch({ action: 'view_room', room_id: res.room_id From 0553d806d71d47a35e77ebd0e87d2c51a36d1b5c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 18:40:54 +0100 Subject: [PATCH 46/73] stop re-invites --- src/components/views/rooms/InviteMemberList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/InviteMemberList.js b/src/components/views/rooms/InviteMemberList.js index c1c83e533b..4742597229 100644 --- a/src/components/views/rooms/InviteMemberList.js +++ b/src/components/views/rooms/InviteMemberList.js @@ -64,7 +64,8 @@ module.exports = React.createClass({ // Load the complete user list for inviting new users if (this._room) { this._userList = MatrixClientPeg.get().getUsers().filter((u) => { - return !this._room.hasMembershipState(u.userId, "join"); + return (!this._room.hasMembershipState(u.userId, "join") && + !this._room.hasMembershipState(u.userId, "invite")); }); } }, From 999c52b650abf6ea3d455b5f7cc9993f5353ea52 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 19:00:22 +0100 Subject: [PATCH 47/73] fix comment --- src/components/views/rooms/MemberInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index e60a717c3e..63ce78340c 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -366,7 +366,7 @@ module.exports = React.createClass({ var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); currentMembers = currentRoom.getJoinedMembers(); } - // if we're currently not in a 1:1 with this user, start a new chat + // if our current room is a 1:1 with the target user, start a new chat rather than NOOPing if (currentMembers && currentMembers.length === 2 && userIds.indexOf(currentMembers[0].userId) !== -1 && userIds.indexOf(currentMembers[1].userId) !== -1) From 554a6ff035ddbc66899f1edafd3e52138bb1227c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 19:41:53 +0100 Subject: [PATCH 48/73] improve wording of MemberInfo's start chat button. Fixes https://github.com/vector-im/vector-web/issues/689 and clarifies https://github.com/matrix-org/matrix-react-sdk/issues/328 --- src/components/views/rooms/MemberInfo.js | 112 +++++++++++++---------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 63ce78340c..c439f8b40c 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -61,12 +61,16 @@ module.exports = React.createClass({ updating: 0, devicesLoading: true, devices: null, + existingOneToOneRoomId: null, } }, - componentWillMount: function() { this._cancelDeviceList = null; + + this.setState({ + existingOneToOneRoomId: this.getExistingOneToOneRoomId() + }); }, componentDidMount: function() { @@ -90,6 +94,44 @@ module.exports = React.createClass({ } }, + getExistingOneToOneRoomId: function() { + var self = this; + var rooms = MatrixClientPeg.get().getRooms(); + var userIds = [ + this.props.member.userId, + MatrixClientPeg.get().credentials.userId + ]; + var existingRoomId; + + // roomId can be null here because of a hack in MatrixChat.onUserClick where we + // abuse this to view users rather than room members. + var currentMembers; + if (this.props.member.roomId) { + var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); + currentMembers = currentRoom.getJoinedMembers(); + } + + // reuse the first private 1:1 we find + existingRoomId = null; + + for (var i = 0; i < rooms.length; i++) { + // don't try to reuse public 1:1 rooms + var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); + if (join_rules && join_rules.getContent().join_rule === 'public') continue; + + var members = rooms[i].getJoinedMembers(); + if (members.length === 2 && + userIds.indexOf(members[0].userId) !== -1 && + userIds.indexOf(members[1].userId) !== -1) + { + existingRoomId = rooms[i].roomId; + break; + } + } + + return existingRoomId; + }, + onDeviceVerificationChanged: function(userId, device) { if (userId == this.props.member.userId) { // no need to re-download the whole thing; just update our copy of @@ -349,54 +391,17 @@ module.exports = React.createClass({ onChatClick: function() { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + // TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere + + var useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId); + // check if there are any existing rooms with just us and them (1:1) // If so, just view that room. If not, create a private room with them. - var self = this; - var rooms = MatrixClientPeg.get().getRooms(); - var userIds = [ - this.props.member.userId, - MatrixClientPeg.get().credentials.userId - ]; - var existingRoomId; - - // roomId can be null here because of a hack in MatrixChat.onUserClick where we - // abuse this to view users rather than room members. - var currentMembers; - if (this.props.member.roomId) { - var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); - currentMembers = currentRoom.getJoinedMembers(); - } - // if our current room is a 1:1 with the target user, start a new chat rather than NOOPing - if (currentMembers && currentMembers.length === 2 && - userIds.indexOf(currentMembers[0].userId) !== -1 && - userIds.indexOf(currentMembers[1].userId) !== -1) - { - existingRoomId = null; - } - // otherwise reuse the first private 1:1 we find - else { - existingRoomId = null; - - for (var i = 0; i < rooms.length; i++) { - // don't try to reuse public 1:1 rooms - var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); - if (join_rules && join_rules.getContent().join_rule === 'public') continue; - - var members = rooms[i].getJoinedMembers(); - if (members.length === 2 && - userIds.indexOf(members[0].userId) !== -1 && - userIds.indexOf(members[1].userId) !== -1) - { - existingRoomId = rooms[i].roomId; - break; - } - } - } - - if (existingRoomId) { + if (this.state.existingOneToOneRoomId) { dis.dispatch({ action: 'view_room', - room_id: existingRoomId + room_id: this.state.existingOneToOneRoomId, }); this.props.onFinished(); } @@ -553,7 +558,22 @@ module.exports = React.createClass({ if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) { // FIXME: we're referring to a vector component from react-sdk var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile'); - startChat = + + var label; + if (this.state.existingOneToOneRoomId) { + if (this.state.existingOneToOneRoomId == this.props.member.roomId) { + label = "Start new direct chat"; + } + else { + label = "Go to direct chat"; + } + } + else { + label = "Start direct chat"; + } + + startChat = } if (this.state.updating) { From 8d749be51f171ee81e6e0ee7eb0608ce9b167d5f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 17 Jul 2016 20:36:53 +0100 Subject: [PATCH 49/73] fix potential bug where canonical alias setting is broken (modulo autoheisting) --- src/components/views/room_settings/AliasSettings.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 34b6083c30..8c8ae659e8 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -83,13 +83,11 @@ module.exports = React.createClass({ alias: this.state.canonicalAlias }, "" ) - ); + ); } - // save new aliases for m.room.aliases var aliasOperations = this.getAliasOperations(); - var promises = []; for (var i = 0; i < aliasOperations.length; i++) { var alias_operation = aliasOperations[i]; console.log("alias %s %s", alias_operation.place, alias_operation.val); @@ -301,7 +299,7 @@ module.exports = React.createClass({
Add -
+
: "" }
From 1559c69ddf0cb9ca36dccdd3b559971db5382b02 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sun, 17 Jul 2016 21:21:27 +0100 Subject: [PATCH 50/73] Create room Dialog Spinner fix. --- src/createRoom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createRoom.js b/src/createRoom.js index 658561e78a..c00eca0486 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -64,7 +64,7 @@ function createRoom(opts) { } ]; - var modal = Modal.createDialog(Loader); + var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); return client.createRoom(createOpts).finally(function() { modal.close(); From a385955c6be8b6d33a90ef9ada37fe169979ad2a Mon Sep 17 00:00:00 2001 From: wmwragg Date: Sun, 17 Jul 2016 21:23:52 +0100 Subject: [PATCH 51/73] Fixed MatrixChat Dialog Spinner. Still needs testing, unsure what triggers it. --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index dcaa82fc75..dc9ca08e94 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -390,7 +390,7 @@ module.exports = React.createClass({ // FIXME: controller shouldn't be loading a view :( var Loader = sdk.getComponent("elements.Spinner"); - var modal = Modal.createDialog(Loader); + var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); d.then(function() { modal.close(); From bcd1c7e0997aab09c91f51463ddb7b536938d1d4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 01:34:26 +0100 Subject: [PATCH 52/73] improve comment --- src/HtmlUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 8b3a368f4d..2ab635081f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -186,7 +186,7 @@ module.exports = { * * highlights: optional list of words to highlight, ordered by longest word first * - * opts.highlightLink: optional href to add to highlights + * opts.highlightLink: optional href to add to highlighted words */ bodyToHtml: function(content, highlights, opts) { opts = opts || {}; From ebdac4ee50a408b6876a94982475ca2f73466fb3 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 01:35:42 +0100 Subject: [PATCH 53/73] first cut (untested) --- src/UserSettingsStore.js | 28 +++++++++ src/components/structures/MessagePanel.js | 4 ++ src/components/structures/RoomView.js | 46 +++++++++++++- src/components/structures/TimelinePanel.js | 4 ++ src/components/structures/UserSettings.js | 61 ++++++++++++++++++- src/components/views/messages/MessageEvent.js | 4 ++ src/components/views/messages/TextualBody.js | 29 +++++---- .../views/room_settings/ColorSettings.js | 2 +- src/components/views/rooms/EventTile.js | 4 ++ src/components/views/rooms/RoomSettings.js | 3 + 10 files changed, 171 insertions(+), 14 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 305994aa0e..990fa8b7a9 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -113,6 +113,34 @@ module.exports = { }); }, + getUrlPreviewsDisabled: function() { + var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + return (event && event.disable); + }, + + setUrlPreviewsDisabled: function(disabled) { + // FIXME: handle errors + MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { + disable: disabled + }); + }, + + getSyncedSettings: function() { + return MatrixClientPeg.get().getAccountData("im.vector.web.settings") || {}; + }, + + getSyncedSetting: function(type) { + var settings = this.getSyncedSettings(); + return settings[type]; + }, + + setSyncedSetting: function(type, value) { + var settings = this.getSyncedSettings(); + settings[type] = value; + // FIXME: handle errors + MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + }, + isFeatureEnabled: function(feature: string): boolean { return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true'; }, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c8e878118b..53efac6406 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -44,6 +44,9 @@ module.exports = React.createClass({ // ID of an event to highlight. If undefined, no event will be highlighted. highlightedEventId: React.PropTypes.string, + // Should we show URL Previews + showUrlPreview: React.PropTypes.bool, + // event after which we should show a read marker readMarkerEventId: React.PropTypes.string, @@ -365,6 +368,7 @@ module.exports = React.createClass({ onWidgetLoad={this._onWidgetLoad} readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} + showUrlPreview={this.props.showUrlPreview} checkUnmounting={this._isUnmounting} eventSendStatus={mxEv.status} last={last} isSelectedEvent={highlight}/> diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cfd359ea01..7a27340cc6 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -239,6 +239,8 @@ module.exports = React.createClass({ MatrixClientPeg.get().stopPeeking(); this._onRoomLoaded(this.state.room); } + + _updatePreviewUrlVisibility(this.state.room); }, shouldComponentUpdate: function(nextProps, nextState) { @@ -341,6 +343,10 @@ module.exports = React.createClass({ // ignore events for other rooms if (!this.state.room || room.roomId != this.state.room.roomId) return; + if (event.getType() === "org.matrix.room.preview_urls") { + _updatePreviewUrlVisibility(room); + } + // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; @@ -384,6 +390,40 @@ module.exports = React.createClass({ } }, + _updatePreviewUrlVisibility: function(room) { + // check our per-room overrides + var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); + if (roomPreviewUrls && roomPreviewUrls.disabled !== undefined) { + this.setState({ + showUrlPreview: !roomPreviewUrls.disabled + }); + return; + } + + // check our global disable override + var userRoomPreviewUrls = MatrixClientPeg().get().getAccountData("org.matrix.preview_urls"); + if (userRoomPreviewUrls && userRoomPreviewUrls.disabled) { + this.setState({ + showUrlPreview: false + }); + return; + } + + // check the room state event + var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); + if (roomStatePreviewUrls && roomStatePreviewUrls.disabled) { + this.setState({ + showUrlPreview: false; + }); + return; + } + + // otherwise, we assume they're on. + this.setState({ + showUrlPreview: true; + }); + }, + onRoom: function(room) { // This event is fired when the room is 'stored' by the JS SDK, which // means it's now a fully-fledged room object ready to be used, so @@ -416,12 +456,15 @@ module.exports = React.createClass({ onRoomAccountData: function(room, event) { if (room.roomId == this.props.roomId) { - if (event.getType === "org.matrix.room.color_scheme") { + if (event.getType() === "org.matrix.room.color_scheme") { var color_scheme = event.getContent(); // XXX: we should validate the event console.log("Tinter.tint from onRoomAccountData"); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } + else if (event.getType() === "org.matrix.room.preview_urls") { + _updatePreviewUrlVisibility(room); + } } }, @@ -1523,6 +1566,7 @@ module.exports = React.createClass({ eventPixelOffset={this.props.eventPixelOffset} onScroll={ this.onMessageListScroll } onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } + showUrlPreview = { this.state.showUrlPreview } opacity={ this.props.opacity } />); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index d804dfd6b9..52225c7c09 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -71,6 +71,9 @@ var TimelinePanel = React.createClass({ // half way down the viewport. eventPixelOffset: React.PropTypes.number, + // Should we show URL Previews + showUrlPreview: React.PropTypes.bool, + // callback which is called when the panel is scrolled. onScroll: React.PropTypes.func, @@ -934,6 +937,7 @@ var TimelinePanel = React.createClass({ readMarkerEventId={ this.state.readMarkerEventId } readMarkerVisible={ this.state.readMarkerVisible } suppressFirstDateSeparator={ this.state.canBackPaginate } + showUrlPreview = { this.state.showUrlPreview } ourUserId={ MatrixClientPeg.get().credentials.userId } stickyBottom={ stickyBottom } onScroll={ this.onMessageListScroll } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 7fcb81a60c..41ac03725b 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -261,6 +261,63 @@ module.exports = React.createClass({ }); }, + _renderUserInterfaceSettings: function() { + var client = MatrixClientPeg.get(); + + var settingsLabels = [ + /* + { + id: 'alwaysShowTimestamps', + label: 'Always show message timestamps', + }, + { + id: 'showTwelveHourTimestamps', + label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', + }, + { + id: 'useCompactLayout', + label: 'Use compact timeline layout', + }, + { + id: 'useFixedWidthFont', + label: 'Use fixed width font', + }, + */ + ]; + + var syncedSettings = UserSettingsStore.getSyncedSettings(); + + return ( +
+

User Interface

+
+
+ UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } + /> + +
+
+ { settingsLabels.forEach( setting => { +
+ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + /> + +
+ })} +
+ ); + }, + _renderDeviceInfo: function() { if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { return null; @@ -378,7 +435,7 @@ module.exports = React.createClass({ this._renderLabs = function () { let features = LABS_FEATURES.map(feature => ( -
+
; }, }); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 310da598fa..2268affbe2 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -39,6 +39,9 @@ module.exports = React.createClass({ /* link URL for the highlights */ highlightLink: React.PropTypes.string, + /* should show URL previews for this event */ + showUrlPreview: React.PropTypes.bool, + /* callback for when our widget has loaded */ onWidgetLoad: React.PropTypes.func, }, @@ -57,16 +60,18 @@ module.exports = React.createClass({ componentDidMount: function() { linkifyElement(this.refs.content, linkifyMatrix.options); - var links = this.findLinks(this.refs.content.children); - if (links.length) { - this.setState({ links: links.map((link)=>{ - return link.getAttribute("href"); - })}); + if (this.props.showUrlPreview) { + var links = this.findLinks(this.refs.content.children); + if (links.length) { + this.setState({ links: links.map((link)=>{ + return link.getAttribute("href"); + })}); - // lazy-load the hidden state of the preview widget from localstorage - if (global.localStorage) { - var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId()); - this.setState({ widgetHidden: hidden }); + // lazy-load the hidden state of the preview widget from localstorage + if (global.localStorage) { + var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId()); + this.setState({ widgetHidden: hidden }); + } } } @@ -163,9 +168,11 @@ module.exports = React.createClass({ render: function() { var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); - var body = HtmlUtils.bodyToHtml(content, this.props.highlights, - {highlightLink: this.props.highlightLink}); + var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); + if (this.props.highlightLink) { + body = { body }; + } var widgets; if (this.state.links.length && !this.state.widgetHidden) { diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index fff97ea817..6d147b1f63 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ data.primary_color = scheme.primary_color; data.secondary_color = scheme.secondary_color; data.index = this._getColorIndex(data); - + if (data.index === -1) { // append the unrecognised colours to our palette data.index = ROOM_COLORS.length; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 70dfe8ac33..a914d513ac 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -101,6 +101,9 @@ module.exports = React.createClass({ /* link URL for the highlights */ highlightLink: React.PropTypes.string, + /* should show URL previews for this event */ + showUrlPreview: React.PropTypes.bool, + /* is this the focused event */ isSelectedEvent: React.PropTypes.bool, @@ -420,6 +423,7 @@ module.exports = React.createClass({
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 1f50a9241b..ee8b716e02 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -422,6 +422,7 @@ module.exports = React.createClass({ var AliasSettings = sdk.getComponent("room_settings.AliasSettings"); var ColorSettings = sdk.getComponent("room_settings.ColorSettings"); + var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); @@ -654,6 +655,8 @@ module.exports = React.createClass({ canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')} aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} /> + +

Permissions

From f13bb5f6562080250c5947637730e23fdf503879 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 01:39:24 +0100 Subject: [PATCH 54/73] typos --- src/components/structures/RoomView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 7a27340cc6..35217ab2f4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -413,14 +413,14 @@ module.exports = React.createClass({ var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); if (roomStatePreviewUrls && roomStatePreviewUrls.disabled) { this.setState({ - showUrlPreview: false; + showUrlPreview: false }); return; } // otherwise, we assume they're on. this.setState({ - showUrlPreview: true; + showUrlPreview: true }); }, From e92024f7a9ecdfc06d83a05bab0b14ff1693386f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 10:42:18 +0100 Subject: [PATCH 55/73] reskindex --- src/component-index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/component-index.js b/src/component-index.js index 4aa0efe21f..5fadb18b6a 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -74,6 +74,8 @@ module.exports.components['views.messages.TextualEvent'] = require('./components module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings'); module.exports.components['views.room_settings.ColorSettings'] = require('./components/views/room_settings/ColorSettings'); +module.exports.components['views.room_settings.UrlPreviewSettings'] = require('./components/views/room_settings/UrlPreviewSettings'); +module.exports.components['views.rooms.Autocomplete'] = require('./components/views/rooms/Autocomplete'); module.exports.components['views.rooms.AuxPanel'] = require('./components/views/rooms/AuxPanel'); module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile'); module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); From 6bf54992895a0e768e4a453a25b5d5438e9d3317 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 18 Jul 2016 10:47:03 +0100 Subject: [PATCH 56/73] typos --- src/components/structures/RoomView.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 35217ab2f4..ea73193b03 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -240,7 +240,7 @@ module.exports = React.createClass({ this._onRoomLoaded(this.state.room); } - _updatePreviewUrlVisibility(this.state.room); + this._updatePreviewUrlVisibility(this.state.room); }, shouldComponentUpdate: function(nextProps, nextState) { @@ -343,8 +343,8 @@ module.exports = React.createClass({ // ignore events for other rooms if (!this.state.room || room.roomId != this.state.room.roomId) return; - if (event.getType() === "org.matrix.room.preview_urls") { - _updatePreviewUrlVisibility(room); + if (ev.getType() === "org.matrix.room.preview_urls") { + this._updatePreviewUrlVisibility(room); } // ignore anything but real-time updates at the end of the room: From 743f79a9a7475aa1d655adbfcda233e58cb00489 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 18 Jul 2016 15:22:08 +0100 Subject: [PATCH 57/73] Fix unpublishing room in room settings Fixes https://github.com/vector-im/vector-web/issues/1743 --- src/components/views/rooms/RoomSettings.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 1f50a9241b..8f1a2cd141 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -65,7 +65,12 @@ module.exports = React.createClass({ tags_changed: false, tags: tags, areNotifsMuted: areNotifsMuted, - isRoomPublished: false, // loaded async in componentWillMount + // isRoomPublished is loaded async in componentWillMount so when the component + // inits, the saved value will always be undefined, however getInitialState() + // is also called from the saving code so we must return the correct value here + // if we have it (although this could race if the user saves before we load whether + // the room is oublisherd or not. + isRoomPublished: this._originalIsRoomPublished, }; }, From ffaf7f44f3a716e656f93b90285b737e2759d839 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 18 Jul 2016 15:36:19 +0100 Subject: [PATCH 58/73] typos --- src/components/views/rooms/RoomSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 8f1a2cd141..df30b63c75 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -69,7 +69,7 @@ module.exports = React.createClass({ // inits, the saved value will always be undefined, however getInitialState() // is also called from the saving code so we must return the correct value here // if we have it (although this could race if the user saves before we load whether - // the room is oublisherd or not. + // the room is published or not). isRoomPublished: this._originalIsRoomPublished, }; }, From 9e4511b8eb60d8a81fd654593dae9a1c4584b697 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Mon, 18 Jul 2016 16:10:07 +0100 Subject: [PATCH 59/73] Now showing three dots when hovering over the badge --- src/components/views/rooms/RoomTile.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index da9f97ab65..d3fbfa0220 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -43,7 +43,10 @@ module.exports = React.createClass({ }, getInitialState: function() { - return( { hover : false }); + return({ + hover : false, + badgeHover : false, + }); }, onClick: function() { @@ -61,6 +64,14 @@ module.exports = React.createClass({ this.setState( { hover : false }); }, + badgeOnMouseEnter: function() { + this.setState( { badgeHover : true } ); + }, + + badgeOnMouseLeave: function() { + this.setState( { badgeHover : false } ); + }, + render: function() { var myUserId = MatrixClientPeg.get().credentials.userId; var me = this.props.room.currentState.members[myUserId]; @@ -83,8 +94,14 @@ module.exports = React.createClass({ name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon var badge; + var badgeContent; if (this.props.highlight || notificationCount > 0) { - badge =
{ notificationCount ? notificationCount : '!' }
; + if (this.state.badgeHover) { + badgeContent = "\u00B7 \u00B7 \u00B7"; + } else { + badgeContent = notificationCount ? notificationCount : '!'; + } + badge =
{ badgeContent }
; } /* if (this.props.highlight) { From 4bd2b93f5909aeb5c4d4e41fd265ce28afda2fbc Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 19 Jul 2016 16:05:15 +0100 Subject: [PATCH 60/73] Add support for sending uploaded content as m.video --- src/ContentMessages.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 56e3499eae..796c1ed58d 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -52,6 +52,36 @@ function infoForImageFile(imageFile) { return deferred.promise; } +function infoForVideoFile(videoFile) { + var deferred = q.defer(); + + // Load the file into an html element + var video = document.createElement("video"); + + var reader = new FileReader(); + reader.onload = function(e) { + video.src = e.target.result; + + // Once ready, returns its size + video.onloadedmetadata = function() { + deferred.resolve({ + w: video.videoWidth, + h: video.videoHeight + }); + }; + video.onerror = function(e) { + deferred.reject(e); + }; + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsDataURL(videoFile); + + return deferred.promise; +} + + class ContentMessages { constructor() { this.inprogress = []; @@ -81,6 +111,12 @@ class ContentMessages { } else if (file.type.indexOf('audio/') == 0) { content.msgtype = 'm.audio'; def.resolve(); + } else if (file.type.indexOf('video/') == 0) { + content.msgtype = 'm.video'; + infoForVideoFile(file).then(function (videoInfo) { + extend(content.info, videoInfo); + def.resolve(); + }); } else { content.msgtype = 'm.file'; def.resolve(); From d6df3682f28af5bb4f1e6430d27615703fa52413 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Jul 2016 11:58:49 +0100 Subject: [PATCH 61/73] Fix enourmous video bug --- src/components/views/messages/MVideoBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 6cbaf0b151..c8327a71ae 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -34,7 +34,7 @@ module.exports = React.createClass({ } if (fullWidth < thumbWidth && fullHeight < thumbHeight) { // no scaling needs to be applied - return fullHeight; + return 1; } var widthMulti = thumbWidth / fullWidth; var heightMulti = thumbHeight / fullHeight; From 1365f188294b67ed88c01faa43a8ce0640d6bfd4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Jul 2016 12:03:13 +0100 Subject: [PATCH 62/73] many stupid thinkos and bugfixes; make it work --- src/UserSettingsStore.js | 5 +- src/components/structures/RoomView.js | 23 +-- src/components/structures/TimelinePanel.js | 2 +- src/components/views/messages/TextualBody.js | 41 +++-- .../views/room_settings/UrlPreviewSettings.js | 157 ++++++++++++++++++ src/components/views/rooms/EventTile.js | 2 + src/components/views/rooms/RoomSettings.js | 10 +- 7 files changed, 211 insertions(+), 29 deletions(-) create mode 100644 src/components/views/room_settings/UrlPreviewSettings.js diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 990fa8b7a9..39f393b242 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -115,7 +115,7 @@ module.exports = { getUrlPreviewsDisabled: function() { var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); - return (event && event.disable); + return (event && event.getContent().disable); }, setUrlPreviewsDisabled: function(disabled) { @@ -126,7 +126,8 @@ module.exports = { }, getSyncedSettings: function() { - return MatrixClientPeg.get().getAccountData("im.vector.web.settings") || {}; + var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + return event ? event.getContent() : {}; }, getSyncedSetting: function(type) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ea73193b03..7b5b3f7c7f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -239,8 +239,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().stopPeeking(); this._onRoomLoaded(this.state.room); } - - this._updatePreviewUrlVisibility(this.state.room); }, shouldComponentUpdate: function(nextProps, nextState) { @@ -372,6 +370,7 @@ module.exports = React.createClass({ // after a successful peek, or after we join the room). _onRoomLoaded: function(room) { this._calculatePeekRules(room); + this._updatePreviewUrlVisibility(room); }, _calculatePeekRules: function(room) { @@ -391,18 +390,20 @@ module.exports = React.createClass({ }, _updatePreviewUrlVisibility: function(room) { + console.log("_updatePreviewUrlVisibility"); + // check our per-room overrides var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); - if (roomPreviewUrls && roomPreviewUrls.disabled !== undefined) { + if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) { this.setState({ - showUrlPreview: !roomPreviewUrls.disabled + showUrlPreview: !roomPreviewUrls.getContent().disable }); return; } // check our global disable override - var userRoomPreviewUrls = MatrixClientPeg().get().getAccountData("org.matrix.preview_urls"); - if (userRoomPreviewUrls && userRoomPreviewUrls.disabled) { + var userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) { this.setState({ showUrlPreview: false }); @@ -411,7 +412,7 @@ module.exports = React.createClass({ // check the room state event var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); - if (roomStatePreviewUrls && roomStatePreviewUrls.disabled) { + if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) { this.setState({ showUrlPreview: false }); @@ -454,8 +455,8 @@ module.exports = React.createClass({ Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); }, - onRoomAccountData: function(room, event) { - if (room.roomId == this.props.roomId) { + onRoomAccountData: function(event, room) { + if (room.roomId == this.state.roomId) { if (event.getType() === "org.matrix.room.color_scheme") { var color_scheme = event.getContent(); // XXX: we should validate the event @@ -463,7 +464,7 @@ module.exports = React.createClass({ Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } else if (event.getType() === "org.matrix.room.preview_urls") { - _updatePreviewUrlVisibility(room); + this._updatePreviewUrlVisibility(room); } } }, @@ -1557,6 +1558,8 @@ module.exports = React.createClass({ hideMessagePanel = true; } + console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); + var messagePanel = ( { @@ -74,19 +98,6 @@ module.exports = React.createClass({ } } } - - if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") - HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); - }, - - shouldComponentUpdate: function(nextProps, nextState) { - // exploit that events are immutable :) - // ...and that .links is only ever set in componentDidMount and never changes - return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || - nextProps.highlights !== this.props.highlights || - nextProps.highlightLink !== this.props.highlightLink || - nextState.links !== this.state.links || - nextState.widgetHidden !== this.state.widgetHidden); }, findLinks: function(nodes) { @@ -175,7 +186,7 @@ module.exports = React.createClass({ } var widgets; - if (this.state.links.length && !this.state.widgetHidden) { + if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget'); widgets = this.state.links.map((link)=>{ return + + Disable URL previews by default for participants in this room + + } + else { + disableRoomPreviewUrls = + + } + + return ( +
+

URL Previews

+ + + { disableRoomPreviewUrls } + + +
+ ); + + } +}); \ No newline at end of file diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index a914d513ac..77be8226a2 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -362,6 +362,8 @@ module.exports = React.createClass({ var SenderProfile = sdk.getComponent('messages.SenderProfile'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + //console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + var content = this.props.mxEvent.getContent(); var msgtype = content.msgtype; diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index ee8b716e02..fda29a38f0 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -211,10 +211,13 @@ module.exports = React.createClass({ // color scheme promises.push(this.saveColor()); + // url preview settings + promises.push(this.saveUrlPreviewSettings()); + // encryption promises.push(this.saveEncryption()); - console.log("Performing %s operations", promises.length); + console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises)); return q.allSettled(promises); }, @@ -228,6 +231,11 @@ module.exports = React.createClass({ return this.refs.color_settings.saveSettings(); }, + saveUrlPreviewSettings: function() { + if (!this.refs.url_preview_settings) { return q(); } + return this.refs.url_preview_settings.saveSettings(); + }, + saveEncryption: function () { if (!this.refs.encrypt) { return q(); } From 33edb1dea96b9eb3a41dac5f479cce75ee0a2122 Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 20 Jul 2016 12:47:32 +0100 Subject: [PATCH 63/73] Made the badge always exist, but if no unread mesasges available then has a different class --- src/components/views/rooms/RoomTile.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index d3fbfa0220..e510c2abed 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -95,14 +95,24 @@ module.exports = React.createClass({ name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon var badge; var badgeContent; - if (this.props.highlight || notificationCount > 0) { - if (this.state.badgeHover) { - badgeContent = "\u00B7 \u00B7 \u00B7"; - } else { - badgeContent = notificationCount ? notificationCount : '!'; - } - badge =
{ badgeContent }
; + var badgeClasses; + + if (this.state.badgeHover) { + badgeContent = "\u00B7 \u00B7 \u00B7"; + } else if (this.props.highlight || notificationCount > 0) { + badgeContent = notificationCount ? notificationCount : '!'; + } else { + badgeContent = '\u200B'; } + + if (this.props.highlight || notificationCount > 0) { + badgeClasses = "mx_RoomTile_badge" + } else { + badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; + } + + badge =
{ badgeContent }
; + /* if (this.props.highlight) { badge =
!
; From ffa43ebb96039920805dff52e721775af343e14e Mon Sep 17 00:00:00 2001 From: wmwragg Date: Wed, 20 Jul 2016 17:12:41 +0100 Subject: [PATCH 64/73] badge content when hovered is now kerned via the CSS --- src/components/views/rooms/RoomTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index e510c2abed..aa83110632 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -98,7 +98,7 @@ module.exports = React.createClass({ var badgeClasses; if (this.state.badgeHover) { - badgeContent = "\u00B7 \u00B7 \u00B7"; + badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { badgeContent = notificationCount ? notificationCount : '!'; } else { @@ -106,7 +106,7 @@ module.exports = React.createClass({ } if (this.props.highlight || notificationCount > 0) { - badgeClasses = "mx_RoomTile_badge" + badgeClasses = "mx_RoomTile_badge"; } else { badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; } From d6415aceca539a8c084ba4932864a532e129fe7d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Jul 2016 18:14:16 +0100 Subject: [PATCH 65/73] handle accountData changes, and errors on toggling URL previews --- src/UserSettingsStore.js | 4 ++-- src/components/structures/RoomView.js | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 39f393b242..f4eb4f0d83 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -120,7 +120,7 @@ module.exports = { setUrlPreviewsDisabled: function(disabled) { // FIXME: handle errors - MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { + return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { disable: disabled }); }, @@ -139,7 +139,7 @@ module.exports = { var settings = this.getSyncedSettings(); settings[type] = value; // FIXME: handle errors - MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); }, isFeatureEnabled: function(feature: string): boolean { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f73b90ae28..cef85931e1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -138,6 +138,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().on("accountData", this.onAccountData); this.tabComplete = new TabComplete({ allowLooping: false, @@ -460,6 +461,12 @@ module.exports = React.createClass({ Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); }, + onAccountData: function(event) { + if (event.getType() === "org.matrix.preview_urls" && this.state.room) { + this._updatePreviewUrlVisibility(this.state.room); + } + }, + onRoomAccountData: function(event, room) { if (room.roomId == this.state.roomId) { if (event.getType() === "org.matrix.room.color_scheme") { From bc87fc575b55bb776d17c67431bc1157d9beeeb8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 20 Jul 2016 18:16:54 +0100 Subject: [PATCH 66/73] remove debug --- 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 cef85931e1..47a81fe18f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1557,7 +1557,7 @@ module.exports = React.createClass({ hideMessagePanel = true; } - console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); + // console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); var messagePanel = ( Date: Wed, 20 Jul 2016 18:17:09 +0100 Subject: [PATCH 67/73] remove debug --- 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 47a81fe18f..8ca3289cb6 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -396,7 +396,7 @@ module.exports = React.createClass({ }, _updatePreviewUrlVisibility: function(room) { - console.log("_updatePreviewUrlVisibility"); + // console.log("_updatePreviewUrlVisibility"); // check our per-room overrides var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); From e41df245c32072cb8bb81495d4af2c12c7ce2110 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 20 Jul 2016 20:20:10 +0100 Subject: [PATCH 68/73] Set the device_id on pre-login MatrixClient In order that device_id is set when we register a new user (or, for that matter, when we register as a guest), we need to make sure that device_id is set on the temporary MatrixClient which is created before the user is logged in - ie, that created by replaceUsingUrls. In order to achieve this, I've basically removed the distinction between replaceUsingAccessToken and replaceUsingUrls. There is a side-effect in that the temporary MatrixClient now gets an e2e sessionStore, but I don't think that is a bad thing. --- src/MatrixClientPeg.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 143b804228..2383a48d0f 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -97,35 +97,20 @@ class MatrixClient { // FIXME, XXX: this all seems very convoluted :( // - // if we replace the singleton using URLs we bypass our createClientForPeg() - // global helper function... but if we replace it using - // an access_token we don't? - // // Why do we have this peg wrapper rather than just MatrixClient.get()? // Why do we name MatrixClient as MatrixClientPeg when we export it? // // -matthew replaceUsingUrls(hs_url, is_url) { - matrixClient = Matrix.createClient({ - baseUrl: hs_url, - idBaseUrl: is_url - }); - - // XXX: factor this out with the localStorage setting in replaceUsingAccessToken - if (localStorage) { - try { - localStorage.setItem("mx_hs_url", hs_url); - localStorage.setItem("mx_is_url", is_url); - } catch (e) { - console.warn("Error using local storage: can't persist HS/IS URLs!"); - } - } else { - console.warn("No local storage available: can't persist HS/IS URLs!"); - } + this.replaceClient(hs_url, is_url); } replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) { + this.replaceClient(hs_url, is_url, user_id, access_token, isGuest); + } + + replaceClient(hs_url, is_url, user_id, access_token, isGuest) { if (localStorage) { try { localStorage.clear(); From 030652f289a80bb30a7dc485fdb060ee1c88fe5d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Jul 2016 13:49:31 +0100 Subject: [PATCH 69/73] Add removeLisatener for account data listener --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8ca3289cb6..accf96f349 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -268,6 +268,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } window.removeEventListener('resize', this.onResize); From 0b0f10ddf6c0488d48837a8796761b6171875c88 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Jul 2016 16:25:51 +0100 Subject: [PATCH 70/73] Fix tab complete order properly Don't return NaN from your sort functions... --- src/TabComplete.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 0ec0b77802..65441c9381 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -341,7 +341,12 @@ class TabComplete { } if (a.kind == 'member') { - return this.memberTabOrder[b.member.userId] - this.memberTabOrder[a.member.userId]; + let orderA = this.memberTabOrder[a.member.userId]; + let orderB = this.memberTabOrder[b.member.userId]; + if (orderA === undefined) orderA = -1; + if (orderB === undefined) orderB = -1; + + return orderB - orderA; } // anything else we have no ordering for From ad7f8d0a58de9222becf88b08d4b13be5c7f9fc0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 22 Jul 2016 10:12:37 +0100 Subject: [PATCH 71/73] Bump to latest react-gemini-scrollbar I've updated our forks of the gemini-scrollbar project to latest upstream. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd81ad7c56..39709e7e2e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "react": "^15.0.1", "react-addons-css-transition-group": "^15.1.0", "react-dom": "^15.0.1", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf", "sanitize-html": "^1.11.1", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" From e3cdeed32b706b892e34658556fcea1a08516c84 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 22 Jul 2016 10:43:50 +0100 Subject: [PATCH 72/73] Bump to react 15.2.1 This should also stop npm complaining about invalid peerDependencies. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 39709e7e2e..0d2f6e1a5b 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,9 @@ "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "q": "^1.4.1", - "react": "^15.0.1", - "react-addons-css-transition-group": "^15.1.0", - "react-dom": "^15.0.1", + "react": "^15.2.1", + "react-addons-css-transition-group": "^15.2.1", + "react-dom": "^15.2.1", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf", "sanitize-html": "^1.11.1", "velocity-vector": "vector-im/velocity#059e3b2", From b07e50d418c5d2e75f0725e5699cf415c689181e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Jul 2016 17:30:25 +0100 Subject: [PATCH 73/73] Fix 'start chat' button on MemberInfo this/self fail & related scoping Fixes https://github.com/vector-im/vector-web/issues/1844 --- src/components/views/rooms/MemberInfo.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index c439f8b40c..07a7b9398d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -406,14 +406,14 @@ module.exports = React.createClass({ this.props.onFinished(); } else { - self.setState({ updating: self.state.updating + 1 }); + this.setState({ updating: this.state.updating + 1 }); createRoom({ createOpts: { invite: [this.props.member.userId], }, - }).finally(function() { - self.props.onFinished(); - self.setState({ updating: self.state.updating - 1 }); + }).finally(() => { + this.props.onFinished(); + this.setState({ updating: this.state.updating - 1 }); }).done(); } },