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); diff --git a/src/component-index.js b/src/component-index.js index 59d3ad53e4..c83c0dbb11 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -79,6 +79,8 @@ import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/Ch views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog); import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); +import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog'; +views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog); import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog'; views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog); import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2fa5e92608..2337d62fd8 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; @@ -317,14 +325,13 @@ 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; switch (payload.action) { case 'logout': - if (MatrixClientPeg.get().isGuest()) { - this.guestCreds = MatrixClientPeg.getCredentials(); - } Lifecycle.logout(); break; case 'start_registration': @@ -344,7 +351,13 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); break; case 'start_login': - if (this.state.logged_in) return; + if (MatrixClientPeg.get() && + MatrixClientPeg.get().isGuest() + ) { + this.setState({ + guestCreds: MatrixClientPeg.getCredentials(), + }); + } this.setStateForNewScreen({ screen: 'login', }); @@ -359,8 +372,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(), @@ -382,25 +395,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(); console.error("Failed to leave room " + payload.room_id + " " + err); Modal.createDialog(ErrorDialog, { @@ -412,6 +423,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) { @@ -659,6 +696,14 @@ 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 (this.state.screenAfterLogin && + ['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) { + this._showScreenAfterLogin(); + } }, /** @@ -709,18 +754,33 @@ 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 (teamToken) { this._teamToken = teamToken; - this._setPage(PageTypes.HomePage); + dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { - this._setPage(PageTypes.UserSettings); + 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) { + this.showScreen( + this.state.screenAfterLogin.screen, + this.state.screenAfterLogin.params + ); + this.notifyNewScreen(this.state.screenAfterLogin.screen); + this.setState({screenAfterLogin: null}); + } else { + dis.dispatch({action: 'view_room_directory'}); } }, @@ -769,12 +829,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}); @@ -791,16 +845,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}); @@ -1003,9 +1048,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}); } }, @@ -1154,7 +1199,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') { @@ -1181,7 +1226,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/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/structures/RoomView.js b/src/components/structures/RoomView.js index 52161012aa..345d0f6b80 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"); console.error("Failed to upload file " + file + " " + error); Modal.createDialog(ErrorDialog, { title: "Failed to upload file", 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; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ed8a271241..6626d2c400 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -280,6 +280,12 @@ module.exports = React.createClass({ but for now be warned. , button: "Sign out", + extraButtons: [ + + ], onFinished: (confirmed) => { if (confirmed) { dis.dispatch({action: 'logout'}); @@ -840,6 +846,14 @@ module.exports = React.createClass({ return medium[0].toUpperCase() + medium.slice(1); }, + presentableTextForThreepid: function(threepid) { + if (threepid.medium == 'msisdn') { + return '+' + threepid.address; + } else { + return threepid.address; + } + }, + render: function() { var Loader = sdk.getComponent("elements.Spinner"); switch (this.state.phase) { @@ -872,7 +886,9 @@ module.exports = React.createClass({
- +
Remove diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index a878657de9..4e0d61e716 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -196,7 +196,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( diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js new file mode 100644 index 0000000000..fc9e55f666 --- /dev/null +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -0,0 +1,73 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; +import sdk from '../../../index'; +import classnames from 'classnames'; + +/* + * A dialog for confirming a redaction. + */ +export default React.createClass({ + displayName: 'ConfirmRedactDialog', + propTypes: { + onFinished: React.PropTypes.func.isRequired, + }, + + defaultProps: { + danger: false, + }, + + onOk: function() { + this.props.onFinished(true); + }, + + onCancel: function() { + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + const title = "Confirm Redaction"; + + const confirmButtonClass = classnames({ + 'mx_Dialog_primary': true, + 'danger': false, + }); + + return ( + +
+ Are you sure you wish to redact (delete) this event? + Note that if you redact a room name or topic change, it could undo the change. +
+
+ + + +
+
+ ); + }, +}); 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 { 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}
diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index a0fe8fdf74..9b1bd74087 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -24,7 +24,7 @@ module.exports = React.createClass({ render: function() { const text = this.props.mxEvent.getContent().body; return ( - + {text} ); 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()); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index d702b7558d..51c9ba881b 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -541,9 +541,9 @@ export default class MessageComposerInput extends React.Component { let sendTextFn = this.client.sendTextMessage; if (contentText.startsWith('/me')) { - contentText = contentText.replace('/me', ''); + contentText = contentText.replace('/me ', ''); // bit of a hack, but the alternative would be quite complicated - if (contentHTML) contentHTML = contentHTML.replace('/me', ''); + if (contentHTML) contentHTML = contentHTML.replace('/me ', ''); sendHtmlFn = this.client.sendHtmlEmote; sendTextFn = this.client.sendEmoteMessage; } 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({ 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}
diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index da8fc17001..b8a8e49769 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -68,48 +68,49 @@ 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); 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