Merge branch 'develop' into kegan/indexeddb

This commit is contained in:
Kegan Dougal 2017-02-16 16:10:23 +00:00
commit 53f3b5780e
20 changed files with 623 additions and 329 deletions

View file

@ -13,6 +13,7 @@ module.exports = {
plugins: [ plugins: [
"react", "react",
"flowtype", "flowtype",
"babel"
], ],
env: { env: {
es6: true, es6: true,
@ -23,6 +24,11 @@ module.exports = {
} }
}, },
rules: { rules: {
// eslint's built in no-invalid-this rule breaks with class properties
"no-invalid-this": "off",
// so we replace it with a version that is class property aware
"babel/no-invalid-this": "error",
/** react **/ /** react **/
// This just uses the react plugin to help eslint known when // This just uses the react plugin to help eslint known when
// variables have been used in JSX // variables have been used in JSX

View file

@ -90,6 +90,7 @@
"babel-preset-react": "^6.11.1", "babel-preset-react": "^6.11.1",
"eslint": "^3.13.1", "eslint": "^3.13.1",
"eslint-config-google": "^0.7.1", "eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.0.1",
"eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^6.9.0", "eslint-plugin-react": "^6.9.0",
"expect": "^1.16.0", "expect": "^1.16.0",

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -24,6 +25,8 @@ import Presence from './Presence';
import dis from './dispatcher'; import dis from './dispatcher';
import DMRoomMap from './utils/DMRoomMap'; import DMRoomMap from './utils/DMRoomMap';
import RtsClient from './RtsClient'; import RtsClient from './RtsClient';
import Modal from './Modal';
import sdk from './index';
/** /**
* Called at startup, to attempt to build a logged-in Matrix session. It tries * Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -109,16 +112,17 @@ export function loadSession(opts) {
return q(); return q();
} }
if (_restoreFromLocalStorage()) { return _restoreFromLocalStorage().then((success) => {
return q(); if (success) {
} return;
}
if (enableGuest) { if (enableGuest) {
return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
} }
// fall back to login screen // fall back to login screen
return q(); });
} }
function _loginWithToken(queryParams, defaultDeviceDisplayName) { function _loginWithToken(queryParams, defaultDeviceDisplayName) {
@ -178,10 +182,11 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
}); });
} }
// returns true if a session is found in localstorage // returns a promise which resolves to true if a session is found in
// localstorage
function _restoreFromLocalStorage() { function _restoreFromLocalStorage() {
if (!localStorage) { if (!localStorage) {
return false; return q(false);
} }
const hs_url = localStorage.getItem("mx_hs_url"); const hs_url = localStorage.getItem("mx_hs_url");
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
@ -208,28 +213,55 @@ function _restoreFromLocalStorage() {
identityServerUrl: is_url, identityServerUrl: is_url,
guest: is_guest, guest: is_guest,
}); });
return true; return q(true);
} catch (e) { } catch (e) {
console.log("Unable to restore session", e); return _handleRestoreFailure(e);
var msg = e.message;
if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
msg = "You need to log back in to generate end-to-end encryption keys "
+ "for this device and submit the public key to your homeserver. "
+ "This is a once off; sorry for the inconvenience.";
}
// don't leak things into the new session
_clearLocalStorage();
throw new Error("Unable to restore previous session: " + msg);
} }
} else { } else {
console.log("No previous session found."); console.log("No previous session found.");
return false; return q(false);
} }
} }
function _handleRestoreFailure(e) {
console.log("Unable to restore session", e);
let msg = e.message;
if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
msg = "You need to log back in to generate end-to-end encryption keys "
+ "for this device and submit the public key to your homeserver. "
+ "This is a once off; sorry for the inconvenience.";
_clearLocalStorage();
return q.reject(new Error(
"Unable to restore previous session: " + msg,
));
}
const def = q.defer();
const SessionRestoreErrorDialog =
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
Modal.createDialog(SessionRestoreErrorDialog, {
error: msg,
onFinished: (success) => {
def.resolve(success);
},
});
return def.promise.then((success) => {
if (success) {
// user clicked continue.
_clearLocalStorage();
return false;
}
// try, try again
return _restoreFromLocalStorage();
});
}
let rtsClient = null; let rtsClient = null;
export function initRtsClient(url) { export function initRtsClient(url) {
rtsClient = new RtsClient(url); rtsClient = new RtsClient(url);

View file

@ -50,18 +50,18 @@ export default class RtsClient {
* Track a referral with the Riot Team Server. This should be called once a referred * Track a referral with the Riot Team Server. This should be called once a referred
* user has been successfully registered. * user has been successfully registered.
* @param {string} referrer the user ID of one who referred the user to Riot. * @param {string} referrer the user ID of one who referred the user to Riot.
* @param {string} userId the user ID of the user being referred. * @param {string} sid the sign-up identity server session ID .
* @param {string} userEmail the email address linked to `userId`. * @param {string} clientSecret the sign-up client secret.
* @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
* success. * success.
*/ */
trackReferral(referrer, userId, userEmail) { trackReferral(referrer, sid, clientSecret) {
return request(this._url + '/register', return request(this._url + '/register',
{ {
body: { body: {
referrer: referrer, referrer: referrer,
user_id: userId, session_id: sid,
user_email: userEmail, client_secret: clientSecret,
}, },
method: 'POST', method: 'POST',
} }

View file

@ -149,6 +149,7 @@ class EmailIdentityStage extends Stage {
nextLink nextLink
).then(function(response) { ).then(function(response) {
self.sid = response.sid; self.sid = response.sid;
self.signupInstance.setIdSid(self.sid);
return self._completeVerify(); return self._completeVerify();
}).then(function(request) { }).then(function(request) {
request.poll_for_success = true; request.poll_for_success = true;

View file

@ -26,7 +26,7 @@ var Notifier = require("./Notifier");
module.exports = { module.exports = {
LABS_FEATURES: [ LABS_FEATURES: [
{ {
name: 'Rich Text Editor', name: 'New Composer & Autocomplete',
id: 'rich_text_editor', id: 'rich_text_editor',
default: false, default: false,
}, },

View file

@ -31,6 +31,8 @@ import structures$CreateRoom from './components/structures/CreateRoom';
structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom); structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom);
import structures$FilePanel from './components/structures/FilePanel'; import structures$FilePanel from './components/structures/FilePanel';
structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel); structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel);
import structures$InteractiveAuth from './components/structures/InteractiveAuth';
structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth);
import structures$LoggedInView from './components/structures/LoggedInView'; import structures$LoggedInView from './components/structures/LoggedInView';
structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView); structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView);
import structures$MatrixChat from './components/structures/MatrixChat'; import structures$MatrixChat from './components/structures/MatrixChat';
@ -75,6 +77,8 @@ import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog); views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
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$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'; import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog); views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog'; import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
@ -85,6 +89,8 @@ import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedT
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog); views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog'; import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog); views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog);
import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog';
views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog);
import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog'; import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog';
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';

View file

@ -0,0 +1,152 @@
/*
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 Matrix from 'matrix-js-sdk';
const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react';
import sdk from '../../index';
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
export default React.createClass({
displayName: 'InteractiveAuth',
propTypes: {
// response from initial request. If not supplied, will do a request on
// mount.
authData: React.PropTypes.shape({
flows: React.PropTypes.array,
params: React.PropTypes.object,
session: React.PropTypes.string,
}),
// callback
makeRequest: React.PropTypes.func.isRequired,
// callback called when the auth process has finished
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param result The result of the authenticated call
onFinished: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
authStage: null,
busy: false,
errorText: null,
stageErrorText: null,
submitButtonEnabled: false,
};
},
componentWillMount: function() {
this._unmounted = false;
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
doRequest: this._requestCallback,
startAuthStage: this._startAuthStage,
});
this._authLogic.attemptAuth().then((result) => {
this.props.onFinished(true, result);
}).catch((error) => {
console.error("Error during user-interactive auth:", error);
if (this._unmounted) {
return;
}
const msg = error.message || error.toString();
this.setState({
errorText: msg
});
}).done();
},
componentWillUnmount: function() {
this._unmounted = true;
},
_startAuthStage: function(stageType, error) {
this.setState({
authStage: stageType,
errorText: error ? error.error : null,
}, this._setFocus);
},
_requestCallback: function(auth) {
this.setState({
busy: true,
errorText: null,
stageErrorText: null,
});
return this.props.makeRequest(auth).finally(() => {
if (this._unmounted) {
return;
}
this.setState({
busy: false,
});
});
},
_setFocus: function() {
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
this.refs.stageComponent.focus();
}
},
_submitAuthDict: function(authData) {
this._authLogic.submitAuthDict(authData);
},
_renderCurrentStage: function() {
const stage = this.state.authStage;
var StageComponent = getEntryComponentForLoginType(stage);
return (
<StageComponent ref="stageComponent"
loginType={stage}
authSessionId={this._authLogic.getSessionId()}
stageParams={this._authLogic.getStageParams(stage)}
submitAuthDict={this._submitAuthDict}
errorText={this.state.stageErrorText}
busy={this.state.busy}
/>
);
},
render: function() {
let error = null;
if (this.state.errorText) {
error = (
<div className="error">
{this.state.errorText}
</div>
);
}
return (
<div>
<div>
{this._renderCurrentStage()}
{error}
</div>
</div>
);
},
});

View file

@ -191,6 +191,17 @@ module.exports = React.createClass({
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
} }
// To enable things like riot.im/geektime in a nicer way than rewriting the URL
// and appending a team token query parameter, use the first path segment to
// indicate a team, with "public" team tokens stored in the config teamTokenMap.
let routedTeamToken = null;
if (this.props.config.teamTokenMap) {
const teamName = window.location.pathname.split('/')[1];
if (teamName && this.props.config.teamTokenMap.hasOwnProperty(teamName)) {
routedTeamToken = this.props.config.teamTokenMap[teamName];
}
}
// Persist the team token across refreshes using sessionStorage. A new window or // Persist the team token across refreshes using sessionStorage. A new window or
// tab will not persist sessionStorage, but refreshes will. // tab will not persist sessionStorage, but refreshes will.
if (this.props.startingFragmentQueryParams.team_token) { if (this.props.startingFragmentQueryParams.team_token) {
@ -202,8 +213,19 @@ module.exports = React.createClass({
// Use the locally-stored team token first, then as a fall-back, check to see if // Use the locally-stored team token first, then as a fall-back, check to see if
// a referral link was used, which will contain a query parameter `team_token`. // a referral link was used, which will contain a query parameter `team_token`.
this._teamToken = window.localStorage.getItem('mx_team_token') || this._teamToken = routedTeamToken ||
window.localStorage.getItem('mx_team_token') ||
window.sessionStorage.getItem('mx_team_token'); window.sessionStorage.getItem('mx_team_token');
// Some users have ended up with "undefined" as their local storage team token,
// treat that as undefined.
if (this._teamToken === "undefined") {
this._teamToken = undefined;
}
if (this._teamToken) {
console.info(`Team token set to ${this._teamToken}`);
}
}, },
componentDidMount: function() { componentDidMount: function() {
@ -888,14 +910,6 @@ module.exports = React.createClass({
onUserClick: function(event, userId) { onUserClick: function(event, userId) {
event.preventDefault(); event.preventDefault();
// var MemberInfo = sdk.getComponent('rooms.MemberInfo');
// var member = new Matrix.RoomMember(null, userId);
// ContextualMenu.createMenu(MemberInfo, {
// member: member,
// right: window.innerWidth - event.pageX,
// top: event.pageY
// });
var member = new Matrix.RoomMember(null, userId); var member = new Matrix.RoomMember(null, userId);
if (!member) { return; } if (!member) { return; }
dis.dispatch({ dis.dispatch({
@ -975,6 +989,11 @@ module.exports = React.createClass({
this._setPage(PageTypes.UserSettings); this._setPage(PageTypes.UserSettings);
}, },
onTeamMemberRegistered: function(teamToken) {
this._teamToken = teamToken;
this._setPage(PageTypes.HomePage);
},
onFinishPostRegistration: function() { onFinishPostRegistration: function() {
// Don't confuse this with "PageType" which is the middle window to show // Don't confuse this with "PageType" which is the middle window to show
this.setState({ this.setState({
@ -1103,6 +1122,7 @@ module.exports = React.createClass({
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl} registrationUrl={this.props.registrationUrl}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onTeamMemberRegistered={this.onTeamMemberRegistered}
onLoggedIn={this.onRegistered} onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick} onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}

View file

@ -223,8 +223,7 @@ module.exports = React.createClass({
users = users.slice(0, limit - 1); users = users.slice(0, limit - 1);
} }
let avatars = users.map((u, index) => { const avatars = users.map((u) => {
let showInitial = othersCount === 0 && index === users.length - 1;
return ( return (
<MemberAvatar <MemberAvatar
key={u.userId} key={u.userId}
@ -232,7 +231,6 @@ module.exports = React.createClass({
width={24} width={24}
height={24} height={24}
resizeMethod="crop" resizeMethod="crop"
defaultToInitialLetter={showInitial}
/> />
); );
}); });

View file

@ -58,6 +58,7 @@ module.exports = React.createClass({
teamServerURL: React.PropTypes.string.isRequired, teamServerURL: React.PropTypes.string.isRequired,
}), }),
teamSelected: React.PropTypes.object, teamSelected: React.PropTypes.object,
onTeamMemberRegistered: React.PropTypes.func.isRequired,
defaultDeviceDisplayName: React.PropTypes.string, defaultDeviceDisplayName: React.PropTypes.string,
@ -213,20 +214,22 @@ module.exports = React.createClass({
accessToken: response.access_token accessToken: response.access_token
}); });
if ( // Done regardless of `teamSelected`. People registering with non-team emails
self._rtsClient && // will just nop. The point of this being we might not have the email address
self.props.referrer && // that the user registered with at this stage (depending on whether this
self.state.teamSelected // is the client they initiated registration).
) { if (self._rtsClient) {
// Track referral, get team_token in order to retrieve team config // Track referral if self.props.referrer set, get team_token in order to
// retrieve team config and see welcome page etc.
self._rtsClient.trackReferral( self._rtsClient.trackReferral(
self.props.referrer, self.props.referrer || '', // Default to empty string = not referred
response.user_id, self.registerLogic.params.idSid,
self.state.formVals.email self.registerLogic.params.clientSecret
).then((data) => { ).then((data) => {
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);
self.props.onTeamMemberRegistered(teamToken);
self._rtsClient.getTeam(teamToken).then((team) => { self._rtsClient.getTeam(teamToken).then((team) => {
console.log( console.log(
@ -426,7 +429,12 @@ module.exports = React.createClass({
return ( return (
<div className="mx_Login"> <div className="mx_Login">
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/> <LoginHeader
icon={this.state.teamSelected ?
this.props.teamServerConfig.teamServerURL + "/static/common/" +
this.state.teamSelected.domain + "/icon.png" :
null}
/>
{this._getRegisterContentJsx()} {this._getRegisterContentJsx()}
<LoginFooter /> <LoginFooter />
</div> </div>

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import * as KeyCode from '../../../KeyCode'; import * as KeyCode from '../../../KeyCode';
import AccessibleButton from '../elements/AccessibleButton';
/** /**
* Basic container for modal dialogs. * Basic container for modal dialogs.
@ -59,9 +60,21 @@ export default React.createClass({
} }
}, },
_onCancelClick: function(e) {
this.props.onFinished();
},
render: function() { render: function() {
return ( return (
<div onKeyDown={this._onKeyDown} className={this.props.className}> <div onKeyDown={this._onKeyDown} className={this.props.className}>
<AccessibleButton onClick={this._onCancelClick}
className="mx_Dialog_cancelButton"
>
<img
src="img/cancel.svg" width="18" height="18"
alt="Cancel" title="Cancel"
/>
</AccessibleButton>
<div className='mx_Dialog_title'> <div className='mx_Dialog_title'>
{ this.props.title } { this.props.title }
</div> </div>

View file

@ -0,0 +1,83 @@
/*
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 an operation on another user.
* Takes a user ID and a verb, displays the target user prominently
* such that it should be easy to confirm that the operation is being
* performed on the right person, and displays the operation prominently
* to make it obvious what is going to happen.
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
export default React.createClass({
displayName: 'ConfirmUserActionDialog',
propTypes: {
member: React.PropTypes.object.isRequired, // matrix-js-sdk member object
action: React.PropTypes.string.isRequired, // eg. 'Ban'
danger: React.PropTypes.bool,
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 MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const title = this.props.action + " this person?";
const confirmButtonClass = classnames({
'mx_Dialog_primary': true,
'danger': this.props.danger,
});
return (
<BaseDialog className="mx_UserActionConfirmDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk }
title={title}
>
<div className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar">
<MemberAvatar member={this.props.member} width={72} height={72} />
</div>
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
</div>
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass} onClick={this.onOk} autoFocus={true}>
{this.props.action}
</button>
<button onClick={this.onCancel}>
Cancel
</button>
</div>
</BaseDialog>
);
},
});

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,13 +16,12 @@ limitations under the License.
*/ */
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import {getEntryComponentForLoginType} from '../login/InteractiveAuthEntryComponents'; import AccessibleButton from '../elements/AccessibleButton';
export default React.createClass({ export default React.createClass({
displayName: 'InteractiveAuthDialog', displayName: 'InteractiveAuthDialog',
@ -41,168 +41,29 @@ export default React.createClass({
onFinished: React.PropTypes.func.isRequired, onFinished: React.PropTypes.func.isRequired,
title: React.PropTypes.string, title: React.PropTypes.string,
submitButtonLabel: React.PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
title: "Authentication", title: "Authentication",
submitButtonLabel: "Submit",
}; };
}, },
getInitialState: function() {
return {
authStage: null,
busy: false,
errorText: null,
stageErrorText: null,
submitButtonEnabled: false,
};
},
componentWillMount: function() {
this._unmounted = false;
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
doRequest: this._requestCallback,
startAuthStage: this._startAuthStage,
});
this._authLogic.attemptAuth().then((result) => {
this.props.onFinished(true, result);
}).catch((error) => {
console.error("Error during user-interactive auth:", error);
if (this._unmounted) {
return;
}
const msg = error.message || error.toString();
this.setState({
errorText: msg
});
}).done();
},
componentWillUnmount: function() {
this._unmounted = true;
},
_startAuthStage: function(stageType, error) {
this.setState({
authStage: stageType,
errorText: error ? error.error : null,
}, this._setFocus);
},
_requestCallback: function(auth) {
this.setState({
busy: true,
errorText: null,
stageErrorText: null,
});
return this.props.makeRequest(auth).finally(() => {
if (this._unmounted) {
return;
}
this.setState({
busy: false,
});
});
},
_onEnterPressed: function(e) {
if (this.state.submitButtonEnabled && !this.state.busy) {
this._onSubmit();
}
},
_onSubmit: function() {
if (this.refs.stageComponent && this.refs.stageComponent.onSubmitClick) {
this.refs.stageComponent.onSubmitClick();
}
},
_setFocus: function() {
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
this.refs.stageComponent.focus();
}
},
_onCancel: function() {
this.props.onFinished(false);
},
_setSubmitButtonEnabled: function(enabled) {
this.setState({
submitButtonEnabled: enabled,
});
},
_submitAuthDict: function(authData) {
this._authLogic.submitAuthDict(authData);
},
_renderCurrentStage: function() {
const stage = this.state.authStage;
var StageComponent = getEntryComponentForLoginType(stage);
return (
<StageComponent ref="stageComponent"
loginType={stage}
authSessionId={this._authLogic.getSessionId()}
stageParams={this._authLogic.getStageParams(stage)}
submitAuthDict={this._submitAuthDict}
setSubmitButtonEnabled={this._setSubmitButtonEnabled}
errorText={this.state.stageErrorText}
/>
);
},
render: function() { render: function() {
const Loader = sdk.getComponent("elements.Spinner"); const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let error = null;
if (this.state.errorText) {
error = (
<div className="error">
{this.state.errorText}
</div>
);
}
const submitLabel = this.state.busy ? <Loader /> : this.props.submitButtonLabel;
const submitEnabled = this.state.submitButtonEnabled && !this.state.busy;
const submitButton = (
<button className="mx_Dialog_primary"
onClick={this._onSubmit}
disabled={!submitEnabled}
>
{submitLabel}
</button>
);
const cancelButton = (
<button onClick={this._onCancel}>
Cancel
</button>
);
return ( return (
<BaseDialog className="mx_InteractiveAuthDialog" <BaseDialog className="mx_InteractiveAuthDialog"
onEnterPressed={this._onEnterPressed}
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this.props.title} title={this.props.title}
> >
<div className="mx_Dialog_content"> <div>
<p>This operation requires additional authentication.</p> <InteractiveAuth ref={this._collectInteractiveAuth}
{this._renderCurrentStage()} authData={this.props.authData}
{error} makeRequest={this.props.makeRequest}
</div> onFinished={this.props.onFinished}
<div className="mx_Dialog_buttons"> />
{submitButton}
{cancelButton}
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -0,0 +1,74 @@
/*
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 SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
export default React.createClass({
displayName: 'SessionRestoreErrorDialog',
propTypes: {
error: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
},
_sendBugReport: function() {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createDialog(BugReportDialog, {});
},
_continueClicked: function() {
this.props.onFinished(true);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let bugreport;
if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = (
<p>Otherwise, <a onClick={this._sendBugReport} href='#'>
click here</a> to send a bug report.
</p>
);
}
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title='Unable to restore session'>
<div className="mx_Dialog_content">
<p>We encountered an error trying to restore your previous session. If
you continue, you will need to log in again, and encrypted chat
history will be unreadable.</p>
<p>If you have previously used a more recent version of Riot, your session
may be incompatible with this version. Close this window and return
to the more recent version.</p>
{bugreport}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
Continue anyway
</button>
</div>
</BaseDialog>
);
},
});

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -20,7 +21,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuthDialog to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
* for an auth stage. (The intention is that they could also be used for other * for an auth stage. (The intention is that they could also be used for other
* components, such as the registration flow). * components, such as the registration flow).
* *
@ -32,10 +33,10 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
* stageParams: params from the server for the stage being attempted * stageParams: params from the server for the stage being attempted
* errorText: error message from a previous attempt to authenticate * errorText: error message from a previous attempt to authenticate
* submitAuthDict: a function which will be called with the new auth dict * submitAuthDict: a function which will be called with the new auth dict
* setSubmitButtonEnabled: a function which will enable/disable the 'submit' button * busy: a boolean indicating whether the auth logic is doing something
* the user needs to wait for.
* *
* Each component may also provide the following functions (beyond the standard React ones): * Each component may also provide the following functions (beyond the standard React ones):
* onSubmitClick: handle a 'submit' button click
* focus: set the input focus appropriately in the form. * focus: set the input focus appropriately in the form.
*/ */
@ -48,12 +49,16 @@ export const PasswordAuthEntry = React.createClass({
propTypes: { propTypes: {
submitAuthDict: React.PropTypes.func.isRequired, submitAuthDict: React.PropTypes.func.isRequired,
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string, errorText: React.PropTypes.string,
// is the auth logic currently waiting for something to
// happen?
busy: React.PropTypes.bool,
}, },
componentWillMount: function() { getInitialState: function() {
this.props.setSubmitButtonEnabled(false); return {
passwordValid: false,
};
}, },
focus: function() { focus: function() {
@ -62,7 +67,10 @@ export const PasswordAuthEntry = React.createClass({
} }
}, },
onSubmitClick: function() { _onSubmit: function(e) {
e.preventDefault();
if (this.props.busy) return;
this.props.submitAuthDict({ this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE, type: PasswordAuthEntry.LOGIN_TYPE,
user: MatrixClientPeg.get().credentials.userId, user: MatrixClientPeg.get().credentials.userId,
@ -72,7 +80,9 @@ export const PasswordAuthEntry = React.createClass({
_onPasswordFieldChange: function(ev) { _onPasswordFieldChange: function(ev) {
// enable the submit button iff the password is non-empty // enable the submit button iff the password is non-empty
this.props.setSubmitButtonEnabled(Boolean(ev.target.value)); this.setState({
passwordValid: Boolean(this.refs.passwordField.value),
});
}, },
render: function() { render: function() {
@ -82,16 +92,34 @@ export const PasswordAuthEntry = React.createClass({
passwordBoxClass = 'error'; passwordBoxClass = 'error';
} }
let submitButtonOrSpinner;
if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner");
submitButtonOrSpinner = <Loader />;
} else {
submitButtonOrSpinner = (
<input type="submit"
className="mx_Dialog_primary"
disabled={!this.state.passwordValid}
/>
);
}
return ( return (
<div> <div>
<p>To continue, please enter your password.</p> <p>To continue, please enter your password.</p>
<p>Password:</p> <p>Password:</p>
<input <form onSubmit={this._onSubmit}>
ref="passwordField" <input
className={passwordBoxClass} ref="passwordField"
onChange={this._onPasswordFieldChange} className={passwordBoxClass}
type="password" onChange={this._onPasswordFieldChange}
/> type="password"
/>
<div className="mx_button_row">
{submitButtonOrSpinner}
</div>
</form>
<div className="error"> <div className="error">
{this.props.errorText} {this.props.errorText}
</div> </div>
@ -110,14 +138,9 @@ export const RecaptchaAuthEntry = React.createClass({
propTypes: { propTypes: {
submitAuthDict: React.PropTypes.func.isRequired, submitAuthDict: React.PropTypes.func.isRequired,
stageParams: React.PropTypes.object.isRequired, stageParams: React.PropTypes.object.isRequired,
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string, errorText: React.PropTypes.string,
}, },
componentWillMount: function() {
this.props.setSubmitButtonEnabled(false);
},
_onCaptchaResponse: function(response) { _onCaptchaResponse: function(response) {
this.props.submitAuthDict({ this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE, type: RecaptchaAuthEntry.LOGIN_TYPE,
@ -148,7 +171,6 @@ export const FallbackAuthEntry = React.createClass({
authSessionId: React.PropTypes.string.isRequired, authSessionId: React.PropTypes.string.isRequired,
loginType: React.PropTypes.string.isRequired, loginType: React.PropTypes.string.isRequired,
submitAuthDict: React.PropTypes.func.isRequired, submitAuthDict: React.PropTypes.func.isRequired,
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string, errorText: React.PropTypes.string,
}, },
@ -156,7 +178,6 @@ export const FallbackAuthEntry = React.createClass({
// we have to make the user click a button, as browsers will block // we have to make the user click a button, as browsers will block
// the popup if we open it immediately. // the popup if we open it immediately.
this._popupWindow = null; this._popupWindow = null;
this.props.setSubmitButtonEnabled(true);
window.addEventListener("message", this._onReceiveMessage); window.addEventListener("message", this._onReceiveMessage);
}, },
@ -167,13 +188,12 @@ export const FallbackAuthEntry = React.createClass({
} }
}, },
onSubmitClick: function() { _onShowFallbackClick: function() {
var url = MatrixClientPeg.get().getFallbackAuthUrl( var url = MatrixClientPeg.get().getFallbackAuthUrl(
this.props.loginType, this.props.loginType,
this.props.authSessionId this.props.authSessionId
); );
this._popupWindow = window.open(url); this._popupWindow = window.open(url);
this.props.setSubmitButtonEnabled(false);
}, },
_onReceiveMessage: function(event) { _onReceiveMessage: function(event) {
@ -188,7 +208,7 @@ export const FallbackAuthEntry = React.createClass({
render: function() { render: function() {
return ( return (
<div> <div>
Click "Submit" to authenticate <a onClick={this._onShowFallbackClick}>Start authentication</a>
<div className="error"> <div className="error">
{this.props.errorText} {this.props.errorText}
</div> </div>

View file

@ -222,7 +222,8 @@ module.exports = React.createClass({
title: "Add an Integration", title: "Add an Integration",
description: description:
<div> <div>
You are about to taken to a third-party site so you can authenticate your account for use with {integrationsUrl}.<br/> You are about to be taken to a third-party site so you can
authenticate your account for use with {integrationsUrl}.<br/>
Do you wish to continue? Do you wish to continue?
</div>, </div>,
button: "Continue", button: "Continue",

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -25,16 +26,16 @@ limitations under the License.
* 'muted': boolean, * 'muted': boolean,
* 'isTargetMod': boolean * 'isTargetMod': boolean
*/ */
var React = require('react'); import React from 'react';
var classNames = require('classnames'); import classNames from 'classnames';
var dis = require("../../../dispatcher"); import dis from '../../../dispatcher';
var Modal = require("../../../Modal"); import Modal from '../../../Modal';
var sdk = require('../../../index'); import sdk from '../../../index';
var createRoom = require('../../../createRoom'); import createRoom from '../../../createRoom';
var DMRoomMap = require('../../../utils/DMRoomMap'); import DMRoomMap from '../../../utils/DMRoomMap';
var Unread = require('../../../Unread'); import Unread from '../../../Unread';
var Receipt = require('../../../utils/Receipt'); import { findReadReceiptFromUserId } from '../../../utils/Receipt';
var WithMatrixClient = require('../../../wrappers/WithMatrixClient'); import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
module.exports = WithMatrixClient(React.createClass({ module.exports = WithMatrixClient(React.createClass({
@ -43,13 +44,6 @@ module.exports = WithMatrixClient(React.createClass({
propTypes: { propTypes: {
matrixClient: React.PropTypes.object.isRequired, matrixClient: React.PropTypes.object.isRequired,
member: React.PropTypes.object.isRequired, member: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func,
},
getDefaultProps: function() {
return {
onFinished: function() {}
};
}, },
getInitialState: function() { getInitialState: function() {
@ -164,7 +158,7 @@ module.exports = WithMatrixClient(React.createClass({
onRoomReceipt: function(receiptEvent, room) { onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count // because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us // only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, this.props.matrixClient.credentials.userId)) { if (findReadReceiptFromUserId(receiptEvent, this.props.matrixClient.credentials.userId)) {
this.forceUpdate(); this.forceUpdate();
} }
}, },
@ -224,46 +218,72 @@ module.exports = WithMatrixClient(React.createClass({
}, },
onKick: function() { onKick: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
var roomId = this.props.member.roomId; Modal.createDialog(ConfirmUserActionDialog, {
var target = this.props.member.userId; member: this.props.member,
this.setState({ updating: this.state.updating + 1 }); action: 'Kick',
this.props.matrixClient.kick(roomId, target).then(function() { danger: true,
// NO-OP; rely on the m.room.member event coming down else we could onFinished: (proceed) => {
// get out of sync if we force setState here! if (!proceed) return;
console.log("Kick success");
}, function(err) { this.setState({ updating: this.state.updating + 1 });
Modal.createDialog(ErrorDialog, { this.props.matrixClient.kick(
title: "Kick error", this.props.member.roomId, this.props.member.userId,
description: err.message ).then(function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Kick success");
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Kick error",
description: err.message
});
}
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
}); });
} }
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
}); });
this.props.onFinished();
}, },
onBan: function() { onBanOrUnban: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
var roomId = this.props.member.roomId; Modal.createDialog(ConfirmUserActionDialog, {
var target = this.props.member.userId; member: this.props.member,
this.setState({ updating: this.state.updating + 1 }); action: this.props.member.membership == 'ban' ? 'Unban' : 'Ban',
this.props.matrixClient.ban(roomId, target).then( danger: this.props.member.membership != 'ban',
function() { onFinished: (proceed) => {
// NO-OP; rely on the m.room.member event coming down else we could if (!proceed) return;
// get out of sync if we force setState here!
console.log("Ban success"); this.setState({ updating: this.state.updating + 1 });
}, function(err) { let promise;
Modal.createDialog(ErrorDialog, { if (this.props.member.membership == 'ban') {
title: "Ban error", promise = this.props.matrixClient.unban(
description: err.message this.props.member.roomId, this.props.member.userId,
);
} else {
promise = this.props.matrixClient.ban(
this.props.member.roomId, this.props.member.userId,
);
}
promise.then(
function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Ban success");
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Ban error",
description: err.message,
});
}
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
}); });
} },
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
}); });
this.props.onFinished();
}, },
onMuteToggle: function() { onMuteToggle: function() {
@ -272,14 +292,12 @@ module.exports = WithMatrixClient(React.createClass({
var target = this.props.member.userId; var target = this.props.member.userId;
var room = this.props.matrixClient.getRoom(roomId); var room = this.props.matrixClient.getRoom(roomId);
if (!room) { if (!room) {
this.props.onFinished();
return; return;
} }
var powerLevelEvent = room.currentState.getStateEvents( var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", ""
); );
if (!powerLevelEvent) { if (!powerLevelEvent) {
this.props.onFinished();
return; return;
} }
var isMuted = this.state.muted; var isMuted = this.state.muted;
@ -314,7 +332,6 @@ module.exports = WithMatrixClient(React.createClass({
this.setState({ updating: this.state.updating - 1 }); this.setState({ updating: this.state.updating - 1 });
}); });
} }
this.props.onFinished();
}, },
onModToggle: function() { onModToggle: function() {
@ -323,19 +340,16 @@ module.exports = WithMatrixClient(React.createClass({
var target = this.props.member.userId; var target = this.props.member.userId;
var room = this.props.matrixClient.getRoom(roomId); var room = this.props.matrixClient.getRoom(roomId);
if (!room) { if (!room) {
this.props.onFinished();
return; return;
} }
var powerLevelEvent = room.currentState.getStateEvents( var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", ""
); );
if (!powerLevelEvent) { if (!powerLevelEvent) {
this.props.onFinished();
return; return;
} }
var me = room.getMember(this.props.matrixClient.credentials.userId); var me = room.getMember(this.props.matrixClient.credentials.userId);
if (!me) { if (!me) {
this.props.onFinished();
return; return;
} }
var defaultLevel = powerLevelEvent.getContent().users_default; var defaultLevel = powerLevelEvent.getContent().users_default;
@ -366,7 +380,6 @@ module.exports = WithMatrixClient(React.createClass({
).finally(()=>{ ).finally(()=>{
this.setState({ updating: this.state.updating - 1 }); this.setState({ updating: this.state.updating - 1 });
}); });
this.props.onFinished();
}, },
_applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) {
@ -386,7 +399,6 @@ module.exports = WithMatrixClient(React.createClass({
).finally(()=>{ ).finally(()=>{
this.setState({ updating: this.state.updating - 1 }); this.setState({ updating: this.state.updating - 1 });
}).done(); }).done();
this.props.onFinished();
}, },
onPowerChange: function(powerLevel) { onPowerChange: function(powerLevel) {
@ -396,14 +408,12 @@ module.exports = WithMatrixClient(React.createClass({
var room = this.props.matrixClient.getRoom(roomId); var room = this.props.matrixClient.getRoom(roomId);
var self = this; var self = this;
if (!room) { if (!room) {
this.props.onFinished();
return; return;
} }
var powerLevelEvent = room.currentState.getStateEvents( var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", ""
); );
if (!powerLevelEvent) { if (!powerLevelEvent) {
this.props.onFinished();
return; return;
} }
if (powerLevelEvent.getContent().users) { if (powerLevelEvent.getContent().users) {
@ -422,9 +432,6 @@ module.exports = WithMatrixClient(React.createClass({
if (confirmed) { if (confirmed) {
self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
} }
else {
self.props.onFinished();
}
}, },
}); });
} }
@ -440,7 +447,6 @@ module.exports = WithMatrixClient(React.createClass({
onNewDMClick: function() { onNewDMClick: function() {
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
createRoom({dmUserId: this.props.member.userId}).finally(() => { createRoom({dmUserId: this.props.member.userId}).finally(() => {
this.props.onFinished();
this.setState({ updating: this.state.updating - 1 }); this.setState({ updating: this.state.updating - 1 });
}).done(); }).done();
}, },
@ -450,30 +456,29 @@ module.exports = WithMatrixClient(React.createClass({
action: 'leave_room', action: 'leave_room',
room_id: this.props.member.roomId, room_id: this.props.member.roomId,
}); });
this.props.onFinished();
}, },
_calculateOpsPermissions: function(member) { _calculateOpsPermissions: function(member) {
var defaultPerms = { const defaultPerms = {
can: {}, can: {},
muted: false, muted: false,
modifyLevel: false modifyLevel: false
}; };
var room = this.props.matrixClient.getRoom(member.roomId); const room = this.props.matrixClient.getRoom(member.roomId);
if (!room) { if (!room) {
return defaultPerms; return defaultPerms;
} }
var powerLevels = room.currentState.getStateEvents( const powerLevels = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", ""
); );
if (!powerLevels) { if (!powerLevels) {
return defaultPerms; return defaultPerms;
} }
var me = room.getMember(this.props.matrixClient.credentials.userId); const me = room.getMember(this.props.matrixClient.credentials.userId);
if (!me) { if (!me) {
return defaultPerms; return defaultPerms;
} }
var them = member; const them = member;
return { return {
can: this._calculateCanPermissions( can: this._calculateCanPermissions(
me, them, powerLevels.getContent() me, them, powerLevels.getContent()
@ -484,22 +489,22 @@ module.exports = WithMatrixClient(React.createClass({
}, },
_calculateCanPermissions: function(me, them, powerLevels) { _calculateCanPermissions: function(me, them, powerLevels) {
var can = { const can = {
kick: false, kick: false,
ban: false, ban: false,
mute: false, mute: false,
modifyLevel: false modifyLevel: false
}; };
var canAffectUser = them.powerLevel < me.powerLevel; const canAffectUser = them.powerLevel < me.powerLevel;
if (!canAffectUser) { if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can; return can;
} }
var editPowerLevel = ( const editPowerLevel = (
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default powerLevels.state_default
); );
var levelToSend = ( const levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) || (powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default powerLevels.events_default
); );
@ -646,10 +651,14 @@ module.exports = WithMatrixClient(React.createClass({
); );
} }
if (this.state.can.ban) { if (this.state.can.ban) {
let label = 'Ban';
if (this.props.member.membership == 'ban') {
label = 'Unban';
}
banButton = ( banButton = (
<AccessibleButton className="mx_MemberInfo_field" <AccessibleButton className="mx_MemberInfo_field"
onClick={this.onBan}> onClick={this.onBanOrUnban}>
Ban {label}
</AccessibleButton> </AccessibleButton>
); );
} }

View file

@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var q = require("q"); import q from 'q';
var React = require('react'); import React from 'react';
var MatrixClientPeg = require('../../../MatrixClientPeg'); import MatrixClientPeg from '../../../MatrixClientPeg';
var SdkConfig = require('../../../SdkConfig'); import SdkConfig from '../../../SdkConfig';
var sdk = require('../../../index'); import sdk from '../../../index';
var Modal = require('../../../Modal'); import Modal from '../../../Modal';
var ObjectUtils = require("../../../ObjectUtils"); import ObjectUtils from '../../../ObjectUtils';
var dis = require("../../../dispatcher"); import dis from '../../../dispatcher';
var ScalarAuthClient = require("../../../ScalarAuthClient"); import ScalarAuthClient from '../../../ScalarAuthClient';
var ScalarMessaging = require('../../../ScalarMessaging'); import ScalarMessaging from '../../../ScalarMessaging';
var UserSettingsStore = require('../../../UserSettingsStore'); import UserSettingsStore from '../../../UserSettingsStore';
import AccessibleButton from '../elements/AccessibleButton';
// parse a string as an integer; if the input is undefined, or cannot be parsed // parse a string as an integer; if the input is undefined, or cannot be parsed
@ -635,16 +636,16 @@ module.exports = React.createClass({
if (myMember) { if (myMember) {
if (myMember.membership === "join") { if (myMember.membership === "join") {
leaveButton = ( leaveButton = (
<div className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }> <AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }>
Leave room Leave room
</div> </AccessibleButton>
); );
} }
else if (myMember.membership === "leave") { else if (myMember.membership === "leave") {
leaveButton = ( leaveButton = (
<div className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }> <AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }>
Forget room Forget room
</div> </AccessibleButton>
); );
} }
} }

View file

@ -67,16 +67,24 @@ describe('InteractiveAuthDialog', function () {
onFinished={onFinished} onFinished={onFinished}
/>, parentDiv); />, parentDiv);
// at this point there should be a password box // at this point there should be a password box and a submit button
const passwordNode = ReactTestUtils.findRenderedDOMComponentWithTag( const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form");
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
dlg, "input" dlg, "input"
); );
expect(passwordNode.type).toEqual("password"); 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();
// submit should be disabled // submit should be disabled
const submitNode = ReactTestUtils.findRenderedDOMComponentWithClass(
dlg, "mx_Dialog_primary"
);
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
@ -84,9 +92,7 @@ describe('InteractiveAuthDialog', function () {
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.keyDown(passwordNode, { ReactTestUtils.Simulate.submit(formNode, {});
key: "Enter", keyCode: 13, which: 13,
});
expect(doRequest.callCount).toEqual(1); expect(doRequest.callCount).toEqual(1);
expect(doRequest.calledWithExactly({ expect(doRequest.calledWithExactly({
@ -96,8 +102,10 @@ describe('InteractiveAuthDialog', function () {
user: "@user:id", user: "@user:id",
})).toBe(true); })).toBe(true);
// the submit button should now be disabled (and be a spinner) // there should now be a spinner
expect(submitNode.disabled).toBe(true); ReactTestUtils.findRenderedComponentWithType(
dlg, sdk.getComponent('elements.Spinner'),
);
// let the request complete // let the request complete
q.delay(1).then(() => { q.delay(1).then(() => {