Merge branch 'develop' into matthew/warn-unknown-devices

This commit is contained in:
Matthew Hodgson 2017-02-01 22:34:55 +00:00
commit c09d173415
20 changed files with 416 additions and 176 deletions

View file

@ -53,7 +53,11 @@ module.exports = {
* things that are errors in the js-sdk config that the current * things that are errors in the js-sdk config that the current
* code does not adhere to, turned down to warn * code does not adhere to, turned down to warn
*/ */
"max-len": ["warn"], "max-len": ["warn", {
// apparently people believe the length limit shouldn't apply
// to JSX.
ignorePattern: '^\\s*<',
}],
"valid-jsdoc": ["warn"], "valid-jsdoc": ["warn"],
"new-cap": ["warn"], "new-cap": ["warn"],
"key-spacing": ["warn"], "key-spacing": ["warn"],

View file

@ -165,6 +165,14 @@ module.exports = function (config) {
}, },
devtool: 'inline-source-map', devtool: 'inline-source-map',
}, },
webpackMiddleware: {
stats: {
// don't fill the console up with a mahoosive list of modules
chunks: false,
},
},
browserNoActivityTimeout: 15000, browserNoActivityTimeout: 15000,
}); });
}; };

80
src/RtsClient.js Normal file
View file

@ -0,0 +1,80 @@
import 'whatwg-fetch';
function checkStatus(response) {
if (!response.ok) {
return response.text().then((text) => {
throw new Error(text);
});
}
return response;
}
function parseJson(response) {
return response.json();
}
function encodeQueryParams(params) {
return '?' + Object.keys(params).map((k) => {
return k + '=' + encodeURIComponent(params[k]);
}).join('&');
}
const request = (url, opts) => {
if (opts && opts.qs) {
url += encodeQueryParams(opts.qs);
delete opts.qs;
}
if (opts && opts.body) {
if (!opts.headers) {
opts.headers = {};
}
opts.body = JSON.stringify(opts.body);
opts.headers['Content-Type'] = 'application/json';
}
return fetch(url, opts)
.then(checkStatus)
.then(parseJson);
};
export default class RtsClient {
constructor(url) {
this._url = url;
}
getTeamsConfig() {
return request(this._url + '/teams');
}
/**
* Track a referral with the Riot Team Server. This should be called once a referred
* user has been successfully registered.
* @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} userEmail the email address linked to `userId`.
* @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
* success.
*/
trackReferral(referrer, userId, userEmail) {
return request(this._url + '/register',
{
body: {
referrer: referrer,
user_id: userId,
user_email: userEmail,
},
method: 'POST',
}
);
}
getTeam(teamToken) {
return request(this._url + '/teamConfiguration',
{
qs: {
team_token: teamToken,
},
}
);
}
}

View file

@ -71,7 +71,7 @@ export default React.createClass({
return this.props.matrixClient.exportRoomKeys(); return this.props.matrixClient.exportRoomKeys();
}).then((k) => { }).then((k) => {
return MegolmExportEncryption.encryptMegolmKeyFile( return MegolmExportEncryption.encryptMegolmKeyFile(
JSON.stringify(k), passphrase JSON.stringify(k), passphrase,
); );
}).then((f) => { }).then((f) => {
const blob = new Blob([f], { const blob = new Blob([f], {
@ -95,9 +95,14 @@ export default React.createClass({
}); });
}, },
_onCancelClick: function(ev) {
ev.preventDefault();
this.props.onFinished(false);
return false;
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const disableForm = (this.state.phase === PHASE_EXPORTING); const disableForm = (this.state.phase === PHASE_EXPORTING);
@ -159,10 +164,9 @@ export default React.createClass({
<input className='mx_Dialog_primary' type='submit' value='Export' <input className='mx_Dialog_primary' type='submit' value='Export'
disabled={disableForm} disabled={disableForm}
/> />
<AccessibleButton element='button' onClick={this.props.onFinished} <button onClick={this._onCancelClick} disabled={disableForm}>
disabled={disableForm}>
Cancel Cancel
</AccessibleButton> </button>
</div> </div>
</form> </form>
</BaseDialog> </BaseDialog>

View file

@ -80,7 +80,7 @@ export default React.createClass({
return readFileAsArrayBuffer(file).then((arrayBuffer) => { return readFileAsArrayBuffer(file).then((arrayBuffer) => {
return MegolmExportEncryption.decryptMegolmKeyFile( return MegolmExportEncryption.decryptMegolmKeyFile(
arrayBuffer, passphrase arrayBuffer, passphrase,
); );
}).then((keys) => { }).then((keys) => {
return this.props.matrixClient.importRoomKeys(JSON.parse(keys)); return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
@ -98,9 +98,14 @@ export default React.createClass({
}); });
}, },
_onCancelClick: function(ev) {
ev.preventDefault();
this.props.onFinished(false);
return false;
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const disableForm = (this.state.phase !== PHASE_EDIT); const disableForm = (this.state.phase !== PHASE_EDIT);
@ -158,10 +163,9 @@ export default React.createClass({
<input className='mx_Dialog_primary' type='submit' value='Import' <input className='mx_Dialog_primary' type='submit' value='Import'
disabled={!this.state.enableSubmit || disableForm} disabled={!this.state.enableSubmit || disableForm}
/> />
<AccessibleButton element='button' onClick={this.props.onFinished} <button onClick={this._onCancelClick} disabled={disableForm}>
disabled={disableForm}>
Cancel Cancel
</AccessibleButton> </button>
</div> </div>
</form> </form>
</BaseDialog> </BaseDialog>

View file

@ -171,6 +171,7 @@ export default React.createClass({
brand={this.props.config.brand} brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs} enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>; if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break; break;

View file

@ -1055,12 +1055,13 @@ module.exports = React.createClass({
sessionId={this.state.register_session_id} sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid} idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email} email={this.props.startingFragmentQueryParams.email}
referrer={this.props.startingFragmentQueryParams.referrer}
username={this.state.upgradeUsername} username={this.state.upgradeUsername}
guestAccessToken={this.state.guestAccessToken} guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.getDefaultHsUrl()} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()} defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand} brand={this.props.config.brand}
teamsConfig={this.props.config.teamsConfig} teamServerConfig={this.props.config.teamServerConfig}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl} registrationUrl={this.props.registrationUrl}

View file

@ -1332,12 +1332,14 @@ module.exports = React.createClass({
}, },
onStatusBarVisible: function() { onStatusBarVisible: function() {
if (this.unmounted) return;
this.setState({ this.setState({
statusBarVisible: true, statusBarVisible: true,
}); });
}, },
onStatusBarHidden: function() { onStatusBarHidden: function() {
if (this.unmounted) return;
this.setState({ this.setState({
statusBarVisible: false, statusBarVisible: false,
}); });
@ -1507,13 +1509,14 @@ module.exports = React.createClass({
}); });
var statusBar; var statusBar;
let isStatusAreaExpanded = true;
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar'); var UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />; statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) { } else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
tabComplete={this.tabComplete} tabComplete={this.tabComplete}
@ -1683,7 +1686,7 @@ module.exports = React.createClass({
); );
} }
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable"; let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
if (this.state.statusBarVisible) { if (isStatusAreaExpanded) {
statusBarAreaClass += " mx_RoomView_statusArea_expanded"; statusBarAreaClass += " mx_RoomView_statusArea_expanded";
} }

View file

@ -570,7 +570,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect(); var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("Scrolling to token '" + node.dataset.scrollToken + "'+" + debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")"); pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) { if(scrollDelta != 0) {
@ -582,7 +582,7 @@ module.exports = React.createClass({
_saveScrollState: function() { _saveScrollState: function() {
if (this.props.stickyBottom && this.isAtBottom()) { if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true }; this.scrollState = { stuckAtBottom: true };
debuglog("Saved scroll state", this.scrollState); debuglog("ScrollPanel: Saved scroll state", this.scrollState);
return; return;
} }
@ -601,12 +601,12 @@ module.exports = React.createClass({
trackedScrollToken: node.dataset.scrollToken, trackedScrollToken: node.dataset.scrollToken,
pixelOffset: wrapperRect.bottom - boundingRect.bottom, pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}; };
debuglog("Saved scroll state", this.scrollState); debuglog("ScrollPanel: saved scroll state", this.scrollState);
return; return;
} }
} }
debuglog("Unable to save scroll state: found no children in the viewport"); debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
}, },
_restoreSavedScrollState: function() { _restoreSavedScrollState: function() {
@ -640,7 +640,7 @@ module.exports = React.createClass({
this._lastSetScroll = scrollNode.scrollTop; this._lastSetScroll = scrollNode.scrollTop;
} }
debuglog("Set scrollTop:", scrollNode.scrollTop, debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
"requested:", scrollTop, "requested:", scrollTop,
"_lastSetScroll:", this._lastSetScroll); "_lastSetScroll:", this._lastSetScroll);
}, },

View file

@ -92,6 +92,9 @@ module.exports = React.createClass({
// True to show the 'labs' section of experimental features // True to show the 'labs' section of experimental features
enableLabs: React.PropTypes.bool, enableLabs: React.PropTypes.bool,
// The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: React.PropTypes.string,
// true if RightPanel is collapsed // true if RightPanel is collapsed
collapsedRhs: React.PropTypes.bool, collapsedRhs: React.PropTypes.bool,
}, },
@ -444,6 +447,27 @@ module.exports = React.createClass({
); );
}, },
_renderReferral: function() {
const teamToken = window.localStorage.getItem('mx_team_token');
if (!teamToken) {
return null;
}
if (typeof teamToken !== 'string') {
console.warn('Team token not a string');
return null;
}
const href = (this.props.referralBaseUrl || window.location.origin) +
`/#/register?referrer=${this._me}&team_token=${teamToken}`;
return (
<div>
<h3>Referral</h3>
<div className="mx_UserSettings_section">
Refer a friend to Riot: <a href={href}>{href}</a>
</div>
</div>
);
},
_renderUserInterfaceSettings: function() { _renderUserInterfaceSettings: function() {
var client = MatrixClientPeg.get(); var client = MatrixClientPeg.get();
@ -819,6 +843,8 @@ module.exports = React.createClass({
{accountJsx} {accountJsx}
</div> </div>
{this._renderReferral()}
{notification_area} {notification_area}
{this._renderUserInterfaceSettings()} {this._renderUserInterfaceSettings()}

View file

@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var RegistrationForm = require("../../views/login/RegistrationForm"); var RegistrationForm = require("../../views/login/RegistrationForm");
var CaptchaForm = require("../../views/login/CaptchaForm"); var CaptchaForm = require("../../views/login/CaptchaForm");
var RtsClient = require("../../../RtsClient");
var MIN_PASSWORD_LENGTH = 6; var MIN_PASSWORD_LENGTH = 6;
@ -47,23 +48,16 @@ module.exports = React.createClass({
defaultIsUrl: React.PropTypes.string, defaultIsUrl: React.PropTypes.string,
brand: React.PropTypes.string, brand: React.PropTypes.string,
email: React.PropTypes.string, email: React.PropTypes.string,
referrer: React.PropTypes.string,
username: React.PropTypes.string, username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string, guestAccessToken: React.PropTypes.string,
teamsConfig: React.PropTypes.shape({ teamServerConfig: React.PropTypes.shape({
// Email address to request new teams // Email address to request new teams
supportEmail: React.PropTypes.string, supportEmail: React.PropTypes.string.isRequired,
teams: React.PropTypes.arrayOf(React.PropTypes.shape({ // URL of the riot-team-server to get team configurations and track referrals
// The displayed name of the team teamServerURL: React.PropTypes.string.isRequired,
"name": React.PropTypes.string,
// The suffix with which every team email address ends
"emailSuffix": React.PropTypes.string,
// The rooms to use during auto-join
"rooms": React.PropTypes.arrayOf(React.PropTypes.shape({
"id": React.PropTypes.string,
"autoJoin": React.PropTypes.bool,
})),
})).required,
}), }),
teamSelected: React.PropTypes.object,
defaultDeviceDisplayName: React.PropTypes.string, defaultDeviceDisplayName: React.PropTypes.string,
@ -75,6 +69,7 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
busy: false, busy: false,
teamServerBusy: false,
errorText: null, errorText: null,
// We remember the values entered by the user because // We remember the values entered by the user because
// the registration form will be unmounted during the // the registration form will be unmounted during the
@ -90,6 +85,7 @@ module.exports = React.createClass({
}, },
componentWillMount: function() { componentWillMount: function() {
this._unmounted = false;
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
// attach this to the instance rather than this.state since it isn't UI // attach this to the instance rather than this.state since it isn't UI
this.registerLogic = new Signup.Register( this.registerLogic = new Signup.Register(
@ -103,10 +99,40 @@ module.exports = React.createClass({
this.registerLogic.setIdSid(this.props.idSid); this.registerLogic.setIdSid(this.props.idSid);
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken); this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
this.registerLogic.recheckState(); this.registerLogic.recheckState();
if (
this.props.teamServerConfig &&
this.props.teamServerConfig.teamServerURL &&
!this._rtsClient
) {
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
this.setState({
teamServerBusy: true,
});
// GET team configurations including domains, names and icons
this._rtsClient.getTeamsConfig().then((data) => {
const teamsConfig = {
teams: data,
supportEmail: this.props.teamServerConfig.supportEmail,
};
console.log('Setting teams config to ', teamsConfig);
this.setState({
teamsConfig: teamsConfig,
teamServerBusy: false,
});
}, (err) => {
console.error('Error retrieving config for teams', err);
this.setState({
teamServerBusy: false,
});
});
}
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
this._unmounted = true;
}, },
componentDidMount: function() { componentDidMount: function() {
@ -184,24 +210,41 @@ module.exports = React.createClass({
accessToken: response.access_token accessToken: response.access_token
}); });
// Auto-join rooms if (
if (self.props.teamsConfig && self.props.teamsConfig.teams) { self._rtsClient &&
for (let i = 0; i < self.props.teamsConfig.teams.length; i++) { self.props.referrer &&
let team = self.props.teamsConfig.teams[i]; self.state.teamSelected
if (self.state.formVals.email.endsWith(team.emailSuffix)) { ) {
console.log("User successfully registered with team " + team.name); // Track referral, get team_token in order to retrieve team config
self._rtsClient.trackReferral(
self.props.referrer,
response.user_id,
self.state.formVals.email
).then((data) => {
const teamToken = data.team_token;
// Store for use /w welcome pages
window.localStorage.setItem('mx_team_token', teamToken);
self._rtsClient.getTeam(teamToken).then((team) => {
console.log(
`User successfully registered with team ${team.name}`
);
if (!team.rooms) { if (!team.rooms) {
break; return;
} }
// Auto-join rooms
team.rooms.forEach((room) => { team.rooms.forEach((room) => {
if (room.autoJoin) { if (room.auto_join && room.room_id) {
console.log("Auto-joining " + room.id); console.log(`Auto-joining ${room.room_id}`);
MatrixClientPeg.get().joinRoom(room.id); MatrixClientPeg.get().joinRoom(room.room_id);
} }
}); });
break; }, (err) => {
} console.error('Error getting team config', err);
} });
}, (err) => {
console.error('Error tracking referral', err);
});
} }
if (self.props.brand) { if (self.props.brand) {
@ -273,7 +316,15 @@ module.exports = React.createClass({
}); });
}, },
onTeamSelected: function(teamSelected) {
if (!this._unmounted) {
this.setState({ teamSelected });
}
},
_getRegisterContentJsx: function() { _getRegisterContentJsx: function() {
const Spinner = sdk.getComponent("elements.Spinner");
var currStep = this.registerLogic.getStep(); var currStep = this.registerLogic.getStep();
var registerStep; var registerStep;
switch (currStep) { switch (currStep) {
@ -283,17 +334,23 @@ module.exports = React.createClass({
case "Register.STEP_m.login.dummy": case "Register.STEP_m.login.dummy":
// NB. Our 'username' prop is specifically for upgrading // NB. Our 'username' prop is specifically for upgrading
// a guest account // a guest account
if (this.state.teamServerBusy) {
registerStep = <Spinner />;
break;
}
registerStep = ( registerStep = (
<RegistrationForm <RegistrationForm
showEmail={true} showEmail={true}
defaultUsername={this.state.formVals.username} defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email} defaultEmail={this.state.formVals.email}
defaultPassword={this.state.formVals.password} defaultPassword={this.state.formVals.password}
teamsConfig={this.props.teamsConfig} teamsConfig={this.state.teamsConfig}
guestUsername={this.props.username} guestUsername={this.props.username}
minPasswordLength={MIN_PASSWORD_LENGTH} minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed} onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit} /> onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected}
/>
); );
break; break;
case "Register.STEP_m.login.email.identity": case "Register.STEP_m.login.email.identity":
@ -322,7 +379,6 @@ module.exports = React.createClass({
} }
var busySpinner; var busySpinner;
if (this.state.busy) { if (this.state.busy) {
var Spinner = sdk.getComponent("elements.Spinner");
busySpinner = ( busySpinner = (
<Spinner /> <Spinner />
); );
@ -367,7 +423,7 @@ 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 /> <LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
{this._getRegisterContentJsx()} {this._getRegisterContentJsx()}
<LoginFooter /> <LoginFooter />
</div> </div>

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 React = require("react"); import React from 'react';
var classNames = require('classnames'); import classNames from 'classnames';
var sdk = require("../../../index"); import sdk from '../../../index';
var Invite = require("../../../Invite"); import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
var createRoom = require("../../../createRoom"); import createRoom from '../../../createRoom';
var MatrixClientPeg = require("../../../MatrixClientPeg"); import MatrixClientPeg from '../../../MatrixClientPeg';
var DMRoomMap = require('../../../utils/DMRoomMap'); import DMRoomMap from '../../../utils/DMRoomMap';
var rate_limited_func = require("../../../ratelimitedfunc"); import rate_limited_func from '../../../ratelimitedfunc';
var dis = require("../../../dispatcher"); import dis from '../../../dispatcher';
var Modal = require('../../../Modal'); import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import q from 'q';
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
@ -186,13 +187,17 @@ module.exports = React.createClass({
// If the query isn't a user we know about, but is a // If the query isn't a user we know about, but is a
// valid address, add an entry for that // valid address, add an entry for that
if (queryList.length == 0) { if (queryList.length == 0) {
const addrType = Invite.getAddressType(query); const addrType = getAddressType(query);
if (addrType !== null) { if (addrType !== null) {
queryList.push({ queryList[0] = {
addressType: addrType, addressType: addrType,
address: query, address: query,
isKnown: false, isKnown: false,
}); };
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
} }
} }
} }
@ -212,6 +217,7 @@ module.exports = React.createClass({
inviteList: inviteList, inviteList: inviteList,
queryList: [], queryList: [],
}); });
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
}; };
}, },
@ -229,6 +235,7 @@ module.exports = React.createClass({
inviteList: inviteList, inviteList: inviteList,
queryList: [], queryList: [],
}); });
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
}, },
_getDirectMessageRoom: function(addr) { _getDirectMessageRoom: function(addr) {
@ -266,7 +273,7 @@ module.exports = React.createClass({
if (this.props.roomId) { if (this.props.roomId) {
// Invite new user to a room // Invite new user to a room
var self = this; var self = this;
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts) inviteMultipleToRoom(this.props.roomId, addrTexts)
.then(function(addrs) { .then(function(addrs) {
var room = MatrixClientPeg.get().getRoom(self.props.roomId); var room = MatrixClientPeg.get().getRoom(self.props.roomId);
return self._showAnyInviteErrors(addrs, room); return self._showAnyInviteErrors(addrs, room);
@ -300,7 +307,7 @@ module.exports = React.createClass({
var room; var room;
createRoom().then(function(roomId) { createRoom().then(function(roomId) {
room = MatrixClientPeg.get().getRoom(roomId); room = MatrixClientPeg.get().getRoom(roomId);
return Invite.inviteMultipleToRoom(roomId, addrTexts); return inviteMultipleToRoom(roomId, addrTexts);
}) })
.then(function(addrs) { .then(function(addrs) {
return self._showAnyInviteErrors(addrs, room); return self._showAnyInviteErrors(addrs, room);
@ -380,7 +387,7 @@ module.exports = React.createClass({
}, },
_isDmChat: function(addrs) { _isDmChat: function(addrs) {
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) { if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
return true; return true;
} else { } else {
return false; return false;
@ -408,7 +415,7 @@ module.exports = React.createClass({
_addInputToList: function() { _addInputToList: function() {
const addressText = this.refs.textinput.value.trim(); const addressText = this.refs.textinput.value.trim();
const addrType = Invite.getAddressType(addressText); const addrType = getAddressType(addressText);
const addrObj = { const addrObj = {
addressType: addrType, addressType: addrType,
address: addressText, address: addressText,
@ -432,9 +439,45 @@ module.exports = React.createClass({
inviteList: inviteList, inviteList: inviteList,
queryList: [], queryList: [],
}); });
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList; return inviteList;
}, },
_lookupThreepid: function(medium, address) {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
// leave it: it's replacing the old one each time so it's
// not like they leak.
this._cancelThreepidLookup = function() {
cancelled = true;
}
// wait a bit to let the user finish typing
return q.delay(500).then(() => {
if (cancelled) return null;
return MatrixClientPeg.get().lookupThreePid(medium, address);
}).then((res) => {
if (res === null || !res.mxid) return null;
if (cancelled) return null;
return MatrixClientPeg.get().getProfileInfo(res.mxid);
}).then((res) => {
if (res === null) return null;
if (cancelled) return null;
this.setState({
queryList: [{
// an InviteAddressType
addressType: medium,
address: address,
displayName: res.displayname,
avatarMxc: res.avatar_url,
isKnown: true,
}]
});
});
},
render: function() { render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
const AddressSelector = sdk.getComponent("elements.AddressSelector"); const AddressSelector = sdk.getComponent("elements.AddressSelector");

View file

@ -94,14 +94,14 @@ export default React.createClass({
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
let info;
let error = false;
if (address.addressType === "mx" && address.isKnown) {
const nameClasses = classNames({ const nameClasses = classNames({
"mx_AddressTile_name": true, "mx_AddressTile_name": true,
"mx_AddressTile_justified": this.props.justified, "mx_AddressTile_justified": this.props.justified,
}); });
let info;
let error = false;
if (address.addressType === "mx" && address.isKnown) {
const idClasses = classNames({ const idClasses = classNames({
"mx_AddressTile_id": true, "mx_AddressTile_id": true,
"mx_AddressTile_justified": this.props.justified, "mx_AddressTile_justified": this.props.justified,
@ -123,13 +123,21 @@ export default React.createClass({
<div className={unknownMxClasses}>{ this.props.address.address }</div> <div className={unknownMxClasses}>{ this.props.address.address }</div>
); );
} else if (address.addressType === "email") { } else if (address.addressType === "email") {
var emailClasses = classNames({ const emailClasses = classNames({
"mx_AddressTile_email": true, "mx_AddressTile_email": true,
"mx_AddressTile_justified": this.props.justified, "mx_AddressTile_justified": this.props.justified,
}); });
let nameNode = null;
if (address.displayName) {
nameNode = <div className={nameClasses}>{ address.displayName }</div>
}
info = ( info = (
<div className="mx_AddressTile_mx">
<div className={emailClasses}>{ address.address }</div> <div className={emailClasses}>{ address.address }</div>
{nameNode}
</div>
); );
} else { } else {
error = true; error = true;

View file

@ -44,8 +44,8 @@ module.exports = React.createClass({
teams: React.PropTypes.arrayOf(React.PropTypes.shape({ teams: React.PropTypes.arrayOf(React.PropTypes.shape({
// The displayed name of the team // The displayed name of the team
"name": React.PropTypes.string, "name": React.PropTypes.string,
// The suffix with which every team email address ends // The domain of team email addresses
"emailSuffix": React.PropTypes.string, "domain": React.PropTypes.string,
})).required, })).required,
}), }),
@ -117,9 +117,6 @@ module.exports = React.createClass({
_doSubmit: function() { _doSubmit: function() {
let email = this.refs.email.value.trim(); let email = this.refs.email.value.trim();
if (this.state.selectedTeam) {
email += "@" + this.state.selectedTeam.emailSuffix;
}
var promise = this.props.onRegisterClick({ var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.guestUsername, username: this.refs.username.value.trim() || this.props.guestUsername,
password: this.refs.password.value.trim(), password: this.refs.password.value.trim(),
@ -134,25 +131,6 @@ module.exports = React.createClass({
} }
}, },
onSelectTeam: function(teamIndex) {
let team = this._getSelectedTeam(teamIndex);
if (team) {
this.refs.email.value = this.refs.email.value.split("@")[0];
}
this.setState({
selectedTeam: team,
showSupportEmail: teamIndex === "other",
});
},
_getSelectedTeam: function(teamIndex) {
if (this.props.teamsConfig &&
this.props.teamsConfig.teams[teamIndex]) {
return this.props.teamsConfig.teams[teamIndex];
}
return null;
},
/** /**
* Returns true if all fields were valid last time * Returns true if all fields were valid last time
* they were validated. * they were validated.
@ -167,20 +145,36 @@ module.exports = React.createClass({
return true; return true;
}, },
_isUniEmail: function(email) {
return email.endsWith('.ac.uk') || email.endsWith('.edu');
},
validateField: function(field_id) { validateField: function(field_id) {
var pwd1 = this.refs.password.value.trim(); var pwd1 = this.refs.password.value.trim();
var pwd2 = this.refs.passwordConfirm.value.trim(); var pwd2 = this.refs.passwordConfirm.value.trim();
switch (field_id) { switch (field_id) {
case FIELD_EMAIL: case FIELD_EMAIL:
let email = this.refs.email.value; const email = this.refs.email.value;
if (this.props.teamsConfig) { if (this.props.teamsConfig && this._isUniEmail(email)) {
let team = this.state.selectedTeam; const matchingTeam = this.props.teamsConfig.teams.find(
if (team) { (team) => {
email = email + "@" + team.emailSuffix; return email.split('@').pop() === team.domain;
} }
) || null;
this.setState({
selectedTeam: matchingTeam,
showSupportEmail: !matchingTeam,
});
this.props.onTeamSelected(matchingTeam);
} else {
this.props.onTeamSelected(null);
this.setState({
selectedTeam: null,
showSupportEmail: false,
});
} }
let valid = email === '' || Email.looksValid(email); const valid = email === '' || Email.looksValid(email);
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID"); this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
break; break;
case FIELD_USERNAME: case FIELD_USERNAME:
@ -260,61 +254,35 @@ module.exports = React.createClass({
return cls; return cls;
}, },
_renderEmailInputSuffix: function() {
let suffix = null;
if (!this.state.selectedTeam) {
return suffix;
}
let team = this.state.selectedTeam;
if (team) {
suffix = "@" + team.emailSuffix;
}
return suffix;
},
render: function() { render: function() {
var self = this; var self = this;
var emailSection, teamSection, teamAdditionSupport, registerButton; var emailSection, belowEmailSection, registerButton;
if (this.props.showEmail) { if (this.props.showEmail) {
let emailSuffix = this._renderEmailInputSuffix();
emailSection = ( emailSection = (
<div>
<input type="text" ref="email" <input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)" autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.props.defaultEmail} defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}} onBlur={function() {self.validateField(FIELD_EMAIL);}}
value={self.state.email}/> value={self.state.email}/>
{emailSuffix ? <input className="mx_Login_field" value={emailSuffix} disabled/> : null }
</div>
); );
if (this.props.teamsConfig) { if (this.props.teamsConfig) {
teamSection = (
<select
defaultValue="-1"
className="mx_Login_field"
onBlur={function() {self.validateField(FIELD_EMAIL);}}
onChange={function(ev) {self.onSelectTeam(ev.target.value);}}
>
<option key="-1" value="-1">No team</option>
{this.props.teamsConfig.teams.map((t, index) => {
return (
<option key={index} value={index}>
{t.name}
</option>
);
})}
<option key="-2" value="other">Other</option>
</select>
);
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
teamAdditionSupport = ( belowEmailSection = (
<span> <p className="mx_Login_support">
If your team is not listed, email&nbsp; Sorry, but your university is not registered with us just yet.&nbsp;
Email us on&nbsp;
<a href={"mailto:" + this.props.teamsConfig.supportEmail}> <a href={"mailto:" + this.props.teamsConfig.supportEmail}>
{this.props.teamsConfig.supportEmail} {this.props.teamsConfig.supportEmail}
</a> </a>&nbsp;
</span> to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
</p>
);
} else if (this.state.selectedTeam) {
belowEmailSection = (
<p className="mx_Login_support">
You are registering with {this.state.selectedTeam.name}
</p>
); );
} }
} }
@ -333,11 +301,8 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
{teamSection}
{teamAdditionSupport}
<br />
{emailSection} {emailSection}
<br /> {belowEmailSection}
<input type="text" ref="username" <input type="text" ref="username"
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername} placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')} className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}

View file

@ -50,7 +50,7 @@ export function decryptMegolmKeyFile(data, password) {
} }
const ciphertextLength = body.length-(1+16+16+4+32); const ciphertextLength = body.length-(1+16+16+4+32);
if (body.length < 0) { if (ciphertextLength < 0) {
throw new Error('Invalid file: too short'); throw new Error('Invalid file: too short');
} }
@ -107,14 +107,14 @@ export function encryptMegolmKeyFile(data, password, options) {
const salt = new Uint8Array(16); const salt = new Uint8Array(16);
window.crypto.getRandomValues(salt); window.crypto.getRandomValues(salt);
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of salt is a price we have to pay.
salt[9] &= 0x7f;
const iv = new Uint8Array(16); const iv = new Uint8Array(16);
window.crypto.getRandomValues(iv); window.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[9] &= 0x7f;
return deriveKeys(salt, kdf_rounds, password).then((keys) => { return deriveKeys(salt, kdf_rounds, password).then((keys) => {
const [aes_key, hmac_key] = keys; const [aes_key, hmac_key] = keys;

View file

@ -42,17 +42,12 @@ describe('RoomView', function () {
it('resolves a room alias to a room id', function (done) { it('resolves a room alias to a room id', function (done) {
peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"})); peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
var onRoomIdResolved = sinon.spy(); function onRoomIdResolved(room_id) {
expect(room_id).toEqual("!randomcharacters:aser.ver");
done();
}
ReactDOM.render(<RoomView roomAddress="#alias:ser.ver" onRoomIdResolved={onRoomIdResolved} />, parentDiv); ReactDOM.render(<RoomView roomAddress="#alias:ser.ver" onRoomIdResolved={onRoomIdResolved} />, parentDiv);
process.nextTick(function() {
// These expect()s don't read very well and don't give very good failure
// messages, but expect's toHaveBeenCalled only takes an expect spy object,
// not a sinon spy object.
expect(onRoomIdResolved.called).toExist();
done();
});
}); });
it('joins by alias if given an alias', function (done) { it('joins by alias if given an alias', function (done) {

View file

@ -73,6 +73,7 @@ var Tester = React.createClass({
/* returns a promise which will resolve when the fill happens */ /* returns a promise which will resolve when the fill happens */
awaitFill: function(dir) { awaitFill: function(dir) {
console.log("ScrollPanel Tester: awaiting " + dir + " fill");
var defer = q.defer(); var defer = q.defer();
this._fillDefers[dir] = defer; this._fillDefers[dir] = defer;
return defer.promise; return defer.promise;
@ -80,7 +81,7 @@ var Tester = React.createClass({
_onScroll: function(ev) { _onScroll: function(ev) {
var st = ev.target.scrollTop; var st = ev.target.scrollTop;
console.log("Scroll event; scrollTop: " + st); console.log("ScrollPanel Tester: scroll event; scrollTop: " + st);
this.lastScrollEvent = st; this.lastScrollEvent = st;
var d = this._scrollDefer; var d = this._scrollDefer;
@ -159,10 +160,29 @@ describe('ScrollPanel', function() {
scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass( scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
tester, "gm-scroll-view"); tester, "gm-scroll-view");
// wait for a browser tick to let the initial paginates complete // we need to make sure we don't call done() until q has finished
setTimeout(function() { // running the completion handlers from the fill requests. We can't
done(); // just use .done(), because that will end up ahead of those handlers
}, 0); // in the queue. We can't use window.setTimeout(0), because that also might
// run ahead of those handlers.
const sp = tester.scrollPanel();
let retriesRemaining = 1;
const awaitReady = function() {
return q().then(() => {
if (sp._pendingFillRequests.b === false &&
sp._pendingFillRequests.f === false
) {
return;
}
if (retriesRemaining == 0) {
throw new Error("fillRequests did not complete");
}
retriesRemaining--;
return awaitReady();
});
};
awaitReady().done(done);
}); });
afterEach(function() { afterEach(function() {

View file

@ -99,7 +99,11 @@ describe('TimelinePanel', function() {
// the document so that we can interact with it properly. // the document so that we can interact with it properly.
parentDiv = document.createElement('div'); parentDiv = document.createElement('div');
parentDiv.style.width = '800px'; parentDiv.style.width = '800px';
parentDiv.style.height = '600px';
// This has to be slightly carefully chosen. We expect to have to do
// exactly one pagination to fill it.
parentDiv.style.height = '500px';
parentDiv.style.overflow = 'hidden'; parentDiv.style.overflow = 'hidden';
document.body.appendChild(parentDiv); document.body.appendChild(parentDiv);
}); });
@ -235,7 +239,7 @@ describe('TimelinePanel', function() {
expect(client.paginateEventTimeline.callCount).toEqual(0); expect(client.paginateEventTimeline.callCount).toEqual(0);
done(); done();
}, 0); }, 0);
}, 0); }, 10);
}); });
it("should let you scroll down to the bottom after you've scrolled up", function(done) { it("should let you scroll down to the bottom after you've scrolled up", function(done) {

View file

@ -14,7 +14,15 @@ var MatrixEvent = jssdk.MatrixEvent;
*/ */
export function beforeEach(context) { export function beforeEach(context) {
var desc = context.currentTest.fullTitle(); var desc = context.currentTest.fullTitle();
console.log(); console.log();
// this puts a mark in the chrome devtools timeline, which can help
// figure out what's been going on.
if (console.timeStamp) {
console.timeStamp(desc);
}
console.log(desc); console.log(desc);
console.log(new Array(1 + desc.length).join("=")); console.log(new Array(1 + desc.length).join("="));
}; };

View file

@ -75,6 +75,16 @@ describe('MegolmExportEncryption', function() {
.toThrow('Trailer line not found'); .toThrow('Trailer line not found');
}); });
it('should handle a too-short body', function() {
const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA-----
AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx
cissyYBxjsfsAn
-----END MEGOLM SESSION DATA-----
`);
expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')})
.toThrow('Invalid file: too short');
});
it('should decrypt a range of inputs', function(done) { it('should decrypt a range of inputs', function(done) {
function next(i) { function next(i) {
if (i >= TEST_VECTORS.length) { if (i >= TEST_VECTORS.length) {