From e1e87807b52ca7233eb0ec331aec6078c022c9aa Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Jan 2017 18:51:28 +0000 Subject: [PATCH 01/49] Look up email addresses in ChatInviteDialog So email addresses known to the IS get a display name & avatar --- .../views/dialogs/ChatInviteDialog.js | 73 +++++++++++++++---- src/components/views/elements/AddressTile.js | 22 ++++-- 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 61503196e5..4fadad5f84 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require("react"); -var classNames = require('classnames'); -var sdk = require("../../../index"); -var Invite = require("../../../Invite"); -var createRoom = require("../../../createRoom"); -var MatrixClientPeg = require("../../../MatrixClientPeg"); -var DMRoomMap = require('../../../utils/DMRoomMap'); -var rate_limited_func = require("../../../ratelimitedfunc"); -var dis = require("../../../dispatcher"); -var Modal = require('../../../Modal'); +import React from 'react'; +import classNames from 'classnames'; +import sdk from '../../../index'; +import { getAddressType } from '../../../Invite'; +import createRoom from '../../../createRoom'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import rate_limited_func from '../../../ratelimitedfunc'; +import dis from '../../../dispatcher'; +import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; +import q from 'q'; const TRUNCATE_QUERY_LIST = 40; @@ -186,13 +187,22 @@ module.exports = React.createClass({ // If the query isn't a user we know about, but is a // valid address, add an entry for that if (queryList.length == 0) { - const addrType = Invite.getAddressType(query); + const addrType = getAddressType(query); if (addrType !== null) { - queryList.push({ + queryList[0] = { addressType: addrType, address: query, isKnown: false, - }); + }; + if (addrType == 'email') { + this._lookupThreepid(addrType, query).then((res) => { + if (res !== null) { + this.setState({ + queryList: [res] + }); + } + }).done(); + } } } } @@ -380,7 +390,7 @@ module.exports = React.createClass({ }, _isDmChat: function(addrs) { - if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) { + if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) { return true; } else { return false; @@ -408,7 +418,7 @@ module.exports = React.createClass({ _addInputToList: function() { const addressText = this.refs.textinput.value.trim(); - const addrType = Invite.getAddressType(addressText); + const addrType = getAddressType(addressText); const addrObj = { addressType: addrType, address: addressText, @@ -435,6 +445,39 @@ module.exports = React.createClass({ return inviteList; }, + _lookupThreepid(medium, address) { + // wait a bit to let the user finish typing + return q.delay(500).then(() => { + // If the query has changed, forget it + if (this.state.queryList[0] && this.state.queryList[0].address !== address) { + return null; + } + return MatrixClientPeg.get().lookupThreePid(medium, address); + }).then((res) => { + if (res === null || !res.mxid) return null; + // If the query has changed now, drop the response + if (this.state.queryList[0] && this.state.queryList[0].address !== address) { + return null; + } + + return MatrixClientPeg.get().getProfileInfo(res.mxid); + }).then((res) => { + if (res === null) return null; + // If the query has changed now, drop the response + if (this.state.queryList[0] && this.state.queryList[0].address !== address) { + return null; + } + // return an InviteAddressType + return { + addressType: medium, + address: address, + displayName: res.displayname, + avatarMxc: res.avatar_url, + isKnown: true, + } + }); + }, + render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const AddressSelector = sdk.getComponent("elements.AddressSelector"); diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 01c1ed3255..18492d8ae6 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -94,14 +94,14 @@ export default React.createClass({ const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const nameClasses = classNames({ + "mx_AddressTile_name": true, + "mx_AddressTile_justified": this.props.justified, + }); + let info; let error = false; if (address.addressType === "mx" && address.isKnown) { - const nameClasses = classNames({ - "mx_AddressTile_name": true, - "mx_AddressTile_justified": this.props.justified, - }); - const idClasses = classNames({ "mx_AddressTile_id": true, "mx_AddressTile_justified": this.props.justified, @@ -123,13 +123,21 @@ export default React.createClass({
{ this.props.address.address }
); } else if (address.addressType === "email") { - var emailClasses = classNames({ + const emailClasses = classNames({ "mx_AddressTile_email": true, "mx_AddressTile_justified": this.props.justified, }); + let nameNode = null; + if (address.displayName) { + nameNode =
{ address.displayName }
+ } + info = ( -
{ address.address }
+
+
{ address.address }
+ {nameNode} +
); } else { error = true; From 81d95ecea01122a9d94f8ff14d55a6377244a2bb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 25 Jan 2017 22:15:09 +0000 Subject: [PATCH 02/49] Fix cancel button on e2e import/export dialogs Make sure that we preventDefault on the cancel button. Fixes https://github.com/vector-im/riot-web/issues/3066 --- .../views/dialogs/ExportE2eKeysDialog.js | 14 +++++++++----- .../views/dialogs/ImportE2eKeysDialog.js | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 816b8eb73d..56b9d56cc9 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -71,7 +71,7 @@ export default React.createClass({ return this.props.matrixClient.exportRoomKeys(); }).then((k) => { return MegolmExportEncryption.encryptMegolmKeyFile( - JSON.stringify(k), passphrase + JSON.stringify(k), passphrase, ); }).then((f) => { const blob = new Blob([f], { @@ -95,9 +95,14 @@ export default React.createClass({ }); }, + _onCancelClick: function(ev) { + ev.preventDefault(); + this.props.onFinished(false); + return false; + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); const disableForm = (this.state.phase === PHASE_EXPORTING); @@ -159,10 +164,9 @@ export default React.createClass({ - + diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 586bd9b6cc..ddd13813e2 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -80,7 +80,7 @@ export default React.createClass({ return readFileAsArrayBuffer(file).then((arrayBuffer) => { return MegolmExportEncryption.decryptMegolmKeyFile( - arrayBuffer, passphrase + arrayBuffer, passphrase, ); }).then((keys) => { return this.props.matrixClient.importRoomKeys(JSON.parse(keys)); @@ -98,9 +98,14 @@ export default React.createClass({ }); }, + _onCancelClick: function(ev) { + ev.preventDefault(); + this.props.onFinished(false); + return false; + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); const disableForm = (this.state.phase !== PHASE_EDIT); @@ -158,10 +163,9 @@ export default React.createClass({ - + From bf66f77acb79d5bad1a354f43e290930e2947731 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Jan 2017 10:08:44 +0000 Subject: [PATCH 03/49] Set state in _lookupThreepid --- .../views/dialogs/ChatInviteDialog.js | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 4fadad5f84..09a18e5208 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -195,13 +195,7 @@ module.exports = React.createClass({ isKnown: false, }; if (addrType == 'email') { - this._lookupThreepid(addrType, query).then((res) => { - if (res !== null) { - this.setState({ - queryList: [res] - }); - } - }).done(); + this._lookupThreepid(addrType, query).done(); } } } @@ -467,14 +461,16 @@ module.exports = React.createClass({ if (this.state.queryList[0] && this.state.queryList[0].address !== address) { return null; } - // return an InviteAddressType - return { - addressType: medium, - address: address, - displayName: res.displayname, - avatarMxc: res.avatar_url, - isKnown: true, - } + this.setState({ + queryList: [{ + // an InviteAddressType + addressType: medium, + address: address, + displayName: res.displayname, + avatarMxc: res.avatar_url, + isKnown: true, + }] + }); }); }, From 23a25e550dcc08993f87ddc6f04fc4d33698dd78 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Jan 2017 10:09:33 +0000 Subject: [PATCH 04/49] Missed a `function` --- src/components/views/dialogs/ChatInviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 09a18e5208..e172ddd657 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -439,7 +439,7 @@ module.exports = React.createClass({ return inviteList; }, - _lookupThreepid(medium, address) { + _lookupThreepid: function(medium, address) { // wait a bit to let the user finish typing return q.delay(500).then(() => { // If the query has changed, forget it From c42b705497d6ee0ea29da0540b4dcf917c210fea Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Jan 2017 10:54:07 +0000 Subject: [PATCH 05/49] Use a cancel function rather than checking queryList each time --- .../views/dialogs/ChatInviteDialog.js | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e172ddd657..fa7b30aa17 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -194,6 +194,7 @@ module.exports = React.createClass({ address: query, isKnown: false, }; + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); if (addrType == 'email') { this._lookupThreepid(addrType, query).done(); } @@ -216,6 +217,7 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }; }, @@ -233,6 +235,7 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }, _getDirectMessageRoom: function(addr) { @@ -436,31 +439,32 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); return inviteList; }, _lookupThreepid: function(medium, address) { + let cancelled = false; + // Note that we can't safely remove this after we're done + // because we don't know that it's the same one, so we just + // leave it: it's replacing the old one each time so it's + // not like they leak. + this._cancelThreepidLookup = function() { + cancelled = true; + } + // wait a bit to let the user finish typing return q.delay(500).then(() => { - // If the query has changed, forget it - if (this.state.queryList[0] && this.state.queryList[0].address !== address) { - return null; - } + if (cancelled) return null; return MatrixClientPeg.get().lookupThreePid(medium, address); }).then((res) => { if (res === null || !res.mxid) return null; - // If the query has changed now, drop the response - if (this.state.queryList[0] && this.state.queryList[0].address !== address) { - return null; - } + if (cancelled) return null; return MatrixClientPeg.get().getProfileInfo(res.mxid); }).then((res) => { if (res === null) return null; - // If the query has changed now, drop the response - if (this.state.queryList[0] && this.state.queryList[0].address !== address) { - return null; - } + if (cancelled) return null; this.setState({ queryList: [{ // an InviteAddressType From 2c7b3d9a02b35ad3a0ed6d40ac724552a7fdda78 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 26 Jan 2017 14:55:58 +0000 Subject: [PATCH 06/49] UnknownDeviceDialog: Reword the warning --- src/components/views/dialogs/UnknownDeviceDialog.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index de69fd1c9d..aa64e9ed99 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -82,10 +82,16 @@ export default React.createClass({ title='Room contains unknown devices' >
-

This room contains unknown devices which have not been +

This room contains devices which have not been verified.

- -

We strongly recommend you verify them before continuing.

+

+ This means there is no guarantee that the devices belong + to a valid user of the room. +

+ We recommend you go through the verification process + for each device before continuing, but you can resend + the message without verifying if you prefer. +

Unknown devices:

From 9c99dafba5916bba9e8020517f690c4a98c54325 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 26 Jan 2017 17:03:01 +0000 Subject: [PATCH 07/49] Guard onStatusBarVisible/Hidden with this.unmounted --- src/components/structures/RoomView.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 24c8ff53c0..eeb852a87f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1332,12 +1332,14 @@ module.exports = React.createClass({ }, onStatusBarVisible: function() { + if (this.unmounted) return; this.setState({ statusBarVisible: true, }); }, onStatusBarHidden: function() { + if (this.unmounted) return; this.setState({ statusBarVisible: false, }); From f462bd8f9948dc9ea22477587ff29f5d56df0242 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 26 Jan 2017 18:07:42 +0000 Subject: [PATCH 08/49] Expand status *area* unless status *bar* not visible (#655) --- src/components/structures/RoomView.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index eeb852a87f..38b3346e29 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1509,13 +1509,14 @@ module.exports = React.createClass({ }); var statusBar; + let isStatusAreaExpanded = true; if (ContentMessages.getCurrentUploads().length > 0) { var UploadBar = sdk.getComponent('structures.UploadBar'); statusBar = ; } else if (!this.state.searchResults) { var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); - + isStatusAreaExpanded = this.state.statusBarVisible; statusBar = Date: Fri, 27 Jan 2017 16:31:36 +0000 Subject: [PATCH 09/49] Redo team-based registration (#657) For compatibility with referral campaign flows, re-implement team registration such that the team is selected through providing an email with a known team domain. The support email is now only shown when an email that _looks_ like a UK/US university email address, but is not known. --- .../structures/login/Registration.js | 16 ++- .../views/login/RegistrationForm.js | 127 +++++++----------- 2 files changed, 60 insertions(+), 83 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 90140b3280..20c26c6b22 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -90,6 +90,7 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this._unmounted = false; this.dispatcherRef = dis.register(this.onAction); // attach this to the instance rather than this.state since it isn't UI this.registerLogic = new Signup.Register( @@ -107,6 +108,7 @@ module.exports = React.createClass({ componentWillUnmount: function() { dis.unregister(this.dispatcherRef); + this._unmounted = true; }, componentDidMount: function() { @@ -273,6 +275,14 @@ module.exports = React.createClass({ }); }, + onTeamSelected: function(team) { + if (!this._unmounted) { + this.setState({ + teamIcon: team ? team.icon : null, + }); + } + }, + _getRegisterContentJsx: function() { var currStep = this.registerLogic.getStep(); var registerStep; @@ -293,7 +303,9 @@ module.exports = React.createClass({ guestUsername={this.props.username} minPasswordLength={MIN_PASSWORD_LENGTH} onError={this.onFormValidationFailed} - onRegisterClick={this.onFormSubmit} /> + onRegisterClick={this.onFormSubmit} + onTeamSelected={this.onTeamSelected} + /> ); break; case "Register.STEP_m.login.email.identity": @@ -367,7 +379,7 @@ module.exports = React.createClass({ return (
- + {this._getRegisterContentJsx()}
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index f8a0863f70..1cb8253812 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -44,8 +44,8 @@ module.exports = React.createClass({ teams: React.PropTypes.arrayOf(React.PropTypes.shape({ // The displayed name of the team "name": React.PropTypes.string, - // The suffix with which every team email address ends - "emailSuffix": React.PropTypes.string, + // The domain of team email addresses + "domain": React.PropTypes.string, })).required, }), @@ -117,9 +117,6 @@ module.exports = React.createClass({ _doSubmit: function() { let email = this.refs.email.value.trim(); - if (this.state.selectedTeam) { - email += "@" + this.state.selectedTeam.emailSuffix; - } var promise = this.props.onRegisterClick({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), @@ -134,25 +131,6 @@ module.exports = React.createClass({ } }, - onSelectTeam: function(teamIndex) { - let team = this._getSelectedTeam(teamIndex); - if (team) { - this.refs.email.value = this.refs.email.value.split("@")[0]; - } - this.setState({ - selectedTeam: team, - showSupportEmail: teamIndex === "other", - }); - }, - - _getSelectedTeam: function(teamIndex) { - if (this.props.teamsConfig && - this.props.teamsConfig.teams[teamIndex]) { - return this.props.teamsConfig.teams[teamIndex]; - } - return null; - }, - /** * Returns true if all fields were valid last time * they were validated. @@ -167,20 +145,36 @@ module.exports = React.createClass({ return true; }, + _isUniEmail: function(email) { + return email.endsWith('.ac.uk') || email.endsWith('.edu'); + }, + validateField: function(field_id) { var pwd1 = this.refs.password.value.trim(); var pwd2 = this.refs.passwordConfirm.value.trim(); switch (field_id) { case FIELD_EMAIL: - let email = this.refs.email.value; - if (this.props.teamsConfig) { - let team = this.state.selectedTeam; - if (team) { - email = email + "@" + team.emailSuffix; - } + const email = this.refs.email.value; + if (this.props.teamsConfig && this._isUniEmail(email)) { + const matchingTeam = this.props.teamsConfig.teams.find( + (team) => { + return email.split('@').pop() === team.domain; + } + ) || null; + this.setState({ + selectedTeam: matchingTeam, + showSupportEmail: !matchingTeam, + }); + this.props.onTeamSelected(matchingTeam); + } else { + this.props.onTeamSelected(null); + this.setState({ + selectedTeam: null, + showSupportEmail: false, + }); } - let valid = email === '' || Email.looksValid(email); + const valid = email === '' || Email.looksValid(email); this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID"); break; case FIELD_USERNAME: @@ -260,61 +254,35 @@ module.exports = React.createClass({ return cls; }, - _renderEmailInputSuffix: function() { - let suffix = null; - if (!this.state.selectedTeam) { - return suffix; - } - let team = this.state.selectedTeam; - if (team) { - suffix = "@" + team.emailSuffix; - } - return suffix; - }, - render: function() { var self = this; - var emailSection, teamSection, teamAdditionSupport, registerButton; + var emailSection, belowEmailSection, registerButton; if (this.props.showEmail) { - let emailSuffix = this._renderEmailInputSuffix(); emailSection = ( -
- - {emailSuffix ? : null } -
+ ); if (this.props.teamsConfig) { - teamSection = ( - - ); if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { - teamAdditionSupport = ( - - If your team is not listed, email  + belowEmailSection = ( +

+ Sorry, but your university is not registered with us just yet.  + Email us on  {this.props.teamsConfig.supportEmail} - - +   + to get your university signed up. Or continue to register with Riot to enjoy our open source platform. +

+ ); + } else if (this.state.selectedTeam) { + belowEmailSection = ( +

+ You are registering with {this.state.selectedTeam.name} +

); } } @@ -333,11 +301,8 @@ module.exports = React.createClass({ return (
- {teamSection} - {teamAdditionSupport} -
{emailSection} -
+ {belowEmailSection} Date: Fri, 27 Jan 2017 21:57:34 +0000 Subject: [PATCH 10/49] Fix inviting import fail --- src/components/views/dialogs/ChatInviteDialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index fa7b30aa17..ca3b07aa00 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import classNames from 'classnames'; import sdk from '../../../index'; -import { getAddressType } from '../../../Invite'; +import { getAddressType, inviteMultipleToRoom } from '../../../Invite'; import createRoom from '../../../createRoom'; import MatrixClientPeg from '../../../MatrixClientPeg'; import DMRoomMap from '../../../utils/DMRoomMap'; @@ -273,7 +273,7 @@ module.exports = React.createClass({ if (this.props.roomId) { // Invite new user to a room var self = this; - Invite.inviteMultipleToRoom(this.props.roomId, addrTexts) + inviteMultipleToRoom(this.props.roomId, addrTexts) .then(function(addrs) { var room = MatrixClientPeg.get().getRoom(self.props.roomId); return self._showAnyInviteErrors(addrs, room); @@ -307,7 +307,7 @@ module.exports = React.createClass({ var room; createRoom().then(function(roomId) { room = MatrixClientPeg.get().getRoom(roomId); - return Invite.inviteMultipleToRoom(roomId, addrTexts); + return inviteMultipleToRoom(roomId, addrTexts); }) .then(function(addrs) { return self._showAnyInviteErrors(addrs, room); From 4e0889454a5d349a375606c2d98b29c558b1bea0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Jan 2017 15:50:31 +0000 Subject: [PATCH 11/49] GET /teams from RTS instead of config.json Now that the RTS contains config for teams, use GET /teams to get that information so that users will see be able to register as a team (but not yet auto-join rooms, be sent to welcome page or be tracked as a referral). --- src/RtsClient.js | 15 ++++++ src/components/structures/MatrixChat.js | 2 +- .../structures/login/Registration.js | 49 ++++++++++++------- 3 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 src/RtsClient.js diff --git a/src/RtsClient.js b/src/RtsClient.js new file mode 100644 index 0000000000..25ed71b72b --- /dev/null +++ b/src/RtsClient.js @@ -0,0 +1,15 @@ +const q = require('q'); +const request = q.nfbind(require('browser-request')); + +export default class RtsClient { + constructor(url) { + this._url = url; + } + + getTeamsConfig() { + return request({ + url: this._url + '/teams', + json: true, + }); + } +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index cb61041d48..989ae5aace 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1060,7 +1060,7 @@ module.exports = React.createClass({ defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} - teamsConfig={this.props.config.teamsConfig} + teamServerConfig={this.props.config.teamServerConfig} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 20c26c6b22..e34007ef42 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig"); var MatrixClientPeg = require("../../../MatrixClientPeg"); var RegistrationForm = require("../../views/login/RegistrationForm"); var CaptchaForm = require("../../views/login/CaptchaForm"); +var RtsClient = require("../../../RtsClient"); var MIN_PASSWORD_LENGTH = 6; @@ -49,20 +50,11 @@ module.exports = React.createClass({ email: React.PropTypes.string, username: React.PropTypes.string, guestAccessToken: React.PropTypes.string, - teamsConfig: React.PropTypes.shape({ + teamServerConfig: React.PropTypes.shape({ // Email address to request new teams - supportEmail: React.PropTypes.string, - teams: React.PropTypes.arrayOf(React.PropTypes.shape({ - // The displayed name of the team - "name": React.PropTypes.string, - // The suffix with which every team email address ends - "emailSuffix": React.PropTypes.string, - // The rooms to use during auto-join - "rooms": React.PropTypes.arrayOf(React.PropTypes.shape({ - "id": React.PropTypes.string, - "autoJoin": React.PropTypes.bool, - })), - })).required, + supportEmail: React.PropTypes.string.isRequired, + // URL of the riot-team-server to get team configurations and track referrals + teamServerURL: React.PropTypes.string.isRequired, }), defaultDeviceDisplayName: React.PropTypes.string, @@ -104,6 +96,27 @@ module.exports = React.createClass({ this.registerLogic.setIdSid(this.props.idSid); this.registerLogic.setGuestAccessToken(this.props.guestAccessToken); this.registerLogic.recheckState(); + + if (this.props.teamServerConfig && + this.props.teamServerConfig.teamServerURL && + !this._rtsClient) { + this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL); + + // GET team configurations including domains, names and icons + this._rtsClient.getTeamsConfig().done((args) => { + // args = [$request, $body] + const teamsConfig = { + teams: args[1], + supportEmail: this.props.teamServerConfig.supportEmail, + }; + console.log('Setting teams config to ', teamsConfig); + this.setState({ + teamsConfig: teamsConfig, + }); + }, (err) => { + console.error('Error retrieving config for teams', err); + }); + } }, componentWillUnmount: function() { @@ -187,10 +200,10 @@ module.exports = React.createClass({ }); // Auto-join rooms - if (self.props.teamsConfig && self.props.teamsConfig.teams) { - for (let i = 0; i < self.props.teamsConfig.teams.length; i++) { - let team = self.props.teamsConfig.teams[i]; - if (self.state.formVals.email.endsWith(team.emailSuffix)) { + if (self.state.teamsConfig && self.state.teamsConfig.teams) { + for (let i = 0; i < self.state.teamsConfig.teams.length; i++) { + let team = self.state.teamsConfig.teams[i]; + if (self.state.formVals.email.endsWith(team.domain)) { console.log("User successfully registered with team " + team.name); if (!team.rooms) { break; @@ -299,7 +312,7 @@ module.exports = React.createClass({ defaultUsername={this.state.formVals.username} defaultEmail={this.state.formVals.email} defaultPassword={this.state.formVals.password} - teamsConfig={this.props.teamsConfig} + teamsConfig={this.state.teamsConfig} guestUsername={this.props.username} minPasswordLength={MIN_PASSWORD_LENGTH} onError={this.onFormValidationFailed} From 318d8710977b99a4b61799625c2620ac3afa105e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Jan 2017 16:13:57 +0000 Subject: [PATCH 12/49] Formatting --- src/components/structures/login/Registration.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index e34007ef42..f1085f2e07 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -97,9 +97,11 @@ module.exports = React.createClass({ this.registerLogic.setGuestAccessToken(this.props.guestAccessToken); this.registerLogic.recheckState(); - if (this.props.teamServerConfig && - this.props.teamServerConfig.teamServerURL && - !this._rtsClient) { + if ( + this.props.teamServerConfig && + this.props.teamServerConfig.teamServerURL && + !this._rtsClient + ) { this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL); // GET team configurations including domains, names and icons From eb4d7f04e79fb4ecea156eb83edb94919b53519f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Jan 2017 16:23:52 +0000 Subject: [PATCH 13/49] Use busy spinner when requesting teams --- src/components/structures/login/Registration.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f1085f2e07..1456b666f4 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -67,6 +67,7 @@ module.exports = React.createClass({ getInitialState: function() { return { busy: false, + teamServerBusy: false, errorText: null, // We remember the values entered by the user because // the registration form will be unmounted during the @@ -104,8 +105,11 @@ module.exports = React.createClass({ ) { this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL); + this.setState({ + teamServerBusy: true, + }); // GET team configurations including domains, names and icons - this._rtsClient.getTeamsConfig().done((args) => { + this._rtsClient.getTeamsConfig().then((args) => { // args = [$request, $body] const teamsConfig = { teams: args[1], @@ -117,6 +121,10 @@ module.exports = React.createClass({ }); }, (err) => { console.error('Error retrieving config for teams', err); + }).finally(() => { + this.setState({ + teamServerBusy: false, + }); }); } }, @@ -299,6 +307,8 @@ module.exports = React.createClass({ }, _getRegisterContentJsx: function() { + var Spinner = sdk.getComponent("elements.Spinner"); + var currStep = this.registerLogic.getStep(); var registerStep; switch (currStep) { @@ -308,6 +318,10 @@ module.exports = React.createClass({ case "Register.STEP_m.login.dummy": // NB. Our 'username' prop is specifically for upgrading // a guest account + if (this.state.teamServerBusy) { + registerStep = ; + break; + } registerStep = ( Date: Mon, 30 Jan 2017 16:33:16 +0000 Subject: [PATCH 14/49] Use const, not var --- src/components/structures/login/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 1456b666f4..eed370a7ac 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -307,7 +307,7 @@ module.exports = React.createClass({ }, _getRegisterContentJsx: function() { - var Spinner = sdk.getComponent("elements.Spinner"); + const Spinner = sdk.getComponent("elements.Spinner"); var currStep = this.registerLogic.getStep(); var registerStep; From 1e279d23354387ec585fd9e4f9d34d0c3a98cb4b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Jan 2017 16:35:34 +0000 Subject: [PATCH 15/49] Finish with .done() --- src/components/structures/login/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index eed370a7ac..a4dcd63d9d 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -125,7 +125,7 @@ module.exports = React.createClass({ this.setState({ teamServerBusy: false, }); - }); + }).done(); } }, From 4e9229e936a1d0991f4e958eb800e1d297658c9f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 30 Jan 2017 16:37:16 +0000 Subject: [PATCH 16/49] Get rid of dupl. declaration --- src/components/structures/login/Registration.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index a4dcd63d9d..730f31c8ad 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -363,7 +363,6 @@ module.exports = React.createClass({ } var busySpinner; if (this.state.busy) { - var Spinner = sdk.getComponent("elements.Spinner"); busySpinner = ( ); From 878e5593ba7990b22cdc31f83e8a7e12e0cfbfd6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Jan 2017 11:13:05 +0000 Subject: [PATCH 17/49] Implement tracking of referrals (#659) * Implement tracking of referrals This also modifies (or fixes) auto-joining. --- src/RtsClient.js | 28 ++++++++++ src/components/structures/MatrixChat.js | 1 + .../structures/login/Registration.js | 54 ++++++++++++------- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/RtsClient.js b/src/RtsClient.js index 25ed71b72b..0067d0ae10 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -12,4 +12,32 @@ export default class RtsClient { json: true, }); } + + /** + * Track a referral with the Riot Team Server. This should be called once a referred + * user has been successfully registered. + * @param {string} referrer the user ID of one who referred the user to Riot. + * @param {string} user_id the user ID of the user being referred. + * @param {string} user_email the email address linked to `user_id`. + * @returns {Promise} a promise that resolves to [$response, $body], where $response + * is the response object created by the request lib and $body is the object parsed + * from the JSON response body. $body should be { team_token: 'sometoken' } upon + * success. + */ + trackReferral(referrer, user_id, user_email) { + return request({ + url: this._url + '/register', + json: true, + body: {referrer, user_id, user_email}, + method: 'POST', + }); + } + + getTeam(team_token) { + return request({ + url: this._url + '/teamConfiguration', + json: true, + qs: {team_token}, + }); + } } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 989ae5aace..6a84fb940f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1055,6 +1055,7 @@ module.exports = React.createClass({ sessionId={this.state.register_session_id} idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} + referrer={this.props.startingFragmentQueryParams.referrer} username={this.state.upgradeUsername} guestAccessToken={this.state.guestAccessToken} defaultHsUrl={this.getDefaultHsUrl()} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 730f31c8ad..82cdbef9ca 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -48,6 +48,7 @@ module.exports = React.createClass({ defaultIsUrl: React.PropTypes.string, brand: React.PropTypes.string, email: React.PropTypes.string, + referrer: React.PropTypes.string, username: React.PropTypes.string, guestAccessToken: React.PropTypes.string, teamServerConfig: React.PropTypes.shape({ @@ -56,6 +57,7 @@ module.exports = React.createClass({ // URL of the riot-team-server to get team configurations and track referrals teamServerURL: React.PropTypes.string.isRequired, }), + teamSelected: null, defaultDeviceDisplayName: React.PropTypes.string, @@ -209,24 +211,42 @@ module.exports = React.createClass({ accessToken: response.access_token }); - // Auto-join rooms - if (self.state.teamsConfig && self.state.teamsConfig.teams) { - for (let i = 0; i < self.state.teamsConfig.teams.length; i++) { - let team = self.state.teamsConfig.teams[i]; - if (self.state.formVals.email.endsWith(team.domain)) { - console.log("User successfully registered with team " + team.name); + if ( + self._rtsClient && + self.props.referrer && + self.state.teamSelected + ) { + // Track referral, get team_token in order to retrieve team config + self._rtsClient.trackReferral( + self.props.referrer, + response.user_id, + self.state.formVals.email + ).then((args) => { + const teamToken = args[1].team_token; + // Store for use /w welcome pages + window.localStorage.setItem('mx_team_token', teamToken); + + self._rtsClient.getTeam(teamToken).then((args) => { + const team = args[1]; + console.log( + `User successfully registered with team ${team.name}` + ); if (!team.rooms) { - break; + return; } + // Auto-join rooms team.rooms.forEach((room) => { - if (room.autoJoin) { - console.log("Auto-joining " + room.id); - MatrixClientPeg.get().joinRoom(room.id); + if (room.auto_join && room.room_id) { + console.log(`Auto-joining ${room.room_id}`); + MatrixClientPeg.get().joinRoom(room.room_id); } }); - break; - } - } + }, (err) => { + console.error('Error getting team config', err); + }); + }, (err) => { + console.error('Error tracking referral', err); + }); } if (self.props.brand) { @@ -298,11 +318,9 @@ module.exports = React.createClass({ }); }, - onTeamSelected: function(team) { + onTeamSelected: function(teamSelected) { if (!this._unmounted) { - this.setState({ - teamIcon: team ? team.icon : null, - }); + this.setState({ teamSelected }); } }, @@ -407,7 +425,7 @@ module.exports = React.createClass({ return (
- + {this._getRegisterContentJsx()}
From 62c8c202681afe869efa2f47d55c4e3118111e6c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Jan 2017 12:29:16 +0000 Subject: [PATCH 18/49] Megolm export: fix Android incompatibility I'd carefully added a workaround to maintain compatibility with the Android AES-CTR implementation... to the wrong thing. --- src/utils/MegolmExportEncryption.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index abae81e5ad..4745aad017 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -107,14 +107,14 @@ export function encryptMegolmKeyFile(data, password, options) { const salt = new Uint8Array(16); window.crypto.getRandomValues(salt); - // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary - // (which would mean we wouldn't be able to decrypt on Android). The loss - // of a single bit of salt is a price we have to pay. - salt[9] &= 0x7f; - const iv = new Uint8Array(16); window.crypto.getRandomValues(iv); + // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of iv is a price we have to pay. + iv[9] &= 0x7f; + return deriveKeys(salt, kdf_rounds, password).then((keys) => { const [aes_key, hmac_key] = keys; From c5f447260afa4f671afdd3f68ecbd521e3df4d0f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Jan 2017 12:30:30 +0000 Subject: [PATCH 19/49] Megolm import: Fix handling of short files Make sure we throw a sensible error when the body of the data is too short. --- src/utils/MegolmExportEncryption.js | 2 +- test/utils/MegolmExportEncryption-test.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 4745aad017..27c6ede937 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -50,7 +50,7 @@ export function decryptMegolmKeyFile(data, password) { } const ciphertextLength = body.length-(1+16+16+4+32); - if (body.length < 0) { + if (ciphertextLength < 0) { throw new Error('Invalid file: too short'); } diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index 28752ae529..0c49fd48d1 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -75,6 +75,16 @@ describe('MegolmExportEncryption', function() { .toThrow('Trailer line not found'); }); + it('should handle a too-short body', function() { + const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA----- +AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx +cissyYBxjsfsAn +-----END MEGOLM SESSION DATA----- +`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Invalid file: too short'); + }); + it('should decrypt a range of inputs', function(done) { function next(i) { if (i >= TEST_VECTORS.length) { From c2b0c603c03fb638c1192270dac9ddd76afecf66 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Jan 2017 13:17:01 +0000 Subject: [PATCH 20/49] Add referral section to user settings This allows those who have registered to referrer other students to Riot and have their referral counted for the campaign competition. --- src/components/structures/UserSettings.js | 23 +++++++++++++++++++ .../structures/login/Registration.js | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index cf4a63e2f7..ab2b73711a 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -444,6 +444,27 @@ module.exports = React.createClass({ ); }, + _renderReferral: function() { + const teamToken = window.localStorage.getItem('mx_team_token'); + if (!teamToken) { + return null; + } + if (typeof teamToken !== 'string') { + console.warn('Team token not a string'); + return null; + } + const href = window.location.origin + + `/#/register?referrer=${this._me}&team_token=${teamToken}`; + return ( +
+

Referral

+
+ Refer a friend to Riot: {href} +
+
+ ); + }, + _renderUserInterfaceSettings: function() { var client = MatrixClientPeg.get(); @@ -819,6 +840,8 @@ module.exports = React.createClass({ {accountJsx}
+ {this._renderReferral()} + {notification_area} {this._renderUserInterfaceSettings()} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 82cdbef9ca..d38441a76c 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ // URL of the riot-team-server to get team configurations and track referrals teamServerURL: React.PropTypes.string.isRequired, }), - teamSelected: null, + teamSelected: React.PropTypes.object, defaultDeviceDisplayName: React.PropTypes.string, From 4c4cc585c74730c7e591688d1e1785a85d030883 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Jan 2017 13:40:01 +0000 Subject: [PATCH 21/49] Throw errors on !==200 status codes from RTS --- src/RtsClient.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/RtsClient.js b/src/RtsClient.js index 0067d0ae10..fe1f0538ca 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -1,5 +1,25 @@ const q = require('q'); -const request = q.nfbind(require('browser-request')); +const request = (opts) => { + const expectingJSONOnSucess = opts.json; + if (opts.json) { + opts.json = false; + } + return q.nfbind(require('browser-request'))(opts).then((args) => { + const response = args[0]; + let body = args[1]; + + // Do not expect JSON on error status code, throw error instead + if (response.statusCode !== 200) { + throw new Error(body); + } + + if (expectingJSONOnSucess) { + body = JSON.parse(body); + } + + return [response, body]; + }); +}; export default class RtsClient { constructor(url) { From c261ca1f5ee8342a00ae5b4949cc03f22586de0b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Jan 2017 15:17:43 +0000 Subject: [PATCH 22/49] Allow base referral URL to be configurable --- src/components/structures/LoggedInView.js | 1 + src/components/structures/UserSettings.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c00bd2c6db..44beb787c8 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -171,6 +171,7 @@ export default React.createClass({ brand={this.props.config.brand} collapsedRhs={this.props.collapse_rhs} enableLabs={this.props.config.enableLabs} + referralBaseUrl={this.props.config.referralBaseUrl} />; if (!this.props.collapse_rhs) right_panel = ; break; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ab2b73711a..3d330e3649 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -92,6 +92,9 @@ module.exports = React.createClass({ // True to show the 'labs' section of experimental features enableLabs: React.PropTypes.bool, + // The base URL to use in the referral link. Defaults to window.location.origin. + referralBaseUrl: React.PropTypes.string, + // true if RightPanel is collapsed collapsedRhs: React.PropTypes.bool, }, @@ -453,7 +456,7 @@ module.exports = React.createClass({ console.warn('Team token not a string'); return null; } - const href = window.location.origin + + const href = (this.props.referralBaseUrl || window.location.origin) + `/#/register?referrer=${this._me}&team_token=${teamToken}`; return (
From 2f188770e56a557871d5f482b40ea31ef5031750 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 31 Jan 2017 15:59:38 +0000 Subject: [PATCH 23/49] Typo --- src/RtsClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RtsClient.js b/src/RtsClient.js index fe1f0538ca..0115edacb4 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -1,6 +1,6 @@ const q = require('q'); const request = (opts) => { - const expectingJSONOnSucess = opts.json; + const expectingJSONOnSuccess = opts.json; if (opts.json) { opts.json = false; } @@ -13,7 +13,7 @@ const request = (opts) => { throw new Error(body); } - if (expectingJSONOnSucess) { + if (expectingJSONOnSuccess) { body = JSON.parse(body); } From cd1cf09dc98af832bcbcdc40da84523ecd53faa7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Jan 2017 22:40:53 +0000 Subject: [PATCH 24/49] Make tests pass on Chrome again It seems that a number of the tests had started failing when run in Chrome. They were fine under PhantomJS, but the MegolmExport tests only work under Chrome, and I need them to work... Mostly the problems were timing-related, where assumptions made about how quickly the `then` handler on a promise would be called were no longer valid. Possibly Chrome 55 has made some changes to the relative priorities of setTimeout and sendMessage calls. One of the TimelinePanel tests was failing because it was expecting the contents of a div to take up more room than they actually were. It's possible this is something very environment-specific; hopefully the new value will work on a wider range of machines. Also some logging tweaks. --- karma.conf.js | 8 +++++ src/components/structures/ScrollPanel.js | 10 +++---- test/components/structures/RoomView-test.js | 13 +++----- .../components/structures/ScrollPanel-test.js | 30 +++++++++++++++---- .../structures/TimelinePanel-test.js | 8 +++-- test/test-utils.js | 8 +++++ 6 files changed, 56 insertions(+), 21 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 131a03ce79..6d3047bb3b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -165,6 +165,14 @@ module.exports = function (config) { }, devtool: 'inline-source-map', }, + + webpackMiddleware: { + stats: { + // don't fill the console up with a mahoosive list of modules + chunks: false, + }, + }, + browserNoActivityTimeout: 15000, }); }; diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 1391d2b740..c6bcdc45cd 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -570,7 +570,7 @@ module.exports = React.createClass({ var boundingRect = node.getBoundingClientRect(); var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; - debuglog("Scrolling to token '" + node.dataset.scrollToken + "'+" + + debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" + pixelOffset + " (delta: "+scrollDelta+")"); if(scrollDelta != 0) { @@ -582,7 +582,7 @@ module.exports = React.createClass({ _saveScrollState: function() { if (this.props.stickyBottom && this.isAtBottom()) { this.scrollState = { stuckAtBottom: true }; - debuglog("Saved scroll state", this.scrollState); + debuglog("ScrollPanel: Saved scroll state", this.scrollState); return; } @@ -601,12 +601,12 @@ module.exports = React.createClass({ trackedScrollToken: node.dataset.scrollToken, pixelOffset: wrapperRect.bottom - boundingRect.bottom, }; - debuglog("Saved scroll state", this.scrollState); + debuglog("ScrollPanel: saved scroll state", this.scrollState); return; } } - debuglog("Unable to save scroll state: found no children in the viewport"); + debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport"); }, _restoreSavedScrollState: function() { @@ -640,7 +640,7 @@ module.exports = React.createClass({ this._lastSetScroll = scrollNode.scrollTop; } - debuglog("Set scrollTop:", scrollNode.scrollTop, + debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop, "requested:", scrollTop, "_lastSetScroll:", this._lastSetScroll); }, diff --git a/test/components/structures/RoomView-test.js b/test/components/structures/RoomView-test.js index 58db29b1ee..8e7c8160b8 100644 --- a/test/components/structures/RoomView-test.js +++ b/test/components/structures/RoomView-test.js @@ -42,17 +42,12 @@ describe('RoomView', function () { it('resolves a room alias to a room id', function (done) { peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"})); - var onRoomIdResolved = sinon.spy(); + function onRoomIdResolved(room_id) { + expect(room_id).toEqual("!randomcharacters:aser.ver"); + done(); + } ReactDOM.render(, parentDiv); - - process.nextTick(function() { - // These expect()s don't read very well and don't give very good failure - // messages, but expect's toHaveBeenCalled only takes an expect spy object, - // not a sinon spy object. - expect(onRoomIdResolved.called).toExist(); - done(); - }); }); it('joins by alias if given an alias', function (done) { diff --git a/test/components/structures/ScrollPanel-test.js b/test/components/structures/ScrollPanel-test.js index 13721c9ecd..eacaeb5fb4 100644 --- a/test/components/structures/ScrollPanel-test.js +++ b/test/components/structures/ScrollPanel-test.js @@ -73,6 +73,7 @@ var Tester = React.createClass({ /* returns a promise which will resolve when the fill happens */ awaitFill: function(dir) { + console.log("ScrollPanel Tester: awaiting " + dir + " fill"); var defer = q.defer(); this._fillDefers[dir] = defer; return defer.promise; @@ -80,7 +81,7 @@ var Tester = React.createClass({ _onScroll: function(ev) { var st = ev.target.scrollTop; - console.log("Scroll event; scrollTop: " + st); + console.log("ScrollPanel Tester: scroll event; scrollTop: " + st); this.lastScrollEvent = st; var d = this._scrollDefer; @@ -159,10 +160,29 @@ describe('ScrollPanel', function() { scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass( tester, "gm-scroll-view"); - // wait for a browser tick to let the initial paginates complete - setTimeout(function() { - done(); - }, 0); + // we need to make sure we don't call done() until q has finished + // running the completion handlers from the fill requests. We can't + // just use .done(), because that will end up ahead of those handlers + // in the queue. We can't use window.setTimeout(0), because that also might + // run ahead of those handlers. + const sp = tester.scrollPanel(); + let retriesRemaining = 1; + const awaitReady = function() { + return q().then(() => { + if (sp._pendingFillRequests.b === false && + sp._pendingFillRequests.f === false + ) { + return; + } + + if (retriesRemaining == 0) { + throw new Error("fillRequests did not complete"); + } + retriesRemaining--; + return awaitReady(); + }); + }; + awaitReady().done(done); }); afterEach(function() { diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js index b2cdfbd590..be60691b5c 100644 --- a/test/components/structures/TimelinePanel-test.js +++ b/test/components/structures/TimelinePanel-test.js @@ -99,7 +99,11 @@ describe('TimelinePanel', function() { // the document so that we can interact with it properly. parentDiv = document.createElement('div'); parentDiv.style.width = '800px'; - parentDiv.style.height = '600px'; + + // This has to be slightly carefully chosen. We expect to have to do + // exactly one pagination to fill it. + parentDiv.style.height = '500px'; + parentDiv.style.overflow = 'hidden'; document.body.appendChild(parentDiv); }); @@ -235,7 +239,7 @@ describe('TimelinePanel', function() { expect(client.paginateEventTimeline.callCount).toEqual(0); done(); }, 0); - }, 0); + }, 10); }); it("should let you scroll down to the bottom after you've scrolled up", function(done) { diff --git a/test/test-utils.js b/test/test-utils.js index cdfae4421c..71d3bd92d6 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -14,7 +14,15 @@ var MatrixEvent = jssdk.MatrixEvent; */ export function beforeEach(context) { var desc = context.currentTest.fullTitle(); + console.log(); + + // this puts a mark in the chrome devtools timeline, which can help + // figure out what's been going on. + if (console.timeStamp) { + console.timeStamp(desc); + } + console.log(desc); console.log(new Array(1 + desc.length).join("=")); }; From cf049f2d75ace79eef292268e57fd9a1d1857907 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 1 Feb 2017 09:59:46 +0000 Subject: [PATCH 25/49] Exempt lines which look like pure JSX from the maxlen line --- .eslintrc.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index d5684e21a7..92280344fa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,7 +53,11 @@ module.exports = { * things that are errors in the js-sdk config that the current * code does not adhere to, turned down to warn */ - "max-len": ["warn"], + "max-len": ["warn", { + // apparently people believe the length limit shouldn't apply + // to JSX. + ignorePattern: '^\\s*<', + }], "valid-jsdoc": ["warn"], "new-cap": ["warn"], "key-spacing": ["warn"], From fa1981ce0915f707d928f4b26de17ef226efcccf Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 1 Feb 2017 10:39:52 +0000 Subject: [PATCH 26/49] Use whatwg-fetch instead of browser-request --- src/RtsClient.js | 83 +++++++++++-------- .../structures/login/Registration.js | 16 ++-- 2 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/RtsClient.js b/src/RtsClient.js index 0115edacb4..f1cbc8e6f1 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -1,36 +1,49 @@ -const q = require('q'); -const request = (opts) => { - const expectingJSONOnSuccess = opts.json; - if (opts.json) { - opts.json = false; +import 'whatwg-fetch'; + +function checkStatus(response) { + if (!response.ok) { + return response.text().then((text) => { + throw new Error(text); + }); } - return q.nfbind(require('browser-request'))(opts).then((args) => { - const response = args[0]; - let body = args[1]; + return response; +} - // Do not expect JSON on error status code, throw error instead - if (response.statusCode !== 200) { - throw new Error(body); +function parseJson(response) { + return response.json(); +} + +function encodeQueryParams(params) { + return '?' + Object.keys(params).map((k) => { + return k + '=' + encodeURIComponent(params[k]); + }).join('&'); +} + +const request = (url, opts) => { + if (opts && opts.qs) { + url += encodeQueryParams(opts.qs); + delete opts.qs; + } + if (opts && opts.body) { + if (!opts.headers) { + opts.headers = {}; } - - if (expectingJSONOnSuccess) { - body = JSON.parse(body); - } - - return [response, body]; - }); + opts.body = JSON.stringify(opts.body); + opts.headers['Content-Type'] = 'application/json'; + } + return fetch(url, opts) + .then(checkStatus) + .then(parseJson); }; + export default class RtsClient { constructor(url) { this._url = url; } getTeamsConfig() { - return request({ - url: this._url + '/teams', - json: true, - }); + return request(this._url + '/teams'); } /** @@ -39,25 +52,23 @@ export default class RtsClient { * @param {string} referrer the user ID of one who referred the user to Riot. * @param {string} user_id the user ID of the user being referred. * @param {string} user_email the email address linked to `user_id`. - * @returns {Promise} a promise that resolves to [$response, $body], where $response - * is the response object created by the request lib and $body is the object parsed - * from the JSON response body. $body should be { team_token: 'sometoken' } upon + * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon * success. */ trackReferral(referrer, user_id, user_email) { - return request({ - url: this._url + '/register', - json: true, - body: {referrer, user_id, user_email}, - method: 'POST', - }); + return request(this._url + '/register', + { + body: {referrer, user_id, user_email}, + method: 'POST', + } + ); } getTeam(team_token) { - return request({ - url: this._url + '/teamConfiguration', - json: true, - qs: {team_token}, - }); + return request(this._url + '/teamConfiguration', + { + qs: {team_token}, + } + ); } } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 82cdbef9ca..3fa2723ad3 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -111,23 +111,22 @@ module.exports = React.createClass({ teamServerBusy: true, }); // GET team configurations including domains, names and icons - this._rtsClient.getTeamsConfig().then((args) => { - // args = [$request, $body] + this._rtsClient.getTeamsConfig().then((data) => { const teamsConfig = { - teams: args[1], + teams: data, supportEmail: this.props.teamServerConfig.supportEmail, }; console.log('Setting teams config to ', teamsConfig); this.setState({ teamsConfig: teamsConfig, + teamServerBusy: false, }); }, (err) => { console.error('Error retrieving config for teams', err); - }).finally(() => { this.setState({ teamServerBusy: false, }); - }).done(); + }); } }, @@ -221,13 +220,12 @@ module.exports = React.createClass({ self.props.referrer, response.user_id, self.state.formVals.email - ).then((args) => { - const teamToken = args[1].team_token; + ).then((data) => { + const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); - self._rtsClient.getTeam(teamToken).then((args) => { - const team = args[1]; + self._rtsClient.getTeam(teamToken).then((team) => { console.log( `User successfully registered with team ${team.name}` ); From 028c40e293d7fe241f55b4bc1361ccd72f5e6929 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 1 Feb 2017 11:16:14 +0000 Subject: [PATCH 27/49] Linting --- src/RtsClient.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/RtsClient.js b/src/RtsClient.js index f1cbc8e6f1..ae62fb8b22 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -50,24 +50,30 @@ export default class RtsClient { * Track a referral with the Riot Team Server. This should be called once a referred * user has been successfully registered. * @param {string} referrer the user ID of one who referred the user to Riot. - * @param {string} user_id the user ID of the user being referred. - * @param {string} user_email the email address linked to `user_id`. + * @param {string} userId the user ID of the user being referred. + * @param {string} userEmail the email address linked to `userId`. * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon * success. */ - trackReferral(referrer, user_id, user_email) { + trackReferral(referrer, userId, userEmail) { return request(this._url + '/register', { - body: {referrer, user_id, user_email}, + body: { + referrer: referrer, + user_id: userId, + user_email: userEmail, + }, method: 'POST', } ); } - getTeam(team_token) { + getTeam(teamToken) { return request(this._url + '/teamConfiguration', { - qs: {team_token}, + qs: { + team_token: teamToken, + }, } ); } From 5e5b7f89f4ee41616e08fb7ae4abd50297ceb5fc Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 2 Feb 2017 00:25:49 +0000 Subject: [PATCH 28/49] support scrollable content for UnknownDeviceDialog --- src/Modal.js | 2 +- src/Resend.js | 2 +- src/components/views/dialogs/UnknownDeviceDialog.js | 7 ++++--- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Modal.js b/src/Modal.js index 89e8b1361c..b6cc46ed45 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -177,7 +177,7 @@ class ModalManager { var modal = this._modals[0]; var dialog = ( -
+
{modal.elem}
diff --git a/src/Resend.js b/src/Resend.js index e67c812b7c..59a8bd4192 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -33,7 +33,7 @@ module.exports = { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { devices: err.devices - }); + }, "mx_Dialog_unknownDevice"); } dis.dispatch({ diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index aa64e9ed99..11d0479f15 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import GeminiScrollbar from 'react-gemini-scrollbar'; function UserUnknownDeviceList(props) { const {userDevices} = props; @@ -81,12 +82,12 @@ export default React.createClass({ onFinished={this.props.onFinished} title='Room contains unknown devices' > -
+

This room contains devices which have not been verified.

This means there is no guarantee that the devices belong - to a valid user of the room. + to a rightful user of the room.

We recommend you go through the verification process for each device before continuing, but you can resend @@ -94,7 +95,7 @@ export default React.createClass({

Unknown devices:

-
+