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({
-
+
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/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