From a967ddd1cb4ef0afd95189fa19cc1a6bc90e26e4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 13 Nov 2016 14:10:46 +0000 Subject: [PATCH 001/113] quick and dirty support for custom welcome pages, with an example for geektime techfest --- .DS_Store | Bin 6148 -> 6148 bytes src/PageTypes.js | 1 + src/components/structures/LoggedInView.js | 10 ++++++++++ src/components/structures/MatrixChat.js | 22 +++++++++++++++++++-- src/components/views/avatars/BaseAvatar.js | 2 +- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.DS_Store b/.DS_Store index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..d2a053831af5212a0eeadcbe98a90fb570842166 100644 GIT binary patch delta 128 zcmZoMXfc=|&e%3FQEZ}~q6iZM0|O%ig8)Nua#DVN4v@#dJTXy29VE`oki(G4kd&5! zB*#$9P{fc76Jp$$7|K4`K!jy8HwO;~W822W@640=MHEFr+7*B}0f>RdGaLY7hRu#5 G` if (!this.props.collapse_rhs) right_panel = break; + + case PageTypes.HomePage: + page_element = + if (!this.props.collapse_rhs) right_panel = + break; + case PageTypes.UserView: page_element = null; // deliberately null for now right_panel = diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 945088106b..9a3ff8f95c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -410,6 +410,10 @@ module.exports = React.createClass({ this._setPage(PageTypes.RoomDirectory); this.notifyNewScreen('directory'); break; + case 'view_home_page': + this._setPage(PageTypes.HomePage); + this.notifyNewScreen('home'); + break; case 'view_create_chat': this._createChat(); break; @@ -629,7 +633,12 @@ module.exports = React.createClass({ )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); } else { - self.setState({ready: true, page_type: PageTypes.RoomDirectory}); + if (self.props.config.home_page) { + self.setState({ready: true, page_type: PageTypes.HomePage}); + } + else { + self.setState({ready: true, page_type: PageTypes.RoomDirectory}); + } } } else { self.setState({ready: true, page_type: PageTypes.RoomView}); @@ -649,7 +658,12 @@ module.exports = React.createClass({ } else { // There is no information on presentedId // so point user to fallback like /directory - self.notifyNewScreen('directory'); + if (self.props.config.home_page) { + self.notifyNewScreen('home'); + } + else { + self.notifyNewScreen('directory'); + } } dis.dispatch({action: 'focus_composer'}); @@ -703,6 +717,10 @@ module.exports = React.createClass({ dis.dispatch({ action: 'view_user_settings', }); + } else if (screen == 'home') { + dis.dispatch({ + action: 'view_home_page', + }); } else if (screen == 'directory') { dis.dispatch({ action: 'view_room_directory', diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47f0a76891..4025859478 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -138,7 +138,7 @@ module.exports = React.createClass({ const { name, idName, title, url, urls, width, height, resizeMethod, - defaultToInitialLetter, + defaultToInitialLetter, viewUserOnClick, ...otherProps } = this.props; From 69f6393ed9fbb19c7460ac9fac3a61af851e3794 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 13 Nov 2016 14:13:21 +0000 Subject: [PATCH 002/113] try to make joining rooms more obvious --- src/components/views/rooms/RoomPreviewBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index baeae4807d..7c03ca89a4 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -146,7 +146,7 @@ module.exports = React.createClass({
You are trying to access { name }.
- Would you like to join in order to participate in the discussion? + Click here to join the discussion!
); From 8a5678efdd74f1997d65533352ec800d0482cc67 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 19 Nov 2016 01:20:09 +0200 Subject: [PATCH 003/113] boldify the preview bar click --- src/components/views/rooms/RoomPreviewBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 7c03ca89a4..d41fed04b4 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -146,7 +146,7 @@ module.exports = React.createClass({
You are trying to access { name }.
- Click here to join the discussion! + Click here to join the discussion!
); From 2e15e8f9b458c01612d60eeaa73fdd0a5308b9b5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 05:13:36 +0000 Subject: [PATCH 004/113] very barebones support for warning users when rooms contain unknown devices --- src/Resend.js | 13 +++- src/component-index.js | 2 + .../views/dialogs/UnknownDeviceDialog.js | 73 +++++++++++++++++++ .../views/rooms/MessageComposerInput.js | 10 ++- .../views/rooms/MessageComposerInputOld.js | 11 ++- .../views/rooms/SimpleRoomHeader.js | 2 +- 6 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 src/components/views/dialogs/UnknownDeviceDialog.js diff --git a/src/Resend.js b/src/Resend.js index ecf504e780..e67c812b7c 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -16,17 +16,26 @@ limitations under the License. var MatrixClientPeg = require('./MatrixClientPeg'); var dis = require('./dispatcher'); +var sdk = require('./index'); +var Modal = require('./Modal'); module.exports = { resend: function(event) { MatrixClientPeg.get().resendEvent( event, MatrixClientPeg.get().getRoom(event.getRoomId()) - ).done(function() { + ).done(function(res) { dis.dispatch({ action: 'message_sent', event: event }); - }, function() { + }, function(err) { + if (err.name === "UnknownDeviceError") { + var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + Modal.createDialog(UnknownDeviceDialog, { + devices: err.devices + }); + } + dis.dispatch({ action: 'message_send_failed', event: event diff --git a/src/component-index.js b/src/component-index.js index e83de8739d..dcf96a6e56 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -89,6 +89,8 @@ import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDi views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog); +import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog'; +views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog); import views$elements$AddressSelector from './components/views/elements/AddressSelector'; views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector); import views$elements$AddressTile from './components/views/elements/AddressTile'; diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js new file mode 100644 index 0000000000..aad310c855 --- /dev/null +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -0,0 +1,73 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 React = require("react"); +var sdk = require('../../../index'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); + +module.exports = React.createClass({ + displayName: 'UnknownEventDialog', + + propTypes: { + devices: React.PropTypes.object.isRequired, + onFinished: React.PropTypes.func.isRequired, + }, + + onKeyDown: function(e) { + if (e.keyCode === 27) { // escape + e.stopPropagation(); + e.preventDefault(); + this.props.onFinished(false); + } + }, + + render: function() { + return ( +
+
+ Room contains unknown devices +
+
+

This room contains unknown devices which have not been verified.

+

We strongly recommend you verify them before continuing.

+

Unknown devices: +

    { + Object.keys(this.props.devices).map(userId=>{ + return
  • +

    { userId }:

    +
      + { + Object.keys(this.props.devices[userId]).map(deviceId=>{ + return
    • + { deviceId } ( { this.props.devices[userId][deviceId].getDisplayName() } ) +
    • + }) + } +
    +
  • + }) + }
+

+
+
+ +
+
+ ); + } +}); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index bf936a2c13..4a6e36b854 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -553,11 +553,17 @@ export default class MessageComposerInput extends React.Component { sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText); } - sendMessagePromise.then(() => { + sendMessagePromise.then((res) => { dis.dispatch({ action: 'message_sent', }); - }, () => { + }, (err) => { + if (err.name === "UnknownDeviceError") { + var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + Modal.createDialog(UnknownDeviceDialog, { + devices: err.devices + }); + } dis.dispatch({ action: 'message_send_failed', }); diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index c5d5f083c1..a5b1f6b786 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -337,11 +337,18 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); } - sendMessagePromise.done(function() { + sendMessagePromise.done(function(res) { dis.dispatch({ action: 'message_sent' }); - }, function() { + }, function(err) { + if (err.name === "UnknownDeviceError") { + var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + Modal.createDialog(UnknownDeviceDialog, { + devices: err.devices + }); + } + dis.dispatch({ action: 'message_send_failed' }); diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 4c63be5b99..c6d09f49ee 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -44,7 +44,7 @@ module.exports = React.createClass({ var cancelButton; if (this.props.onCancelClick) { - cancelButton =
Cancel
; + cancelButton =
Cancel
; } var showRhsButton; From 3071fc0ddcdf6065cf4e64b647c21ade53722173 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 17:39:31 +0000 Subject: [PATCH 005/113] UI for blacklisting unverified devices per-room & globally (written blind; untested as yet) --- src/UserSettingsStore.js | 16 +++++++ src/components/structures/UserSettings.js | 36 ++++++++++++++++ .../views/dialogs/UnknownDeviceDialog.js | 2 + src/components/views/rooms/RoomSettings.js | 43 +++++++++++++++++-- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index e5dba62ee7..f2f99603d6 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -149,6 +149,22 @@ module.exports = { return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); }, + getLocalSettings: function() { + return localStorage.getItem('mx_local_settings'); + }, + + getLocalSetting: function(type, defaultValue = null) { + var settings = this.getLocalSettings(); + return settings.hasOwnProperty(type) ? settings[type] : null; + }, + + setLocalSetting: function(type, value) { + var settings = this.getLocalSettings(); + settings[type] = value; + // FIXME: handle errors + localStorage.setItem('mx_local_settings', settings); + }, + isFeatureEnabled: function(feature: string): boolean { // Disable labs for guests. if (MatrixClientPeg.get().isGuest()) return false; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 4a1332be8c..a262431f2b 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -57,6 +57,18 @@ const SETTINGS_LABELS = [ */ ]; +const CRYPTO_SETTINGS_LABELS = [ + { + id: 'blacklistUnverifiedDevices', + label: 'Never send encrypted messages to unverified devices', + }, + // XXX: this is here for documentation; the actual setting is managed via RoomSettings + // { + // id: 'blacklistUnverifiedDevicesPerRoom' + // label: 'Never send encrypted messages to unverified devices in this room', + // } +]; + // Enumerate the available themes, with a nice human text label. // 'id' gives the key name in the im.vector.web.settings account data event // 'value' is the value for that key in the event @@ -146,6 +158,8 @@ module.exports = React.createClass({ syncedSettings.theme = 'light'; } this._syncedSettings = syncedSettings; + + this._localSettings = UserSettingsStore.getLocalSettings(); }, componentDidMount: function() { @@ -471,10 +485,32 @@ module.exports = React.createClass({
  • {identityKey}
  • + { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } ); }, + _renderLocalSetting: function(setting) { + const client = MatrixClientPeg.get(); + return
    + { + UserSettingsStore.setLocalSetting(setting.id, e.target.checked) + if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly + client.setGlobalBlacklistUnverifiedDevices(e.target.checked); + } + } + } + /> + +
    ; + }, + _renderDevicesPanel: function() { var DevicesPanel = sdk.getComponent('settings.DevicesPanel'); return ( diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index aad310c855..8e04414b9c 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -69,5 +69,7 @@ module.exports = React.createClass({ ); + // XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point? + // It feels like confused users will likely turn it on and then disappear in a cloud of UISIs... } }); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index e14a929ebe..7283201858 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -228,11 +228,13 @@ module.exports = React.createClass({ } // encryption - p = this.saveEncryption(); + p = this.saveEnableEncryption(); if (!q.isFulfilled(p)) { promises.push(p); } + this.saveBlacklistUnverifiedDevicesPerRoom(); + console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises)); return promises; }, @@ -252,11 +254,11 @@ module.exports = React.createClass({ return this.refs.url_preview_settings.saveSettings(); }, - saveEncryption: function() { + saveEnableEncryption: function() { if (!this.refs.encrypt) { return q(); } var encrypt = this.refs.encrypt.checked; - if (!encrypt) { return q(); } + if (encrypt) { return q(); } var roomId = this.props.room.roomId; return MatrixClientPeg.get().sendStateEvent( @@ -265,6 +267,29 @@ module.exports = React.createClass({ ); }, + saveBlacklistUnverifiedDevicesPerRoom: function() { + if (!this.refs.blacklistUnverified) return; + if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) { + this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked); + } + }, + + _isRoomBlacklistUnverified: function() { + var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; + if (blacklistUnverifiedDevicesPerRoom) { + return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId]; + } + return false; + }, + + _setRoomBlacklistUnverified: function(value) { + var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; + blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value; + UserSettingsStore.setLocalSettings('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom); + + this.props.room.setBlacklistUnverifiedDevices(value); + }, + _hasDiff: function(strA, strB) { // treat undefined as an empty string because other components may blindly // call setName("") when there has been no diff made to the name! @@ -477,6 +502,16 @@ module.exports = React.createClass({ var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); + var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices; + var isRoomBlacklistUnverified = this._isRoomBlacklistUnverified(); + + var settings = + ; if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) { @@ -486,6 +521,7 @@ module.exports = React.createClass({ Enable encryption (warning: cannot be disabled again!) + { settings } ); } else { @@ -497,6 +533,7 @@ module.exports = React.createClass({ } Encryption is { isEncrypted ? "" : "not " } enabled in this room. + { settings } ); } }, From c618880af96c560e0434be0d842e2ef47d306887 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 17:43:46 +0000 Subject: [PATCH 006/113] oops --- src/components/views/rooms/RoomSettings.js | 32 ++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 7283201858..a5782dad7b 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -516,24 +516,28 @@ module.exports = React.createClass({ if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) { return ( - - { settings } +
    + + { settings } +
    ); } else { return ( - - { settings } +
    + + { settings } +
    ); } }, From 071e364be29cfafab94bf0442370fa0a955f3197 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 17:56:48 +0000 Subject: [PATCH 007/113] improve the verify warning if blacklisting is unabled --- src/Resend.js | 3 ++- src/components/views/dialogs/UnknownDeviceDialog.js | 12 +++++++++++- src/components/views/rooms/MessageComposerInput.js | 3 ++- .../views/rooms/MessageComposerInputOld.js | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Resend.js b/src/Resend.js index e67c812b7c..21da1c173b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -32,7 +32,8 @@ module.exports = { if (err.name === "UnknownDeviceError") { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices + devices: err.devices, + room: MatrixClientPeg.get().getRoom(event.getRoomId()), }); } diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 8e04414b9c..bd23427d2b 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -22,6 +22,7 @@ module.exports = React.createClass({ displayName: 'UnknownEventDialog', propTypes: { + room: React.PropTypes.object.isRequired, devices: React.PropTypes.object.isRequired, onFinished: React.PropTypes.func.isRequired, }, @@ -35,6 +36,15 @@ module.exports = React.createClass({ }, render: function() { + var client = MatrixClientPeg.get(); + var warning; + if (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()) { + warning =

    You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

    ; + } + else { + warning =

    We strongly recommend you verify them before continuing.

    ; + } + return (
    @@ -42,7 +52,7 @@ module.exports = React.createClass({

    This room contains unknown devices which have not been verified.

    -

    We strongly recommend you verify them before continuing.

    + { warning }

    Unknown devices:

      { Object.keys(this.props.devices).map(userId=>{ diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4a6e36b854..54e4894adf 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -561,7 +561,8 @@ export default class MessageComposerInput extends React.Component { if (err.name === "UnknownDeviceError") { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices + devices: err.devices, + room: this.props.room, }); } dis.dispatch({ diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index a5b1f6b786..7fd3e0b427 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -345,7 +345,8 @@ module.exports = React.createClass({ if (err.name === "UnknownDeviceError") { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices + devices: err.devices, + room: this.props.room, }); } From 532f4e59c904afe4967ec80dbff3d57b6be0e88a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 19:06:15 +0100 Subject: [PATCH 008/113] literally blindly add verification buttons --- src/components/views/dialogs/UnknownDeviceDialog.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index bd23427d2b..a2e7bd13fb 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -37,8 +37,9 @@ module.exports = React.createClass({ render: function() { var client = MatrixClientPeg.get(); + var blacklistUnverified = (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()); var warning; - if (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()) { + if (blacklistUnverified) { warning =

      You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

      ; } else { @@ -61,8 +62,11 @@ module.exports = React.createClass({
        { Object.keys(this.props.devices[userId]).map(deviceId=>{ + var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); + var device = this.props.devices[userId][deviceId]; + var buttons = return
      • - { deviceId } ( { this.props.devices[userId][deviceId].getDisplayName() } ) + { deviceId } ( { device.getDisplayName() } ) { buttons }
      • }) } From 39c122fe4f09d048b5237dd539c66b09855cad76 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 22:27:55 +0100 Subject: [PATCH 009/113] fix local storage idiocy --- src/UserSettingsStore.js | 5 +++-- src/components/structures/UserSettings.js | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index f2f99603d6..d7d3e7bc7a 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -150,7 +150,8 @@ module.exports = { }, getLocalSettings: function() { - return localStorage.getItem('mx_local_settings'); + var localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; + return JSON.parse(localSettingsString); }, getLocalSetting: function(type, defaultValue = null) { @@ -162,7 +163,7 @@ module.exports = { var settings = this.getLocalSettings(); settings[type] = value; // FIXME: handle errors - localStorage.setItem('mx_local_settings', settings); + localStorage.setItem('mx_local_settings', JSON.stringify(settings)); }, isFeatureEnabled: function(feature: string): boolean { diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a262431f2b..1dd30b679c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -485,7 +485,9 @@ module.exports = React.createClass({
      • {identityKey}

    - { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } +
    + { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } +
    ); }, From d9c0513ee28f007353b934562733b17a269e527f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 21 Jan 2017 22:49:29 +0100 Subject: [PATCH 010/113] make it work --- src/components/views/rooms/RoomSettings.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index a5782dad7b..548809e9ad 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -24,6 +24,8 @@ var ObjectUtils = require("../../../ObjectUtils"); var dis = require("../../../dispatcher"); var ScalarAuthClient = require("../../../ScalarAuthClient"); var ScalarMessaging = require('../../../ScalarMessaging'); +var UserSettingsStore = require('../../../UserSettingsStore'); + // parse a string as an integer; if the input is undefined, or cannot be parsed // as an integer, return a default. @@ -283,9 +285,9 @@ module.exports = React.createClass({ }, _setRoomBlacklistUnverified: function(value) { - var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; + var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {}; blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value; - UserSettingsStore.setLocalSettings('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom); + UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom); this.props.room.setBlacklistUnverifiedDevices(value); }, @@ -508,8 +510,8 @@ module.exports = React.createClass({ var settings = ; From 7bc3fc86961bf11e1840ff5f9978c17dc7eb03d7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 22 Jan 2017 01:28:43 +0100 Subject: [PATCH 011/113] make UnknownDeviceDialog work again, other than the mess of vector-im/vector-web#3020 --- src/components/structures/UserSettings.js | 2 +- .../views/dialogs/UnknownDeviceDialog.js | 49 ++++++++++--------- .../views/elements/DeviceVerifyButtons.js | 4 +- .../views/rooms/MessageComposerInput.js | 3 +- .../views/rooms/MessageComposerInputOld.js | 3 +- src/components/views/rooms/RoomSettings.js | 2 +- 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1dd30b679c..00219e3bb2 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -60,7 +60,7 @@ const SETTINGS_LABELS = [ const CRYPTO_SETTINGS_LABELS = [ { id: 'blacklistUnverifiedDevices', - label: 'Never send encrypted messages to unverified devices', + label: 'Never send encrypted messages to unverified devices from this device', }, // XXX: this is here for documentation; the actual setting is managed via RoomSettings // { diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index a2e7bd13fb..656f831ba1 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -36,14 +36,15 @@ module.exports = React.createClass({ }, render: function() { + var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); var client = MatrixClientPeg.get(); - var blacklistUnverified = (client.getGlobalBlacklistUnverifiedDevices() || room.getBlacklistUnverifiedDevices()); + var blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() || this.props.room.getBlacklistUnverifiedDevices(); var warning; if (blacklistUnverified) { - warning =

    You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

    ; + warning =

    You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

    } else { - warning =

    We strongly recommend you verify them before continuing.

    ; + warning =

    We strongly recommend you verify them before continuing.

    } return ( @@ -54,27 +55,27 @@ module.exports = React.createClass({

    This room contains unknown devices which have not been verified.

    { warning } -

    Unknown devices: -

      { - Object.keys(this.props.devices).map(userId=>{ - return
    • -

      { userId }:

      -
        - { - Object.keys(this.props.devices[userId]).map(deviceId=>{ - var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); - var device = this.props.devices[userId][deviceId]; - var buttons = - return
      • - { deviceId } ( { device.getDisplayName() } ) { buttons } -
      • - }) - } -
      -
    • - }) - }
    -

    + Unknown devices: +
      { + Object.keys(this.props.devices).map(userId=>{ + return
    • +

      { userId }:

      +
        + { + Object.keys(this.props.devices[userId]).map(deviceId=>{ + var device = this.props.devices[userId][deviceId]; + var buttons = + return
      • + { buttons } + { deviceId }
        + { device.getDisplayName() } +
      • + }) + } +
      +
    • + }) + }
    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 5da6ca8fc10dcb5dec74e7943060e1ff051e500d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 25 Jan 2017 18:25:40 +0000 Subject: [PATCH 015/113] Refactor UnknownDeviceDialog hopefully make this a bit more readable, and use our new BaseDialog. --- .../views/dialogs/UnknownDeviceDialog.js | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index aad310c855..bb50da71a5 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -14,60 +14,77 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require("react"); -var sdk = require('../../../index'); -var MatrixClientPeg = require("../../../MatrixClientPeg"); +import React from 'react'; +import sdk from '../../../index'; -module.exports = React.createClass({ +function UserUnknownDeviceList(props) { + const {userDevices} = props; + + const deviceListEntries = Object.keys(userDevices).map((deviceId) => +
  • + { deviceId } ( { userDevices[deviceId].getDisplayName() } ) +
  • , + ); + + return
      {deviceListEntries}
    ; +} + +UserUnknownDeviceList.propTypes = { + // map from deviceid -> deviceinfo + userDevices: React.PropTypes.object.isRequired, +}; + + +function UnknownDeviceList(props) { + const {devices} = props; + + const userListEntries = Object.keys(devices).map((userId) => +
  • +

    { userId }:

    + +
  • , + ); + + return
      {userListEntries}
    ; +} + +UnknownDeviceList.propTypes = { + // map from userid -> deviceid -> deviceinfo + devices: React.PropTypes.object.isRequired, +}; + + +export default React.createClass({ displayName: 'UnknownEventDialog', propTypes: { + // map from userid -> deviceid -> deviceinfo devices: React.PropTypes.object.isRequired, onFinished: React.PropTypes.func.isRequired, }, - onKeyDown: function(e) { - if (e.keyCode === 27) { // escape - e.stopPropagation(); - e.preventDefault(); - this.props.onFinished(false); - } - }, - render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( -
    -
    - Room contains unknown devices -
    +
    -

    This room contains unknown devices which have not been verified.

    +

    This room contains unknown devices which have not been + verified.

    +

    We strongly recommend you verify them before continuing.

    -

    Unknown devices: -

      { - Object.keys(this.props.devices).map(userId=>{ - return
    • -

      { userId }:

      -
        - { - Object.keys(this.props.devices[userId]).map(deviceId=>{ - return
      • - { deviceId } ( { this.props.devices[userId][deviceId].getDisplayName() } ) -
      • - }) - } -
      -
    • - }) - }
    -

    +

    Unknown devices:

    +
    -
    -
    + ); - } + }, }); From 70190be65cfab451b998bc4b7f7730f0c15fd7f7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 25 Jan 2017 19:06:15 +0000 Subject: [PATCH 016/113] Factor out common onSendMessageFailed --- .../views/rooms/MessageComposerInput.js | 15 +++-------- .../views/rooms/MessageComposerInputOld.js | 26 ++++++++++--------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4a6e36b854..80e41555a2 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -40,6 +40,7 @@ import * as HtmlUtils from '../../../HtmlUtils'; import Autocomplete from './Autocomplete'; import {Completion} from "../../../autocomplete/Autocompleter"; import Markdown from '../../../Markdown'; +import {onSendMessageFailed} from './MessageComposerInputOld'; const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; @@ -553,21 +554,11 @@ export default class MessageComposerInput extends React.Component { sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText); } - sendMessagePromise.then((res) => { + sendMessagePromise.done((res) => { dis.dispatch({ action: 'message_sent', }); - }, (err) => { - if (err.name === "UnknownDeviceError") { - var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); - Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices - }); - } - dis.dispatch({ - action: 'message_send_failed', - }); - }); + }, onSendMessageFailed); this.setState({ editorState: this.createEditorState(), diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index a5b1f6b786..d81f89a3c3 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -29,10 +29,22 @@ var TYPING_USER_TIMEOUT = 10000; var TYPING_SERVER_TIMEOUT = 30000; var MARKDOWN_ENABLED = true; +export function onSendMessageFailed(err) { + if (err.name === "UnknownDeviceError") { + const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + Modal.createDialog(UnknownDeviceDialog, { + devices: err.devices, + }); + } + dis.dispatch({ + action: 'message_send_failed', + }); +} + /* * The textInput part of the MessageComposer */ -module.exports = React.createClass({ +export default React.createClass({ displayName: 'MessageComposerInput', statics: { @@ -341,18 +353,8 @@ module.exports = React.createClass({ dis.dispatch({ action: 'message_sent' }); - }, function(err) { - if (err.name === "UnknownDeviceError") { - var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); - Modal.createDialog(UnknownDeviceDialog, { - devices: err.devices - }); - } + }, onSendMessageFailed); - dis.dispatch({ - action: 'message_send_failed' - }); - }); this.refs.textarea.value = ''; this.resizeInput(); ev.preventDefault(); From ebf6ba89940362ae565f49507c178ebd62cd4f4b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 25 Jan 2017 23:52:48 +0100 Subject: [PATCH 017/113] explicitly set device known-ness --- src/components/views/dialogs/UnknownDeviceDialog.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index aad310c855..b771534852 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -26,6 +26,16 @@ module.exports = React.createClass({ onFinished: React.PropTypes.func.isRequired, }, + componentDidMount: function() { + // Given we've now shown the user the unknown device, it is no longer + // unknown to them. Therefore mark it as 'known'. + Object.keys(this.props.devices).forEach(userId=>{ + Object.keys(this.props.devices[userId]).map(deviceId=>{ + MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true); + }) + }); + }, + onKeyDown: function(e) { if (e.keyCode === 27) { // escape e.stopPropagation(); From bf66f77acb79d5bad1a354f43e290930e2947731 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Jan 2017 10:08:44 +0000 Subject: [PATCH 018/113] 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 019/113] 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 020/113] 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 0a3d9fc17fc5f31ab3d5289cd23376e13d8b0c5f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 26 Jan 2017 14:12:00 +0000 Subject: [PATCH 021/113] Fix lint --- .../views/dialogs/UnknownDeviceDialog.js | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index e99c423206..178dd55657 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -45,7 +45,8 @@ function UserUnknownDeviceList(props) { const {userId, userDevices} = props; const deviceListEntries = Object.keys(userDevices).map((deviceId) => - , + , ); return ( @@ -105,14 +106,19 @@ export default React.createClass({ render: function() { const client = MatrixClientPeg.get(); - const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() || this.props.room.getBlacklistUnverifiedDevices(); + const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() || + this.props.room.getBlacklistUnverifiedDevices(); let warning; if (blacklistUnverified) { - warning =

    You are currently blacklisting unverified devices; to send messages to these devices you must verify them.

    - } - else { - warning =

    We strongly recommend you verify them before continuing.

    + warning = ( +

    + You are currently blacklisting unverified devices; to send + messages to these devices you must verify them. +

    + ); + } else { + warning =

    We strongly recommend you verify them before continuing.

    ; } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); @@ -122,7 +128,10 @@ export default React.createClass({ title='Room contains unknown devices' >
    -

    This room contains unknown devices which have not been verified.

    +

    + This room contains unknown devices which have not been + verified. +

    { warning } Unknown devices: 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 022/113] 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 023/113] 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 024/113] 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 025/113] 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 026/113] 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 027/113] 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 028/113] 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 029/113] 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 030/113] 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 031/113] 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 032/113] 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 033/113] 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 034/113] 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 035/113] 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 036/113] 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 037/113] 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 038/113] 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 039/113] 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 040/113] 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 041/113] 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 042/113] 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 043/113] 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 044/113] 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:

    -
    +
    diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index b91e5c4391..63821b7d96 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -30,11 +30,19 @@ var TYPING_SERVER_TIMEOUT = 30000; var MARKDOWN_ENABLED = true; export function onSendMessageFailed(err, room) { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); if (err.name === "UnknownDeviceError") { const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); Modal.createDialog(UnknownDeviceDialog, { devices: err.devices, room: room, + onFinished: (r) => { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('UnknownDeviceDialog closed with '+r); + }, }, "mx_Dialog_unknownDevice"); } dis.dispatch({ From af19ea8bb789bd4877f0e40125b577d7cb6ac747 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 12:01:44 +0000 Subject: [PATCH 096/113] Document login API --- src/RtsClient.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/RtsClient.js b/src/RtsClient.js index b8b51791ba..5cf2e811ad 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -78,6 +78,13 @@ export default class RtsClient { ); } + /** + * Signal to the RTS that a login has occurred and that a user requires their team's + * token. + * @param {string} userId the user ID of the user who is a member of a team. + * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon + * success. + */ login(userId) { return request(this._url + '/login', { From c93b6c3c3461e8ac2bda3bf27adef9196cb8034f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 13:15:40 +0000 Subject: [PATCH 097/113] Style, fixes --- src/components/structures/MatrixChat.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b986cd88f0..eea237c014 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -194,7 +194,7 @@ module.exports = React.createClass({ // Use the locally-stored team token first, then as a fall-back, check to see if // a referral link was used, which will contain a query parameter `team_token`. this._teamToken = window.localStorage.getItem('mx_team_token') || - startingFragmentQueryParams.team_token; + this.props.startingFragmentQueryParams.team_token; }, componentDidMount: function() { @@ -699,10 +699,9 @@ module.exports = React.createClass({ )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); } else { - if (this._teamToken) { + if (self._teamToken) { self.setState({ready: true, page_type: PageTypes.HomePage}); - } - else { + } else { self.setState({ready: true, page_type: PageTypes.RoomDirectory}); } } @@ -726,8 +725,7 @@ module.exports = React.createClass({ // so point user to fallback like /directory if (self.props.config.home_page) { self.notifyNewScreen('home'); - } - else { + } else { self.notifyNewScreen('directory'); } } From d5f6ecdc496fda661b9afb34e86d2a41d3e0e26f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 13:23:58 +0000 Subject: [PATCH 098/113] Use teamToken, not config when doing screen fallback --- 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 eea237c014..db0b031453 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -723,7 +723,7 @@ module.exports = React.createClass({ } else { // There is no information on presentedId // so point user to fallback like /directory - if (self.props.config.home_page) { + if (self._teamToken) { self.notifyNewScreen('home'); } else { self.notifyNewScreen('directory'); From 3f9f59bb73b743680ef99110ea9beb5e618f940e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 15:23:23 +0000 Subject: [PATCH 099/113] Import RtsClient at top --- src/Lifecycle.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 475cffcff8..5aad0fae36 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -23,6 +23,7 @@ import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; +import RtsClient from './RtsClient'; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -228,7 +229,7 @@ function _restoreFromLocalStorage() { return false; } } -const RtsClient = require("./RtsClient"); + let rtsClient = null; export function initRtsClient(url) { rtsClient = new RtsClient(url); From 3d30db81e0d7571ae5208063320456563fb02f27 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 7 Feb 2017 15:24:57 +0000 Subject: [PATCH 100/113] Only init RTS if configured correctly --- src/components/structures/MatrixChat.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3e57684ac2..73c8ed21d3 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -210,7 +210,11 @@ module.exports = React.createClass({ window.addEventListener('resize', this.handleResize); this.handleResize(); - Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); + if (this.props.config.teamServerConfig && + this.props.config.teamServerConfig.teamServerURL + ) { + Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); + } // the extra q() ensures that synchronous exceptions hit the same codepath as // asynchronous ones. From 8fea4c27cb6c8c1793ced6dc4e99659a7466aed4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 7 Feb 2017 22:00:56 +0000 Subject: [PATCH 101/113] fix NPE --- src/components/views/elements/MemberEventListSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 61fa0e076f..1a73b5a50e 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -385,7 +385,7 @@ module.exports = React.createClass({ } userEvents[userId].push({ mxEvent: e, - displayName: e.target.name || userId, + displayName: (e.target ? e.target.name : null) || userId, index: index, }); }); From 260cb624385a21d056ab609d9ff30a866df3e369 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Feb 2017 16:49:33 +0000 Subject: [PATCH 102/113] Do not set team_token if not returned by RTS on login --- src/Lifecycle.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 5aad0fae36..e899ec6ad8 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -270,7 +270,9 @@ export function setLoggedIn(credentials) { if (rtsClient) { rtsClient.login(credentials.userId).then((body) => { - localStorage.setItem("mx_team_token", body.team_token); + if (body.team_token) { + localStorage.setItem("mx_team_token", body.team_token); + } }, (err) =>{ console.error( "Failed to get team token on login, not persisting to localStorage", From c2d5b72d68a09f7071601cf0d1b4fa76ffa9485e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Feb 2017 17:58:40 +0000 Subject: [PATCH 103/113] Set referrer qp in nextLink This is so that when the verification link is clicked from an email, the referrer is set on the new instance of riot when /rts/register is hit --- src/Signup.js | 4 ++++ src/SignupStages.js | 5 +++++ src/components/structures/login/Registration.js | 3 +++ 3 files changed, 12 insertions(+) diff --git a/src/Signup.js b/src/Signup.js index d3643bd749..022a93524c 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -91,6 +91,10 @@ class Register extends Signup { this.params.idSid = idSid; } + setReferrer(referrer) { + this.params.referrer = referrer; + } + setGuestAccessToken(token) { this.guestAccessToken = token; } diff --git a/src/SignupStages.js b/src/SignupStages.js index 6bdc331566..cdb9d5989b 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -136,6 +136,11 @@ class EmailIdentityStage extends Stage { "&session_id=" + encodeURIComponent(this.signupInstance.getServerData().session); + // Add the user ID of the referring user, if set + if (this.signupInstance.params.referrer) { + nextLink += "&referrer=" + encodeURIComponent(this.signupInstance.params.referrer); + } + var self = this; return this.client.requestRegisterEmailToken( this.signupInstance.email, diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 0fc0cac527..efe7dae723 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -98,6 +98,9 @@ module.exports = React.createClass({ this.registerLogic.setRegistrationUrl(this.props.registrationUrl); this.registerLogic.setIdSid(this.props.idSid); this.registerLogic.setGuestAccessToken(this.props.guestAccessToken); + if (this.props.referrer) { + this.registerLogic.setReferrer(this.props.referrer); + } this.registerLogic.recheckState(); if ( From 231997dd63da33b231307e8ea5ff6db5b8375d80 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 9 Feb 2017 01:18:09 +0000 Subject: [PATCH 104/113] unbreak /markdown off --- src/HtmlUtils.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index c7b13bc071..b9d0ce67e8 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -290,7 +290,7 @@ export function bodyToHtml(content, highlights, opts) { } EMOJI_REGEX.lastIndex = 0; - let contentBodyTrimmed = content.body.trim(); + let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : ''; let match = EMOJI_REGEX.exec(contentBodyTrimmed); let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length; diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 63821b7d96..020c2238cb 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -352,7 +352,7 @@ export default React.createClass({ MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { - const contentText = mdown.toPlaintext(); + if (mdown) contentText = mdown.toPlaintext(); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From ea50acfc8748835a0c3e5d22cfa3d78f4ddffc7f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 9 Feb 2017 01:26:53 +0000 Subject: [PATCH 105/113] fix dark theme for uploadbar --- src/components/structures/UploadBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index e91e558cb2..8266a11bc8 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -90,8 +90,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
    - - +
    From ad2710ec5d94aa0ad98437d088b0de8f7e9fe51b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 9 Feb 2017 02:00:58 +0000 Subject: [PATCH 106/113] fix CSS for import/export buttons --- src/components/structures/UserSettings.js | 28 +++++++++++------------ 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 8885747585..fdade60dfd 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -552,21 +552,20 @@ module.exports = React.createClass({ const deviceId = client.deviceId; const identityKey = client.getDeviceEd25519Key() || ""; - let exportButton = null, - importButton = null; + let importExportButtons = null; if (client.isCryptoEnabled) { - exportButton = ( - - Export E2E room keys - - ); - importButton = ( - - Import E2E room keys - + importExportButtons = ( +
    + + Export E2E room keys + + + Import E2E room keys + +
    ); } return ( @@ -577,8 +576,7 @@ module.exports = React.createClass({
  • {deviceId}
  • {identityKey}
  • - {exportButton} - {importButton} + { importExportButtons }
    { CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } From 40658494b4b90bfe9779fa1e849b592c89210da8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 09:29:55 +0000 Subject: [PATCH 107/113] Consider emails ending in matrix.org as a uni email For the purposes of testing (and having a team page) --- src/components/views/login/RegistrationForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 1cb8253812..1a448fa84e 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -146,7 +146,7 @@ module.exports = React.createClass({ }, _isUniEmail: function(email) { - return email.endsWith('.ac.uk') || email.endsWith('.edu'); + return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org'); }, validateField: function(field_id) { From cc69e982a75985964afdccb3e9fcc264e221ff5e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 10:01:51 +0000 Subject: [PATCH 108/113] Use single state to set both avatars and typing notif --- src/WhoIsTyping.js | 3 +-- src/components/structures/RoomStatusBar.js | 21 +++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index ecd7c495f9..bff536a61e 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -48,8 +48,7 @@ module.exports = { return whoIsTyping; }, - whoIsTypingString: function(room, limit) { - const whoIsTyping = this.usersTypingApartFromMe(room); + whoIsTypingString: function(whoIsTyping, limit) { const othersCount = limit === undefined ? 0 : Math.max(whoIsTyping.length - limit, 0); if (whoIsTyping.length == 0) { diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 3fd0a3b751..d0f4fbfa0f 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -89,10 +89,7 @@ module.exports = React.createClass({ getInitialState: function() { return { syncState: MatrixClientPeg.get().getSyncState(), - whoisTypingString: WhoIsTyping.whoIsTypingString( - this.props.room, - this.props.whoIsTypingLimit - ), + usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), }; }, @@ -141,10 +138,7 @@ module.exports = React.createClass({ onRoomMemberTyping: function(ev, member) { this.setState({ - whoisTypingString: WhoIsTyping.whoIsTypingString( - this.props.room, - this.props.whoIsTypingLimit - ), + usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), }); }, @@ -153,7 +147,7 @@ module.exports = React.createClass({ // indicate other sizes. _getSize: function(state, props) { if (state.syncState === "ERROR" || - state.whoisTypingString || + (state.usersTyping.length > 0) || props.numUnreadMessages || !props.atEndOfLiveTimeline || props.hasActiveCall) { @@ -220,7 +214,7 @@ module.exports = React.createClass({ }, _renderTypingIndicatorAvatars: function(limit) { - let users = WhoIsTyping.usersTypingApartFromMe(this.props.room); + let users = this.state.usersTyping; let othersCount = Math.max(users.length - limit, 0); users = users.slice(0, limit); @@ -324,7 +318,10 @@ module.exports = React.createClass({ ); } - var typingString = this.state.whoisTypingString; + const typingString = WhoIsTyping.whoIsTypingString( + this.state.usersTyping, + this.props.whoIsTypingLimit + ); if (typingString) { return (
    @@ -347,7 +344,7 @@ module.exports = React.createClass({ render: function() { var content = this._getContent(); - var indicator = this._getIndicator(this.state.whoisTypingString !== null); + var indicator = this._getIndicator(this.state.usersTyping.length > 0); return (
    From 553054409fdd930656da2a755c7d3d0dff8a8d6a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 10:01:59 +0000 Subject: [PATCH 109/113] Use (props,state) ordering of arguments There was a bug here that meant that sometimes arguments were given in the wrong order; presumably leading to the status bar not appearing for calls etc. --- src/components/structures/RoomStatusBar.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index d0f4fbfa0f..adbfb25337 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -103,7 +103,7 @@ module.exports = React.createClass({ this.props.onResize(); } - const size = this._getSize(this.state, this.props); + const size = this._getSize(this.props, this.state); if (size > 0) { this.props.onVisible(); } else { @@ -145,7 +145,7 @@ module.exports = React.createClass({ // We don't need the actual height - just whether it is likely to have // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. - _getSize: function(state, props) { + _getSize: function(props, state) { if (state.syncState === "ERROR" || (state.usersTyping.length > 0) || props.numUnreadMessages || @@ -163,7 +163,8 @@ module.exports = React.createClass({ // determine if we need to call onResize _checkForResize: function(prevProps, prevState) { // figure out the old height and the new height of the status bar. - return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state); + return this._getSize(prevProps, prevState) + !== this._getSize(this.props, this.state); }, // return suitable content for the image on the left of the status bar. From 103710728f74f3c74e5ead0a65eaf59084788f5a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Feb 2017 10:30:06 +0000 Subject: [PATCH 110/113] Do not show "+1 other" Instead show a user name or avatar. --- src/WhoIsTyping.js | 8 +++++--- src/components/structures/RoomStatusBar.js | 9 ++++++--- src/components/structures/RoomView.js | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index bff536a61e..4502b0ccd9 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -49,8 +49,10 @@ module.exports = { }, whoIsTypingString: function(whoIsTyping, limit) { - const othersCount = limit === undefined ? - 0 : Math.max(whoIsTyping.length - limit, 0); + let othersCount = 0; + if (whoIsTyping.length > limit) { + othersCount = whoIsTyping.length - limit + 1; + } if (whoIsTyping.length == 0) { return ''; } else if (whoIsTyping.length == 1) { @@ -61,7 +63,7 @@ module.exports = { }); if (othersCount) { const other = ' other' + (othersCount > 1 ? 's' : ''); - return names.slice(0, limit).join(', ') + ' and ' + + return names.slice(0, limit - 1).join(', ') + ' and ' + othersCount + other + ' are typing'; } else { const lastPerson = names.pop(); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index adbfb25337..288ca0b974 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -82,7 +82,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - whoIsTypingLimit: 2, + whoIsTypingLimit: 3, }; }, @@ -217,8 +217,11 @@ module.exports = React.createClass({ _renderTypingIndicatorAvatars: function(limit) { let users = this.state.usersTyping; - let othersCount = Math.max(users.length - limit, 0); - users = users.slice(0, limit); + let othersCount = 0; + if (users.length > limit) { + othersCount = users.length - limit + 1; + users = users.slice(0, limit - 1); + } let avatars = users.map((u, index) => { let showInitial = othersCount === 0 && index === users.length - 1; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index a7d52019c4..432dc5b724 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1537,7 +1537,7 @@ module.exports = React.createClass({ onResize={this.onChildResize} onVisible={this.onStatusBarVisible} onHidden={this.onStatusBarHidden} - whoIsTypingLimit={2} + whoIsTypingLimit={3} />; } From 45f5b8b3a9261f63c3c7ac00a518d7694abfb62e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 09:57:28 +0000 Subject: [PATCH 111/113] Thread teamToken through to LeftPanel for "Home" button This means the riot-web will use the same teamToken used by sdk components. This includes cases where only the fragment query parameter has been provided. Fixes matrix-org/riot-web#3185 --- src/components/structures/LoggedInView.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index ba63794f60..961277a4a1 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -232,7 +232,12 @@ export default React.createClass({
    {topBar}
    - +
    {page_element}
    From bab6a0b84a621210ce5e0a8f2dafb069d7f51e85 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 11:31:04 +0000 Subject: [PATCH 112/113] Persist query parameter team token across refreshes --- src/components/structures/MatrixChat.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ab73ba366b..c3467355bd 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -191,10 +191,18 @@ module.exports = React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } + // Persist the team token across refreshes + if (this.props.startingFragmentQueryParams.team_token) { + window.sessionStorage.setItem( + 'mx_team_token', + this.props.startingFragmentQueryParams.team_token, + ); + } + // Use the locally-stored team token first, then as a fall-back, check to see if // a referral link was used, which will contain a query parameter `team_token`. this._teamToken = window.localStorage.getItem('mx_team_token') || - this.props.startingFragmentQueryParams.team_token; + window.sessionStorage.getItem('mx_team_token'); }, componentDidMount: function() { From ec730056d85d2d1e9ec04dff4cbf705f5ffa2e5f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 10 Feb 2017 11:39:22 +0000 Subject: [PATCH 113/113] Alter comment --- src/components/structures/MatrixChat.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c3467355bd..8fdcf15e1b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -191,7 +191,8 @@ module.exports = React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } - // Persist the team token across refreshes + // Persist the team token across refreshes using sessionStorage. A new window or + // tab will not persist sessionStorage, but refreshes will. if (this.props.startingFragmentQueryParams.team_token) { window.sessionStorage.setItem( 'mx_team_token',