Merge remote-tracking branch 'origin/develop' into dbkr/threepid_display

This commit is contained in:
David Baker 2017-03-17 13:55:07 +00:00
commit 92d5f55a46
14 changed files with 225 additions and 164 deletions

View file

@ -18,13 +18,17 @@ import dis from './dispatcher';
import sdk from './index'; import sdk from './index';
import Modal from './Modal'; import Modal from './Modal';
let isDialogOpen = false;
const onAction = function(payload) { const onAction = function(payload) {
if (payload.action === 'unknown_device_error') { if (payload.action === 'unknown_device_error' && !isDialogOpen) {
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog"); var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
isDialogOpen = true;
Modal.createDialog(UnknownDeviceDialog, { Modal.createDialog(UnknownDeviceDialog, {
devices: payload.err.devices, devices: payload.err.devices,
room: payload.room, room: payload.room,
onFinished: (r) => { onFinished: (r) => {
isDialogOpen = false;
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('UnknownDeviceDialog closed with '+r); console.log('UnknownDeviceDialog closed with '+r);

View file

@ -63,6 +63,13 @@ module.exports = React.createClass({
// called when the session load completes // called when the session load completes
onLoadCompleted: React.PropTypes.func, 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 // displayname, if any, to set on the device when logging
// in/registering. // in/registering.
defaultDeviceDisplayName: React.PropTypes.string, defaultDeviceDisplayName: React.PropTypes.string,
@ -89,6 +96,12 @@ module.exports = React.createClass({
var s = { var s = {
loading: true, loading: true,
screen: undefined, 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 // What the LoggedInView would be showing if visible
page_type: null, page_type: null,
@ -184,11 +197,6 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
SdkConfig.put(this.props.config); 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 // if the automatic session load failed, the error
this.sessionLoadError = null; this.sessionLoadError = null;
@ -317,14 +325,13 @@ module.exports = React.createClass({
}, },
onAction: function(payload) { onAction: function(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var roomIndexDelta = 1; var roomIndexDelta = 1;
var self = this; var self = this;
switch (payload.action) { switch (payload.action) {
case 'logout': case 'logout':
if (MatrixClientPeg.get().isGuest()) {
this.guestCreds = MatrixClientPeg.getCredentials();
}
Lifecycle.logout(); Lifecycle.logout();
break; break;
case 'start_registration': case 'start_registration':
@ -344,7 +351,13 @@ module.exports = React.createClass({
this.notifyNewScreen('register'); this.notifyNewScreen('register');
break; break;
case 'start_login': case 'start_login':
if (this.state.logged_in) return; if (MatrixClientPeg.get() &&
MatrixClientPeg.get().isGuest()
) {
this.setState({
guestCreds: MatrixClientPeg.getCredentials(),
});
}
this.setStateForNewScreen({ this.setStateForNewScreen({
screen: 'login', screen: 'login',
}); });
@ -359,8 +372,8 @@ module.exports = React.createClass({
// also stash our credentials, then if we restore the session, // also stash our credentials, then if we restore the session,
// we can just do it the same way whether we started upgrade // we can just do it the same way whether we started upgrade
// registration or explicitly logged out // registration or explicitly logged out
this.guestCreds = MatrixClientPeg.getCredentials();
this.setStateForNewScreen({ this.setStateForNewScreen({
guestCreds: MatrixClientPeg.getCredentials(),
screen: "register", screen: "register",
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
guestAccessToken: MatrixClientPeg.get().getAccessToken(), guestAccessToken: MatrixClientPeg.get().getAccessToken(),
@ -382,25 +395,23 @@ module.exports = React.createClass({
this.notifyNewScreen('forgot_password'); this.notifyNewScreen('forgot_password');
break; break;
case 'leave_room': case 'leave_room':
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var roomId = payload.room_id;
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Leave room", title: "Leave room",
description: "Are you sure you want to leave the room?", description: "Are you sure you want to leave the room?",
onFinished: function(should_leave) { onFinished: (should_leave) => {
if (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 :( // FIXME: controller shouldn't be loading a view :(
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then(function() { d.then(() => {
modal.close(); modal.close();
dis.dispatch({action: 'view_next_room'}); if (this.currentRoomId === payload.room_id) {
}, function(err) { dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
modal.close(); modal.close();
console.error("Failed to leave room " + payload.room_id + " " + err); console.error("Failed to leave room " + payload.room_id + " " + err);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
@ -412,6 +423,32 @@ module.exports = React.createClass({
} }
}); });
break; 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': case 'view_user':
// FIXME: ugly hack to expand the RightPanel and then re-dispatch. // FIXME: ugly hack to expand the RightPanel and then re-dispatch.
if (this.state.collapse_rhs) { if (this.state.collapse_rhs) {
@ -659,6 +696,14 @@ module.exports = React.createClass({
_onLoadCompleted: function() { _onLoadCompleted: function() {
this.props.onLoadCompleted(); this.props.onLoadCompleted();
this.setState({loading: false}); 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 * Called when a new logged in session has started
*/ */
_onLoggedIn: function(teamToken) { _onLoggedIn: function(teamToken) {
this.guestCreds = null;
this.notifyNewScreen('');
this.setState({ this.setState({
screen: undefined, guestCreds: null,
logged_in: true, logged_in: true,
}); });
if (teamToken) { if (teamToken) {
this._teamToken = teamToken; this._teamToken = teamToken;
this._setPage(PageTypes.HomePage); dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) { } 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() cli.getRooms()
)[0].roomId; )[0].roomId;
self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView}); 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 { } else {
self.setState({ready: true, page_type: PageTypes.RoomView}); self.setState({ready: true, page_type: PageTypes.RoomView});
@ -791,16 +845,7 @@ module.exports = React.createClass({
if (presentedId != undefined) { if (presentedId != undefined) {
self.notifyNewScreen('room/'+presentedId); 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'}); dis.dispatch({action: 'focus_composer'});
} else { } else {
self.setState({ready: true}); self.setState({ready: true});
@ -1003,9 +1048,9 @@ module.exports = React.createClass({
onReturnToGuestClick: function() { onReturnToGuestClick: function() {
// reanimate our guest login // reanimate our guest login
if (this.guestCreds) { if (this.state.guestCreds) {
Lifecycle.setLoggedIn(this.guestCreds); Lifecycle.setLoggedIn(this.state.guestCreds);
this.guestCreds = null; this.setState({guestCreds: null});
} }
}, },
@ -1154,7 +1199,7 @@ module.exports = React.createClass({
onLoggedIn={this.onRegistered} onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick} onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
/> />
); );
} else if (this.state.screen == 'forgot_password') { } else if (this.state.screen == 'forgot_password') {
@ -1181,7 +1226,7 @@ module.exports = React.createClass({
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick} onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest} enableGuest={this.props.enableGuest}
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
initialErrorText={this.sessionLoadError} initialErrorText={this.sessionLoadError}
/> />
); );

View file

@ -413,7 +413,7 @@ module.exports = React.createClass({
var continuation = false; var continuation = false;
if (prevEvent !== null if (prevEvent !== null
&& !prevEvent.isRedacted() && prevEvent.sender && mxEv.sender && prevEvent.sender && mxEv.sender
&& mxEv.sender.userId === prevEvent.sender.userId && mxEv.sender.userId === prevEvent.sender.userId
&& mxEv.getType() == prevEvent.getType()) { && mxEv.getType() == prevEvent.getType()) {
continuation = true; continuation = true;

View file

@ -915,8 +915,6 @@ module.exports = React.createClass({
}, },
uploadFile: function(file) { uploadFile: function(file) {
var self = this;
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
@ -928,8 +926,16 @@ module.exports = React.createClass({
ContentMessages.sendContentToRoom( ContentMessages.sendContentToRoom(
file, this.state.room.roomId, MatrixClientPeg.get() file, this.state.room.roomId, MatrixClientPeg.get()
).done(undefined, function(error) { ).done(undefined, (error) => {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); 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); console.error("Failed to upload file " + file + " " + error);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failed to upload file", title: "Failed to upload file",

View file

@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling. // The amount of extra scroll distance to allow prior to unfilling.
// See _getExcessHeight. // See _getExcessHeight.
const UNPAGINATION_PADDING = 3000; const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent // The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests. // many scroll events causing many unfilling requests.
const UNFILL_REQUEST_DEBOUNCE_MS = 200; const UNFILL_REQUEST_DEBOUNCE_MS = 200;

View file

@ -268,6 +268,12 @@ module.exports = React.createClass({
but for now be warned. but for now be warned.
</div>, </div>,
button: "Sign out", button: "Sign out",
extraButtons: [
<button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
</button>
],
onFinished: (confirmed) => { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
dis.dispatch({action: 'logout'}); dis.dispatch({action: 'logout'});

View file

@ -185,7 +185,6 @@ module.exports = React.createClass({
const teamToken = data.team_token; const teamToken = data.team_token;
// Store for use /w welcome pages // Store for use /w welcome pages
window.localStorage.setItem('mx_team_token', teamToken); window.localStorage.setItem('mx_team_token', teamToken);
this.props.onTeamMemberRegistered(teamToken);
this._rtsClient.getTeam(teamToken).then((team) => { this._rtsClient.getTeam(teamToken).then((team) => {
console.log( console.log(

View file

@ -18,7 +18,7 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import Velocity from 'velocity-vector'; import Velocity from 'velocity-vector';
export default class DeactivateAccountDialog extends React.Component { export default class DeactivateAccountDialog extends React.Component {

View file

@ -21,10 +21,8 @@ export default React.createClass({
displayName: 'QuestionDialog', displayName: 'QuestionDialog',
propTypes: { propTypes: {
title: React.PropTypes.string, title: React.PropTypes.string,
description: React.PropTypes.oneOfType([ description: React.PropTypes.node,
React.PropTypes.element, extraButtons: React.PropTypes.node,
React.PropTypes.string,
]),
button: React.PropTypes.string, button: React.PropTypes.string,
focus: React.PropTypes.bool, focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired, onFinished: React.PropTypes.func.isRequired,
@ -34,6 +32,7 @@ export default React.createClass({
return { return {
title: "", title: "",
description: "", description: "",
extraButtons: null,
button: "OK", button: "OK",
focus: true, focus: true,
hasCancelButton: true, hasCancelButton: true,
@ -67,6 +66,7 @@ export default React.createClass({
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}> <button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button} {this.props.button}
</button> </button>
{this.props.extraButtons}
{cancelButton} {cancelButton}
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -435,10 +435,7 @@ module.exports = WithMatrixClient(React.createClass({
let avatarSize; let avatarSize;
let needsSenderProfile; let needsSenderProfile;
if (isRedacted) { if (this.props.tileShape === "notif") {
avatarSize = 0;
needsSenderProfile = false;
} else if (this.props.tileShape === "notif") {
avatarSize = 24; avatarSize = 24;
needsSenderProfile = true; needsSenderProfile = true;
} else if (isInfoMessage) { } else if (isInfoMessage) {
@ -503,8 +500,8 @@ module.exports = WithMatrixClient(React.createClass({
else if (e2eEnabled) { else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>; e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
} }
const timestamp = this.props.mxEvent.isRedacted() ? const timestamp = this.props.mxEvent.getTs() ?
null : <MessageTimestamp ts={this.props.mxEvent.getTs()} />; <MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
if (this.props.tileShape === "notif") { if (this.props.tileShape === "notif") {
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());

View file

@ -485,11 +485,12 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] } <RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
label="People" label="People"
editable={ false } editable={ true }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
alwaysShowHeader={ true }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />

View file

@ -56,8 +56,7 @@ module.exports = React.createClass({
return({ return({
hover : false, hover : false,
badgeHover : false, badgeHover : false,
notificationTagMenu: false, menuDisplayed: false,
roomTagMenu: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
}); });
}, },
@ -136,62 +135,32 @@ module.exports = React.createClass({
this.setState({ hover: false }); this.setState({ hover: false });
} }
var NotificationStateMenu = sdk.getComponent('context_menus.NotificationStateContextMenu'); var RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
var elementRect = e.target.getBoundingClientRect(); var elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page // The window X and Y offsets are to adjust position when zoomed in to page
var x = elementRect.right + window.pageXOffset + 3; const x = elementRect.right + window.pageXOffset + 3;
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53; 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; var self = this;
ContextualMenu.createMenu(NotificationStateMenu, { ContextualMenu.createMenu(RoomTileContextMenu, {
menuWidth: 188, chevronOffset: chevronOffset,
menuHeight: 126,
chevronOffset: 45,
left: x, left: x,
top: y, top: y,
room: this.props.room, room: this.props.room,
onFinished: function() { onFinished: function() {
self.setState({ notificationTagMenu: false }); self.setState({ menuDisplayed: false });
self.props.refreshSubList(); self.props.refreshSubList();
} }
}); });
this.setState({ notificationTagMenu: true }); this.setState({ menuDisplayed: true });
} }
// Prevent the RoomTile onClick event firing as well // Prevent the RoomTile onClick event firing as well
e.stopPropagation(); 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() { render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId; var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId]; var me = this.props.room.currentState.members[myUserId];
@ -210,7 +179,7 @@ module.exports = React.createClass({
'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'), 'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_notificationTagMenu': this.state.notificationTagMenu, 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_noBadges': !badges, 'mx_RoomTile_noBadges': !badges,
}); });
@ -218,14 +187,9 @@ module.exports = React.createClass({
'mx_RoomTile_avatar': true, 'mx_RoomTile_avatar': true,
}); });
var avatarContainerClasses = classNames({
'mx_RoomTile_avatar_container': true,
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
});
var badgeClasses = classNames({ var badgeClasses = classNames({
'mx_RoomTile_badge': true, '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 // XXX: We should never display raw room IDs, but sometimes the
@ -236,7 +200,7 @@ module.exports = React.createClass({
var badge; var badge;
var badgeContent; var badgeContent;
if (this.state.badgeHover || this.state.notificationTagMenu) { if (this.state.badgeHover || this.state.menuDisplayed) {
badgeContent = "\u00B7\u00B7\u00B7"; badgeContent = "\u00B7\u00B7\u00B7";
} else if (badges) { } else if (badges) {
var limitedCount = FormattingUtils.formatCount(notificationCount); var limitedCount = FormattingUtils.formatCount(notificationCount);
@ -254,7 +218,7 @@ module.exports = React.createClass({
var nameClasses = classNames({ var nameClasses = classNames({
'mx_RoomTile_name': true, 'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite, '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) { if (this.props.selected) {
@ -293,11 +257,9 @@ module.exports = React.createClass({
<div> { /* Only native elements can be wrapped in a DnD object. */} <div> { /* Only native elements can be wrapped in a DnD object. */}
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}> <div className={avatarClasses}>
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}> <div className="mx_RoomTile_avatar_container">
<div className={avatarContainerClasses}> <RoomAvatar room={this.props.room} width={24} height={24} />
<RoomAvatar room={this.props.room} width={24} height={24} /> {directMessageIndicator}
{directMessageIndicator}
</div>
</div> </div>
</div> </div>
<div className="mx_RoomTile_nameContainer"> <div className="mx_RoomTile_nameContainer">

View file

@ -68,48 +68,49 @@ describe('InteractiveAuthDialog', function () {
onFinished={onFinished} onFinished={onFinished}
/>, parentDiv); />, parentDiv);
// at this point there should be a password box and a submit button // wait for a password box and a submit button
const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form"); test_utils.waitForRenderedDOMComponentWithTag(dlg, "form").then((formNode) => {
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag( const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
dlg, "input" dlg, "input"
); );
let passwordNode; let passwordNode;
let submitNode; let submitNode;
for (const node of inputNodes) { for (const node of inputNodes) {
if (node.type == 'password') { if (node.type == 'password') {
passwordNode = node; passwordNode = node;
} else if (node.type == 'submit') { } else if (node.type == 'submit') {
submitNode = node; submitNode = node;
}
} }
} expect(passwordNode).toExist();
expect(passwordNode).toExist(); expect(submitNode).toExist();
expect(submitNode).toExist();
// submit should be disabled // submit should be disabled
expect(submitNode.disabled).toBe(true); expect(submitNode.disabled).toBe(true);
// put something in the password box, and hit enter; that should // put something in the password box, and hit enter; that should
// trigger a request // trigger a request
passwordNode.value = "s3kr3t"; passwordNode.value = "s3kr3t";
ReactTestUtils.Simulate.change(passwordNode); ReactTestUtils.Simulate.change(passwordNode);
expect(submitNode.disabled).toBe(false); expect(submitNode.disabled).toBe(false);
ReactTestUtils.Simulate.submit(formNode, {}); ReactTestUtils.Simulate.submit(formNode, {});
expect(doRequest.callCount).toEqual(1); expect(doRequest.callCount).toEqual(1);
expect(doRequest.calledWithExactly({ expect(doRequest.calledWithExactly({
session: "sess", session: "sess",
type: "m.login.password", type: "m.login.password",
password: "s3kr3t", password: "s3kr3t",
user: "@user:id", user: "@user:id",
})).toBe(true); })).toBe(true);
// there should now be a spinner // there should now be a spinner
ReactTestUtils.findRenderedComponentWithType( ReactTestUtils.findRenderedComponentWithType(
dlg, sdk.getComponent('elements.Spinner'), dlg, sdk.getComponent('elements.Spinner'),
); );
// let the request complete // let the request complete
q.delay(1).then(() => { return q.delay(1);
}).then(() => {
expect(onFinished.callCount).toEqual(1); expect(onFinished.callCount).toEqual(1);
expect(onFinished.calledWithExactly(true, {a:1})).toBe(true); expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
}).done(done, done); }).done(done, done);

View file

@ -1,11 +1,51 @@
"use strict"; "use strict";
var sinon = require('sinon'); import sinon from 'sinon';
var q = require('q'); import q from 'q';
import ReactTestUtils from 'react-addons-test-utils';
var peg = require('../src/MatrixClientPeg.js'); import peg from '../src/MatrixClientPeg.js';
var jssdk = require('matrix-js-sdk'); import jssdk from 'matrix-js-sdk';
var MatrixEvent = jssdk.MatrixEvent; 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 * Perform common actions before each test case, e.g. printing the test case