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

This commit is contained in:
David Baker 2017-03-21 13:44:55 +00:00
commit d5f7b3983b
18 changed files with 314 additions and 168 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

@ -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); views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = 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'; import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog); views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';

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

@ -280,6 +280,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'});
@ -840,6 +846,14 @@ module.exports = React.createClass({
return medium[0].toUpperCase() + medium.slice(1); return medium[0].toUpperCase() + medium.slice(1);
}, },
presentableTextForThreepid: function(threepid) {
if (threepid.medium == 'msisdn') {
return '+' + threepid.address;
} else {
return threepid.address;
}
},
render: function() { render: function() {
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
switch (this.state.phase) { switch (this.state.phase) {
@ -872,7 +886,9 @@ module.exports = React.createClass({
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label> <label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
</div> </div>
<div className="mx_UserSettings_profileInputCell"> <div className="mx_UserSettings_profileInputCell">
<input type="text" key={val.address} id={id} value={val.address} disabled /> <input type="text" key={val.address} id={id}
value={this.presentableTextForThreepid(val)} disabled
/>
</div> </div>
<div className="mx_UserSettings_threepidButton mx_filterFlipColor"> <div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} /> <img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />

View file

@ -196,7 +196,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

@ -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 (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk }
title={title}
>
<div className="mx_Dialog_content">
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.
</div>
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass} onClick={this.onOk}>
Redact
</button>
<button onClick={this.onCancel}>
Cancel
</button>
</div>
</BaseDialog>
);
},
});

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

@ -24,7 +24,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const text = this.props.mxEvent.getContent().body; const text = this.props.mxEvent.getContent().body;
return ( return (
<span className="mx_UnknownBody"> <span className="mx_UnknownBody" title="Redacted or unknown message type">
{text} {text}
</span> </span>
); );

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

@ -541,9 +541,9 @@ export default class MessageComposerInput extends React.Component {
let sendTextFn = this.client.sendTextMessage; let sendTextFn = this.client.sendTextMessage;
if (contentText.startsWith('/me')) { if (contentText.startsWith('/me')) {
contentText = contentText.replace('/me', ''); contentText = contentText.replace('/me ', '');
// bit of a hack, but the alternative would be quite complicated // 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; sendHtmlFn = this.client.sendHtmlEmote;
sendTextFn = this.client.sendEmoteMessage; sendTextFn = this.client.sendEmoteMessage;
} }

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