From 06a05c351d95fbb90ee2db65cb04bb902eb23228 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 10:25:54 +0000 Subject: [PATCH 01/16] Decide on which screen to show after login in one place This follows from a small amount of refactoring done when RTS was introduced. Instead of setting the screen after sync, do it only after login. This requires as-yet-to-be-PRd riot-web changes. This includes: - initialScreenAfterLogin, which can be used to set the screen after login, and represents the screen that would be viewed if the window.location at the time of initialising Riot were routed. - guestCreds are now part of state, because otherwise they don't cause the login/registration views to update when set. - instead of worrying about races and using this._setPage, use a dispatch. --- src/components/structures/MatrixChat.js | 79 +++++++++++-------- .../structures/login/Registration.js | 1 - 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44fdfcf23e..7c398b39f9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -63,6 +63,13 @@ module.exports = React.createClass({ // called when the session load completes onLoadCompleted: React.PropTypes.func, + // Represents the screen to display as a result of parsing the initial + // window.location + initialScreenAfterLogin: React.PropTypes.shape({ + screen: React.PropTypes.string.isRequired, + params: React.PropTypes.object, + }), + // displayname, if any, to set on the device when logging // in/registering. defaultDeviceDisplayName: React.PropTypes.string, @@ -89,6 +96,12 @@ module.exports = React.createClass({ var s = { loading: true, screen: undefined, + screenAfterLogin: this.props.initialScreenAfterLogin, + + // Stashed guest credentials if the user logs out + // whilst logged in as a guest user (so they can change + // their mind & log back in) + guestCreds: null, // What the LoggedInView would be showing if visible page_type: null, @@ -184,11 +197,6 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); - // Stashed guest credentials if the user logs out - // whilst logged in as a guest user (so they can change - // their mind & log back in) - this.guestCreds = null; - // if the automatic session load failed, the error this.sessionLoadError = null; @@ -322,9 +330,6 @@ module.exports = React.createClass({ var self = this; switch (payload.action) { case 'logout': - if (MatrixClientPeg.get().isGuest()) { - this.guestCreds = MatrixClientPeg.getCredentials(); - } Lifecycle.logout(); break; case 'start_registration': @@ -344,7 +349,11 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); break; case 'start_login': - if (this.state.logged_in) return; + if (MatrixClientPeg.get().isGuest()) { + this.setState({ + guestCreds: MatrixClientPeg.getCredentials(), + }); + } this.setStateForNewScreen({ screen: 'login', }); @@ -359,8 +368,8 @@ module.exports = React.createClass({ // also stash our credentials, then if we restore the session, // we can just do it the same way whether we started upgrade // registration or explicitly logged out - this.guestCreds = MatrixClientPeg.getCredentials(); this.setStateForNewScreen({ + guestCreds: MatrixClientPeg.getCredentials(), screen: "register", upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), guestAccessToken: MatrixClientPeg.get().getAccessToken(), @@ -708,19 +717,34 @@ module.exports = React.createClass({ * Called when a new logged in session has started */ _onLoggedIn: function(teamToken) { - this.guestCreds = null; - this.notifyNewScreen(''); this.setState({ - screen: undefined, + guestCreds: null, logged_in: true, }); + // If screenAfterLogin is set, use that, then null it so that a second login will + // result in view_home_page, _user_settings or _room_directory + if (this.state.screenAfterLogin) { + this.showScreen( + this.state.screenAfterLogin.screen, + this.state.screenAfterLogin.params + ); + this.setState({screenAfterLogin: null}); + return; + } else { + this.setState({screen: undefined}); + } + if (teamToken) { this._teamToken = teamToken; - this._setPage(PageTypes.HomePage); + dis.dispatch({action: 'view_home_page'}); + return; } else if (this._is_registered) { - this._setPage(PageTypes.UserSettings); + dis.dispatch({action: 'view_user_settings'}); + return; } + + dis.dispatch({action: 'view_room_directory'}); }, /** @@ -768,12 +792,6 @@ module.exports = React.createClass({ cli.getRooms() )[0].roomId; self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); - } else { - if (self._teamToken) { - 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}); @@ -790,16 +808,7 @@ module.exports = React.createClass({ if (presentedId != undefined) { self.notifyNewScreen('room/'+presentedId); - } else { - // There is no information on presentedId - // so point user to fallback like /directory - if (self._teamToken) { - self.notifyNewScreen('home'); - } else { - self.notifyNewScreen('directory'); - } } - dis.dispatch({action: 'focus_composer'}); } else { self.setState({ready: true}); @@ -1002,9 +1011,9 @@ module.exports = React.createClass({ onReturnToGuestClick: function() { // reanimate our guest login - if (this.guestCreds) { - Lifecycle.setLoggedIn(this.guestCreds); - this.guestCreds = null; + if (this.state.guestCreds) { + Lifecycle.setLoggedIn(this.state.guestCreds); + this.setState({guestCreds: null}); } }, @@ -1153,7 +1162,7 @@ module.exports = React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} - onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} + onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} /> ); } else if (this.state.screen == 'forgot_password') { @@ -1180,7 +1189,7 @@ module.exports = React.createClass({ defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} onForgotPasswordClick={this.onForgotPasswordClick} enableGuest={this.props.enableGuest} - onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} + onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} initialErrorText={this.sessionLoadError} /> ); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 92f64eb6ab..57a7d6e19d 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -192,7 +192,6 @@ module.exports = React.createClass({ const teamToken = data.team_token; // Store for use /w welcome pages window.localStorage.setItem('mx_team_token', teamToken); - this.props.onTeamMemberRegistered(teamToken); this._rtsClient.getTeam(teamToken).then((team) => { console.log( From eca82bdb42fd03e889d4618bedf2695212dc8e51 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 10:45:07 +0000 Subject: [PATCH 02/16] Make sure the screen is set, otherwise ignore screenAfterLogin --- 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 7c398b39f9..bae1f0849a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -724,7 +724,7 @@ module.exports = React.createClass({ // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory - if (this.state.screenAfterLogin) { + if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { this.showScreen( this.state.screenAfterLogin.screen, this.state.screenAfterLogin.params From c4001b5c5d43c78101ca51b48f4cc63a2c5667f2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 8 Mar 2017 15:11:38 +0000 Subject: [PATCH 03/16] Use else instead of two returns --- src/components/structures/MatrixChat.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index bae1f0849a..fbb585924e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -738,13 +738,11 @@ module.exports = React.createClass({ if (teamToken) { this._teamToken = teamToken; dis.dispatch({action: 'view_home_page'}); - return; } else if (this._is_registered) { dis.dispatch({action: 'view_user_settings'}); - return; + } else { + dis.dispatch({action: 'view_room_directory'}); } - - dis.dispatch({action: 'view_room_directory'}); }, /** From 4f7914813dc07df330364b20509703163ce17968 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Mar 2017 10:44:09 +0000 Subject: [PATCH 04/16] Make UDD appear when UDE on uploading a file This has highlighted the fact that an unsent image looks very much like a sent image (https://github.com/vector-im/riot-web/issues/3391). Also, the "Resend" status bar doesn't appear when an image is unsent. --- src/components/structures/RoomView.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 696d15f84a..47b572b6c3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -915,8 +915,6 @@ module.exports = React.createClass({ }, uploadFile: function(file) { - var self = this; - if (MatrixClientPeg.get().isGuest()) { var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { @@ -928,8 +926,16 @@ module.exports = React.createClass({ ContentMessages.sendContentToRoom( file, this.state.room.roomId, MatrixClientPeg.get() - ).done(undefined, function(error) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + ).done(undefined, (error) => { + if (error.name === "UnknownDeviceError") { + dis.dispatch({ + action: 'unknown_device_error', + err: error, + room: this.state.room, + }); + return; + } + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", description: error.toString() From 1f788feacd89811315fd8cb2262ad94e30906ec7 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 9 Mar 2017 17:03:57 +0000 Subject: [PATCH 05/16] Merge the two RoomTile context menus into one This will require riot-web changes https://github.com/vector-im/riot-web/pull/3395 --- src/components/structures/MatrixChat.js | 48 ++++++++++++---- src/components/views/rooms/RoomTile.js | 76 +++++++------------------ 2 files changed, 56 insertions(+), 68 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44fdfcf23e..fd0c69c662 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -317,6 +317,8 @@ module.exports = React.createClass({ }, onAction: function(payload) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); var roomIndexDelta = 1; var self = this; @@ -382,25 +384,23 @@ module.exports = React.createClass({ this.notifyNewScreen('forgot_password'); break; case 'leave_room': - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - var roomId = payload.room_id; Modal.createDialog(QuestionDialog, { title: "Leave room", description: "Are you sure you want to leave the room?", - onFinished: function(should_leave) { + onFinished: (should_leave) => { if (should_leave) { - var d = MatrixClientPeg.get().leave(roomId); + const d = MatrixClientPeg.get().leave(payload.room_id); // FIXME: controller shouldn't be loading a view :( - var Loader = sdk.getComponent("elements.Spinner"); - var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + const Loader = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - d.then(function() { + d.then(() => { modal.close(); - dis.dispatch({action: 'view_next_room'}); - }, function(err) { + if (this.currentRoomId === payload.room_id) { + dis.dispatch({action: 'view_next_room'}); + } + }, (err) => { modal.close(); Modal.createDialog(ErrorDialog, { title: "Failed to leave room", @@ -411,6 +411,32 @@ module.exports = React.createClass({ } }); break; + case 'reject_invite': + Modal.createDialog(QuestionDialog, { + title: "Reject invitation", + description: "Are you sure you want to reject the invitation?", + onFinished: (confirm) => { + if (confirm) { + // FIXME: controller shouldn't be loading a view :( + const Loader = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + + MatrixClientPeg.get().leave(payload.room_id).done(() => { + modal.close(); + if (this.currentRoomId === payload.room_id) { + dis.dispatch({action: 'view_next_room'}); + } + }, (err) => { + modal.close(); + Modal.createDialog(ErrorDialog, { + title: "Failed to reject invitation", + description: err.toString() + }); + }); + } + } + }); + break; case 'view_user': // FIXME: ugly hack to expand the RightPanel and then re-dispatch. if (this.state.collapse_rhs) { diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 7d9034edd2..06b05e9299 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -56,8 +56,7 @@ module.exports = React.createClass({ return({ hover : false, badgeHover : false, - notificationTagMenu: false, - roomTagMenu: false, + menuDisplayed: false, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), }); }, @@ -136,62 +135,32 @@ module.exports = React.createClass({ this.setState({ hover: false }); } - var NotificationStateMenu = sdk.getComponent('context_menus.NotificationStateContextMenu'); + var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); var elementRect = e.target.getBoundingClientRect(); + // The window X and Y offsets are to adjust position when zoomed in to page - var x = elementRect.right + window.pageXOffset + 3; - var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; + const x = elementRect.right + window.pageXOffset + 3; + const chevronOffset = 12; + let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + y = y - (chevronOffset + 8); // where 8 is half the height of the chevron + var self = this; - ContextualMenu.createMenu(NotificationStateMenu, { - menuWidth: 188, - menuHeight: 126, - chevronOffset: 45, + ContextualMenu.createMenu(RoomTileContextMenu, { + chevronOffset: chevronOffset, left: x, top: y, room: this.props.room, onFinished: function() { - self.setState({ notificationTagMenu: false }); + self.setState({ menuDisplayed: false }); self.props.refreshSubList(); } }); - this.setState({ notificationTagMenu: true }); + this.setState({ menuDisplayed: true }); } // Prevent the RoomTile onClick event firing as well e.stopPropagation(); }, - onAvatarClicked: function(e) { - // Only allow none guests to access the context menu - if (!MatrixClientPeg.get().isGuest() && !this.props.collapsed) { - - // If the badge is clicked, then no longer show tooltip - if (this.props.collapsed) { - this.setState({ hover: false }); - } - - var RoomTagMenu = sdk.getComponent('context_menus.RoomTagContextMenu'); - var elementRect = e.target.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - var x = elementRect.right + window.pageXOffset + 3; - var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 19; - var self = this; - ContextualMenu.createMenu(RoomTagMenu, { - chevronOffset: 10, - // XXX: fix horrid hardcoding - menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF", - left: x, - top: y, - room: this.props.room, - onFinished: function() { - self.setState({ roomTagMenu: false }); - } - }); - this.setState({ roomTagMenu: true }); - // Prevent the RoomTile onClick event firing as well - e.stopPropagation(); - } - }, - render: function() { var myUserId = MatrixClientPeg.get().credentials.userId; var me = this.props.room.currentState.members[myUserId]; @@ -210,7 +179,7 @@ module.exports = React.createClass({ 'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_invited': (me && me.membership == 'invite'), - 'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu, + 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, 'mx_RoomTile_noBadges': !badges, }); @@ -218,14 +187,9 @@ module.exports = React.createClass({ 'mx_RoomTile_avatar': true, }); - var avatarContainerClasses = classNames({ - 'mx_RoomTile_avatar_container': true, - 'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu, - }); - var badgeClasses = classNames({ 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.notificationTagMenu, + 'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed, }); // XXX: We should never display raw room IDs, but sometimes the @@ -236,7 +200,7 @@ module.exports = React.createClass({ var badge; var badgeContent; - if (this.state.badgeHover || this.state.notificationTagMenu) { + if (this.state.badgeHover || this.state.menuDisplayed) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (badges) { var limitedCount = FormattingUtils.formatCount(notificationCount); @@ -254,7 +218,7 @@ module.exports = React.createClass({ var nameClasses = classNames({ 'mx_RoomTile_name': true, 'mx_RoomTile_invite': this.props.isInvite, - 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.notificationTagMenu, + 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, }); if (this.props.selected) { @@ -293,11 +257,9 @@ module.exports = React.createClass({
{ /* Only native elements can be wrapped in a DnD object. */}
-
-
- - {directMessageIndicator} -
+
+ + {directMessageIndicator}
From d8a30aa848eab30138a9de102106b014230a3a5d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Mar 2017 13:48:15 +0000 Subject: [PATCH 06/16] Double UNPAGINATION_PADDING again --- src/components/structures/ScrollPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 4a0faae9db..44176f73af 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -25,7 +25,7 @@ var DEBUG_SCROLL = false; // The amount of extra scroll distance to allow prior to unfilling. // See _getExcessHeight. -const UNPAGINATION_PADDING = 3000; +const UNPAGINATION_PADDING = 6000; // The number of milliseconds to debounce calls to onUnfillRequest, to prevent // many scroll events causing many unfilling requests. const UNFILL_REQUEST_DEBOUNCE_MS = 200; From ba0715ba7c3818d9b7656d66bab4de534bd33bfb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Mar 2017 14:10:14 +0000 Subject: [PATCH 07/16] Fix import for Lifecycle This fixes https://github.com/vector-im/riot-web/issues/2991 although we might need to give more feedback than just showing the login screen. Maybe a dialog that says "your account has been deactivated". --- src/components/views/dialogs/DeactivateAccountDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 54a4e99424..b4879982bf 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -18,7 +18,7 @@ import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import Lifecycle from '../../../Lifecycle'; +import * as Lifecycle from '../../../Lifecycle'; import Velocity from 'velocity-vector'; export default class DeactivateAccountDialog extends React.Component { From 47958180a6cbf352fa5ac1a293374c5021b9ba39 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Mar 2017 15:13:36 +0000 Subject: [PATCH 08/16] Add null check to start_login --- src/components/structures/MatrixChat.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7229c71437..4f3c8fe7e5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -351,7 +351,9 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); break; case 'start_login': - if (MatrixClientPeg.get().isGuest()) { + if (MatrixClientPeg.get() && + MatrixClientPeg.get().isGuest() + ) { this.setState({ guestCreds: MatrixClientPeg.getCredentials(), }); From 238e48e4afb46a387d5632c552163b0b58ce7078 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 15 Mar 2017 12:02:08 +0000 Subject: [PATCH 09/16] Do routing to /register _onLoadCompleted _onLoadCompleted happens straight away because Lifecycle finishes loading the session instantly when registration parameters (client_secret etc.) are set. --- src/components/structures/MatrixChat.js | 29 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4f3c8fe7e5..48e7f40dca 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -696,6 +696,13 @@ module.exports = React.createClass({ _onLoadCompleted: function() { this.props.onLoadCompleted(); this.setState({loading: false}); + + // Show screens (like 'register') that need to be shown without onLoggedIn + // being called. 'register' needs to be routed here when the email confirmation + // link is clicked on. + if (['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { + this._showScreenAfterLogin(); + } }, /** @@ -751,6 +758,17 @@ module.exports = React.createClass({ logged_in: true, }); + if (teamToken) { + this._teamToken = teamToken; + dis.dispatch({action: 'view_home_page'}); + } else if (this._is_registered) { + dis.dispatch({action: 'view_user_settings'}); + } else { + this._showScreenAfterLogin(); + } + }, + + _showScreenAfterLogin: function() { // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { @@ -758,17 +776,8 @@ module.exports = React.createClass({ this.state.screenAfterLogin.screen, this.state.screenAfterLogin.params ); + this.notifyNewScreen(this.state.screenAfterLogin.screen); this.setState({screenAfterLogin: null}); - return; - } else { - this.setState({screen: undefined}); - } - - if (teamToken) { - this._teamToken = teamToken; - dis.dispatch({action: 'view_home_page'}); - } else if (this._is_registered) { - dis.dispatch({action: 'view_user_settings'}); } else { dis.dispatch({action: 'view_room_directory'}); } From 5330e47b3fd91df7833f1cc22bd96c7cea4d0130 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 15 Mar 2017 13:05:03 +0000 Subject: [PATCH 10/16] Add null check --- 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 48e7f40dca..2337d62fd8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -700,7 +700,8 @@ module.exports = React.createClass({ // Show screens (like 'register') that need to be shown without onLoggedIn // being called. 'register' needs to be routed here when the email confirmation // link is clicked on. - if (['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { + if (this.state.screenAfterLogin && + ['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { this._showScreenAfterLogin(); } }, From ed22a74eafc63ba8206cae3dcc31c2dee2162906 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 11:31:47 +0000 Subject: [PATCH 11/16] Fix People section a bit This does two things: - Sets `editable` to true for the "People" `RoomSubList` so that people can be dragged to favourites. This has the downside that you can drag a "People" to "Rooms", but it won't set the direct chat flag. This is because im.vector.fake.direct != m.direct, sadly. - Sets `alwaysShowHeader` to `true` so that the `IncomingCallDialog` can be showneven when there are no people in `sortedList`. Fixes https://github.com/vector-im/riot-web/issues/2956. --- src/components/views/rooms/RoomList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c3ee5f1730..e84c56e693 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -485,11 +485,12 @@ module.exports = React.createClass({ From b21f016d37badbe94be6689675eaf15f4c64fb9b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 14:18:18 +0000 Subject: [PATCH 12/16] Add "Export E2E keys" option to logout dialog Fixes https://github.com/vector-im/riot-web/issues/3184 --- src/components/structures/UserSettings.js | 6 ++++++ src/components/views/dialogs/QuestionDialog.js | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index febdccd9c3..9e6d454fe9 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -268,6 +268,12 @@ module.exports = React.createClass({ but for now be warned.
, button: "Sign out", + extraButtons: [ + + ], onFinished: (confirmed) => { if (confirmed) { dis.dispatch({action: 'logout'}); diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 0260fc29e2..6012541b94 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -21,10 +21,8 @@ export default React.createClass({ displayName: 'QuestionDialog', propTypes: { title: React.PropTypes.string, - description: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.string, - ]), + description: React.PropTypes.node, + extraButtons: React.PropTypes.node, button: React.PropTypes.string, focus: React.PropTypes.bool, onFinished: React.PropTypes.func.isRequired, @@ -34,6 +32,7 @@ export default React.createClass({ return { title: "", description: "", + extraButtons: null, button: "OK", focus: true, hasCancelButton: true, @@ -67,6 +66,7 @@ export default React.createClass({ + {this.props.extraButtons} {cancelButton}
From 3ce0da452849fbb3eedfd63392c2f8f580b9ea8e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 15:05:54 +0000 Subject: [PATCH 13/16] Fix UDD for voip in e2e rooms When starting a call, several events are sent and if some devices are unverified, all three will trigger their own UnknownDeviceError. This causes three overlapping, identical UnknownDeviceDialogs. This change effectively dedupes the dialogs so that only one is shown. This is safe to do because the UDD allows resending of _all_ events that were left unsent. Fixes https://github.com/vector-im/riot-web/issues/3285 --- src/UnknownDeviceErrorHandler.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index d842cc3a6e..2aa0573e22 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -18,13 +18,17 @@ import dis from './dispatcher'; import sdk from './index'; import Modal from './Modal'; +let isDialogOpen = false; + const onAction = function(payload) { - if (payload.action === 'unknown_device_error') { + if (payload.action === 'unknown_device_error' && !isDialogOpen) { var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); + isDialogOpen = true; Modal.createDialog(UnknownDeviceDialog, { devices: payload.err.devices, room: payload.room, onFinished: (r) => { + isDialogOpen = false; // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('UnknownDeviceDialog closed with '+r); From d4ed9e816ba6fa0820f005276744c5996cefad07 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 16 Mar 2017 17:00:10 +0000 Subject: [PATCH 14/16] Display timestamps and profiles for redacted events This is part of fixing https://github.com/vector-im/riot-web/issues/3390 --- src/components/structures/MessagePanel.js | 2 +- src/components/views/rooms/EventTile.js | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index ff507b6f90..0f8d35f525 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -413,7 +413,7 @@ module.exports = React.createClass({ var continuation = false; if (prevEvent !== null - && !prevEvent.isRedacted() && prevEvent.sender && mxEv.sender + && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && mxEv.getType() == prevEvent.getType()) { continuation = true; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 48f0f282c1..b451d1c046 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -435,10 +435,7 @@ module.exports = WithMatrixClient(React.createClass({ let avatarSize; let needsSenderProfile; - if (isRedacted) { - avatarSize = 0; - needsSenderProfile = false; - } else if (this.props.tileShape === "notif") { + if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; } else if (isInfoMessage) { @@ -503,8 +500,8 @@ module.exports = WithMatrixClient(React.createClass({ else if (e2eEnabled) { e2e = ; } - const timestamp = this.props.mxEvent.isRedacted() ? - null : ; + const timestamp = this.props.mxEvent.getTs() ? + : null; if (this.props.tileShape === "notif") { var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); From 375ae8fb04d8775951569d9a75027553bcbf82e8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2017 17:26:42 +0000 Subject: [PATCH 15/16] Fix password UI auth test By adding a way to wait a short time for a component to appear in the DOM, so we don't get flakey failures like this when we change something to returning a promise that needs to resolve before the component actually appears. --- .../dialogs/InteractiveAuthDialog-test.js | 75 ++++++++++--------- test/test-utils.js | 50 +++++++++++-- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index da8fc17001..50500ba6e3 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -68,50 +68,51 @@ describe('InteractiveAuthDialog', function () { onFinished={onFinished} />, parentDiv); - // at this point there should be a password box and a submit button - const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form"); - const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( - dlg, "input" - ); - let passwordNode; - let submitNode; - for (const node of inputNodes) { - if (node.type == 'password') { - passwordNode = node; - } else if (node.type == 'submit') { - submitNode = node; + // wait for a password box and a submit button + test_utils.waitForRenderedDOMComponentWithTag(dlg, "form").then((formNode) => { + const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( + dlg, "input" + ); + let passwordNode; + let submitNode; + for (const node of inputNodes) { + if (node.type == 'password') { + passwordNode = node; + } else if (node.type == 'submit') { + submitNode = node; + } } - } - expect(passwordNode).toExist(); - expect(submitNode).toExist(); + expect(passwordNode).toExist(); + expect(submitNode).toExist(); - // submit should be disabled - expect(submitNode.disabled).toBe(true); + // submit should be disabled + expect(submitNode.disabled).toBe(true); - // put something in the password box, and hit enter; that should - // trigger a request - passwordNode.value = "s3kr3t"; - ReactTestUtils.Simulate.change(passwordNode); - expect(submitNode.disabled).toBe(false); - ReactTestUtils.Simulate.submit(formNode, {}); + // put something in the password box, and hit enter; that should + // trigger a request + passwordNode.value = "s3kr3t"; + ReactTestUtils.Simulate.change(passwordNode); + expect(submitNode.disabled).toBe(false); + ReactTestUtils.Simulate.submit(formNode, {}); - expect(doRequest.callCount).toEqual(1); - expect(doRequest.calledWithExactly({ - session: "sess", - type: "m.login.password", - password: "s3kr3t", - user: "@user:id", - })).toBe(true); + expect(doRequest.callCount).toEqual(1); + expect(doRequest.calledWithExactly({ + session: "sess", + type: "m.login.password", + password: "s3kr3t", + user: "@user:id", + })).toBe(true); - // there should now be a spinner - ReactTestUtils.findRenderedComponentWithType( - dlg, sdk.getComponent('elements.Spinner'), - ); + // there should now be a spinner + ReactTestUtils.findRenderedComponentWithType( + dlg, sdk.getComponent('elements.Spinner'), + ); - // let the request complete - q.delay(1).then(() => { + // let the request complete + return q.delay(1); + }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a:1})).toBe(true); - }).done(done, done); + }).done(done); }); }); diff --git a/test/test-utils.js b/test/test-utils.js index aca91ad399..5209465362 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,11 +1,51 @@ "use strict"; -var sinon = require('sinon'); -var q = require('q'); +import sinon from 'sinon'; +import q from 'q'; +import ReactTestUtils from 'react-addons-test-utils'; -var peg = require('../src/MatrixClientPeg.js'); -var jssdk = require('matrix-js-sdk'); -var MatrixEvent = jssdk.MatrixEvent; +import peg from '../src/MatrixClientPeg.js'; +import jssdk from 'matrix-js-sdk'; +const MatrixEvent = jssdk.MatrixEvent; + +/** + * Wrapper around window.requestAnimationFrame that returns a promise + * @private + */ +function _waitForFrame() { + const def = q.defer(); + window.requestAnimationFrame(() => { + def.resolve(); + }); + return def.promise; +} + +/** + * Waits a small number of animation frames for a component to appear + * in the DOM. Like findRenderedDOMComponentWithTag(), but allows + * for the element to appear a short time later, eg. if a promise needs + * to resolve first. + * @return a promise that resolves once the component appears, or rejects + * if it doesn't appear after a nominal number of animation frames. + */ +export function waitForRenderedDOMComponentWithTag(tree, tag, attempts) { + if (attempts === undefined) { + // Let's start by assuming we'll only need to wait a single frame, and + // we can try increasing this if necessary. + attempts = 1; + } else if (attempts == 0) { + return q.reject("Gave up waiting for component with tag: " + tag); + } + + return _waitForFrame().then(() => { + const result = ReactTestUtils.scryRenderedDOMComponentsWithTag(tree, tag); + if (result.length > 0) { + return result[0]; + } else { + return waitForRenderedDOMComponentWithTag(tree, tag, attempts - 1); + } + }); +} /** * Perform common actions before each test case, e.g. printing the test case From 23c38bd8a3370304d8678b8ea11aade4fff8dc91 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Mar 2017 17:47:15 +0000 Subject: [PATCH 16/16] Put back both done's mocha takes the exception arg and does the right thing --- test/components/views/dialogs/InteractiveAuthDialog-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 50500ba6e3..b8a8e49769 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -113,6 +113,6 @@ describe('InteractiveAuthDialog', function () { }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a:1})).toBe(true); - }).done(done); + }).done(done, done); }); });