Merge remote-tracking branch 'upstream/develop' into feature-autocomplete

This commit is contained in:
Aviral Dasgupta 2016-07-03 00:00:02 +05:30
commit cd928fe6f5
20 changed files with 464 additions and 290 deletions

104
.eslintrc Normal file
View file

@ -0,0 +1,104 @@
{
"parser": "babel-eslint",
"plugins": [
"react",
"flowtype"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"impliedStrict": true
}
},
"env": {
"browser": true,
"amd": true,
"es6": true,
"node": true,
"mocha": true
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"rules": {
"no-undef": ["warn"],
"global-strict": ["off"],
"no-extra-semi": ["warn"],
"no-underscore-dangle": ["off"],
"no-console": ["off"],
"no-unused-vars": ["off"],
"no-trailing-spaces": ["warn", {
"skipBlankLines": true
}],
"no-unreachable": ["warn"],
"no-spaced-func": ["warn"],
"no-new-func": ["error"],
"no-new-wrappers": ["error"],
"no-invalid-regexp": ["error"],
"no-extra-bind": ["error"],
"no-magic-numbers": ["error"],
"consistent-return": ["error"],
"valid-jsdoc": ["error"],
"no-use-before-define": ["error"],
"camelcase": ["warn"],
"array-callback-return": ["error"],
"dot-location": ["warn", "property"],
"guard-for-in": ["error"],
"no-useless-call": ["warn"],
"no-useless-escape": ["warn"],
"no-useless-concat": ["warn"],
"brace-style": ["warn", "1tbs"],
"comma-style": ["warn", "last"],
"space-before-function-paren": ["warn", "never"],
"space-before-blocks": ["warn", "always"],
"keyword-spacing": ["warn", {
"before": true,
"after": true
}],
// dangling commas required, but only for multiline objects/arrays
"comma-dangle": ["warn", "always-multiline"],
// always === instead of ==, unless dealing with null/undefined
"eqeqeq": ["error", "smart"],
// always use curly braces, even with single statements
"curly": ["error", "all"],
// phasing out var in favour of let/const is a good idea
"no-var": ["warn"],
// always require semicolons
"semi": ["error", "always"],
// prefer rest and spread over the Old Ways
"prefer-spread": ["warn"],
"prefer-rest-params": ["warn"],
/** react **/
// bind or arrow function in props causes performance issues
"react/jsx-no-bind": ["error"],
"react/jsx-key": ["error"],
"react/prefer-stateless-function": ["warn"],
"react/sort-comp": ["warn"],
/** flowtype **/
"flowtype/require-parameter-type": 1,
"flowtype/require-return-type": [
1,
"always",
{
"annotateUndefined": "never"
}
],
"flowtype/space-after-type-colon": [
1,
"always"
],
"flowtype/space-before-type-colon": [
1,
"never"
]
},
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
}
}

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
npm-debug.log
/node_modules /node_modules
/lib /lib

View file

@ -2,6 +2,7 @@
set -e set -e
export KARMAFLAGS="--no-colors"
export NVM_DIR="/home/jenkins/.nvm" export NVM_DIR="/home/jenkins/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 4 nvm use 4
@ -14,6 +15,9 @@ npm install
# run the mocha tests # run the mocha tests
npm run test npm run test
# run eslint
npm run lint -- -f checkstyle -o eslint.xml || true
# delete the old tarball, if it exists # delete the old tarball, if it exists
rm -f matrix-react-sdk-*.tgz rm -f matrix-react-sdk-*.tgz

View file

@ -16,10 +16,12 @@
"reskindex": "reskindex -h header", "reskindex": "reskindex -h header",
"build": "babel src -d lib --source-maps", "build": "babel src -d lib --source-maps",
"start": "babel src -w -d lib --source-maps", "start": "babel src -w -d lib --source-maps",
"lint": "eslint src/",
"lintall": "eslint src/ test/",
"clean": "rimraf lib", "clean": "rimraf lib",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt", "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
"test": "karma start --browsers PhantomJS", "test": "karma start $KARMAFLAGS --browsers PhantomJS",
"test-multi": "karma start --single-run=false" "test-multi": "karma start $KARMAFLAGS --single-run=false"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.1.2", "classnames": "^2.1.2",
@ -55,8 +57,12 @@
"devDependencies": { "devDependencies": {
"babel": "^5.8.23", "babel": "^5.8.23",
"babel-core": "^5.8.38", "babel-core": "^5.8.38",
"babel-eslint": "^6.1.0",
"babel-loader": "^5.4.0", "babel-loader": "^5.4.0",
"babel-polyfill": "^6.5.0", "babel-polyfill": "^6.5.0",
"eslint": "^2.13.1",
"eslint-plugin-flowtype": "^2.3.0",
"eslint-plugin-react": "^5.2.2",
"expect": "^1.16.0", "expect": "^1.16.0",
"json-loader": "^0.5.3", "json-loader": "^0.5.3",
"karma": "^0.13.22", "karma": "^0.13.22",

View file

@ -24,30 +24,5 @@ module.exports = {
getDisplayAliasForRoom: function(room) { getDisplayAliasForRoom: function(room) {
return room.getCanonicalAlias() || room.getAliases()[0]; return room.getCanonicalAlias() || room.getAliases()[0];
}, },
/**
* Given a list of room objects, return the room which has the given alias,
* else null.
*/
getRoomForAlias: function(rooms, room_alias) {
var room;
for (var i = 0; i < rooms.length; i++) {
var aliasEvents = rooms[i].currentState.getStateEvents(
"m.room.aliases"
);
for (var j = 0; j < aliasEvents.length; j++) {
var aliases = aliasEvents[j].getContent().aliases || [];
for (var k = 0; k < aliases.length; k++) {
if (aliases[k] === room_alias) {
room = rooms[i];
break;
}
}
if (room) { break; }
}
if (room) { break; }
}
return room || null;
}
} }

47
src/SdkConfig.js Normal file
View file

@ -0,0 +1,47 @@
/*
Copyright 2016 OpenMarket 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.
*/
var DEFAULTS = {
// URL to a page we show in an iframe to configure integrations
//integrations_ui_url: "https://scalar.vector.im/",
integrations_ui_url: "http://127.0.0.1:5051/",
// Base URL to the REST interface of the integrations server
//integrations_rest_url: "https://scalar.vector.im/api",
integrations_rest_url: "http://127.0.0.1:5050",
};
class SdkConfig {
static get() {
return global.mxReactSdkConfig;
}
static put(cfg) {
var defaultKeys = Object.keys(DEFAULTS);
for (var i = 0; i < defaultKeys.length; ++i) {
if (cfg[defaultKeys[i]] === undefined) {
cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]];
}
}
global.mxReactSdkConfig = cfg;
}
static unset() {
global.mxReactSdkConfig = undefined;
}
}
module.exports = SdkConfig;

View file

@ -17,7 +17,6 @@ limitations under the License.
var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixClientPeg = require("./MatrixClientPeg");
var MatrixTools = require("./MatrixTools"); var MatrixTools = require("./MatrixTools");
var dis = require("./dispatcher"); var dis = require("./dispatcher");
var encryption = require("./encryption");
var Tinter = require("./Tinter"); var Tinter = require("./Tinter");
@ -89,25 +88,6 @@ var commands = {
return reject(this.getUsage()); return reject(this.getUsage());
}), }),
encrypt: new Command("encrypt", "<on|off>", function(room_id, args) {
if (args == "on") {
var client = MatrixClientPeg.get();
var members = client.getRoom(room_id).currentState.members;
var user_ids = Object.keys(members);
return success(
encryption.enableEncryption(client, room_id, user_ids)
);
}
if (args == "off") {
var client = MatrixClientPeg.get();
return success(
encryption.disableEncryption(client, room_id)
);
}
return reject(this.getUsage());
}),
// Change the room topic // Change the room topic
topic: new Command("topic", "<topic>", function(room_id, args) { topic: new Command("topic", "<topic>", function(room_id, args) {
if (args) { if (args) {
@ -132,48 +112,27 @@ var commands = {
}), }),
// Join a room // Join a room
join: new Command("join", "<room_alias>", function(room_id, args) { join: new Command("join", "#alias:domain", function(room_id, args) {
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); var matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
var room_alias = matches[1]; var room_alias = matches[1];
if (room_alias[0] !== '#') { if (room_alias[0] !== '#') {
return reject("Usage: /join #alias:domain"); return reject(this.getUsage());
} }
if (!room_alias.match(/:/)) { if (!room_alias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain(); room_alias += ':' + MatrixClientPeg.get().getDomain();
} }
// Try to find a room with this alias
// XXX: do we need to do this? Doesn't the JS SDK suppress duplicate attempts to join the same room?
var foundRoom = MatrixTools.getRoomForAlias(
MatrixClientPeg.get().getRooms(),
room_alias
);
if (foundRoom) { // we've already joined this room, view it if it's not archived.
var me = foundRoom.getMember(MatrixClientPeg.get().credentials.userId);
if (me && me.membership !== "leave") {
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: foundRoom.roomId room_alias: room_alias,
auto_join: true,
}); });
return success(); return success();
} }
} }
// otherwise attempt to join this alias.
return success(
MatrixClientPeg.get().joinRoom(room_alias).then(
function(room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId
});
})
);
}
}
return reject(this.getUsage()); return reject(this.getUsage());
}), }),

View file

@ -24,7 +24,6 @@ var PresetValues = {
Custom: "custom", Custom: "custom",
}; };
var q = require('q'); var q = require('q');
var encryption = require("../../encryption");
var sdk = require('../../index'); var sdk = require('../../index');
module.exports = React.createClass({ module.exports = React.createClass({
@ -108,17 +107,8 @@ module.exports = React.createClass({
var deferred = cli.createRoom(options); var deferred = cli.createRoom(options);
var response;
if (this.state.encrypt) { if (this.state.encrypt) {
deferred = deferred.then(function(res) { // TODO
response = res;
return encryption.enableEncryption(
cli, response.room_id, options.invite
);
}).then(function() {
return q(response) }
);
} }
this.setState({ this.setState({

View file

@ -108,10 +108,14 @@ module.exports = React.createClass({
return window.localStorage.getItem("mx_hs_url"); return window.localStorage.getItem("mx_hs_url");
} }
else { else {
return this.props.config.default_hs_url || "https://matrix.org"; return this.getDefaultHsUrl();
} }
}, },
getDefaultHsUrl() {
return this.props.config.default_hs_url || "https://matrix.org";
},
getFallbackHsUrl: function() { getFallbackHsUrl: function() {
return this.props.config.fallback_hs_url; return this.props.config.fallback_hs_url;
}, },
@ -126,10 +130,14 @@ module.exports = React.createClass({
return window.localStorage.getItem("mx_is_url"); return window.localStorage.getItem("mx_is_url");
} }
else { else {
return this.props.config.default_is_url || "https://vector.im" return this.getDefaultIsUrl();
} }
}, },
getDefaultIsUrl() {
return this.props.config.default_is_url || "https://vector.im";
},
componentWillMount: function() { componentWillMount: function() {
this.favicon = new Favico({animation: 'none'}); this.favicon = new Favico({animation: 'none'});
}, },
@ -151,8 +159,8 @@ module.exports = React.createClass({
this.onLoggedIn({ this.onLoggedIn({
userId: this.props.startingQueryParams.guest_user_id, userId: this.props.startingQueryParams.guest_user_id,
accessToken: this.props.startingQueryParams.guest_access_token, accessToken: this.props.startingQueryParams.guest_access_token,
homeserverUrl: this.props.config.default_hs_url, homeserverUrl: this.getDefaultHsUrl(),
identityServerUrl: this.props.config.default_is_url, identityServerUrl: this.getDefaultIsUrl(),
guest: true guest: true
}); });
} }
@ -403,10 +411,7 @@ module.exports = React.createClass({
// known to be in (eg. user clicks on a room in the recents panel), supply the ID // known to be in (eg. user clicks on a room in the recents panel), supply the ID
// If the user is clicking on a room in the context of the alias being presented // If the user is clicking on a room in the context of the alias being presented
// to them, supply the room alias. If both are supplied, the room ID will be ignored. // to them, supply the room alias. If both are supplied, the room ID will be ignored.
this._viewRoom( this._viewRoom(payload);
payload.room_id, payload.room_alias, payload.show_settings, payload.event_id,
payload.third_party_invite, payload.oob_data
);
break; break;
case 'view_prev_room': case 'view_prev_room':
roomIndexDelta = -1; roomIndexDelta = -1;
@ -423,7 +428,7 @@ module.exports = React.createClass({
} }
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1; if (roomIndex < 0) roomIndex = allRooms.length - 1;
this._viewRoom(allRooms[roomIndex].roomId); this._viewRoom({ room_id: allRooms[roomIndex].roomId });
break; break;
case 'view_indexed_room': case 'view_indexed_room':
var allRooms = RoomListSorter.mostRecentActivityFirst( var allRooms = RoomListSorter.mostRecentActivityFirst(
@ -431,7 +436,7 @@ module.exports = React.createClass({
); );
var roomIndex = payload.roomIndex; var roomIndex = payload.roomIndex;
if (allRooms[roomIndex]) { if (allRooms[roomIndex]) {
this._viewRoom(allRooms[roomIndex].roomId); this._viewRoom({ room_id: allRooms[roomIndex].roomId });
} }
break; break;
case 'view_user_settings': case 'view_user_settings':
@ -491,39 +496,45 @@ module.exports = React.createClass({
// switch view to the given room // switch view to the given room
// //
// eventId is optional and will cause a switch to the context of that // @param {Object} room_info Object containing data about the room to be joined
// particular event. // @param {string=} room_info.room_id ID of the room to join. One of room_id or room_alias must be given.
// @param {Object} thirdPartyInvite Object containing data about the third party // @param {string=} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given.
// @param {boolean=} room_info.auto_join If true, automatically attempt to join the room if not already a member.
// @param {boolean=} room_info.show_settings Makes RoomView show the room settings dialog.
// @param {string=} room_info.event_id ID of the event in this room to show: this will cause a switch to the
// context of that particular event.
// @param {Object=} room_info.third_party_invite Object containing data about the third party
// we received to join the room, if any. // we received to join the room, if any.
// @param {string} thirdPartyInvite.inviteSignUrl 3pid invite sign URL // @param {string=} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL
// @param {string} thirdPartyInvite.invitedwithEmail The email address the invite was sent to // @param {string=} room_info.third_party_invite.invitedEmail The email address the invite was sent to
// @param {Object} oob_data Object of additional data about the room // @param {Object=} room_info.oob_data Object of additional data about the room
// that has been passed out-of-band (eg. // that has been passed out-of-band (eg.
// room name and avatar from an invite email) // room name and avatar from an invite email)
_viewRoom: function(roomId, roomAlias, showSettings, eventId, thirdPartyInvite, oob_data) { _viewRoom: function(room_info) {
// before we switch room, record the scroll state of the current room // before we switch room, record the scroll state of the current room
this._updateScrollMap(); this._updateScrollMap();
this.focusComposer = true; this.focusComposer = true;
var newState = { var newState = {
initialEventId: eventId, initialEventId: room_info.event_id,
highlightedEventId: eventId, highlightedEventId: room_info.event_id,
initialEventPixelOffset: undefined, initialEventPixelOffset: undefined,
page_type: this.PageTypes.RoomView, page_type: this.PageTypes.RoomView,
thirdPartyInvite: thirdPartyInvite, thirdPartyInvite: room_info.third_party_invite,
roomOobData: oob_data, roomOobData: room_info.oob_data,
currentRoomAlias: roomAlias, currentRoomAlias: room_info.room_alias,
autoJoin: room_info.auto_join,
}; };
if (!roomAlias) { if (!room_info.room_alias) {
newState.currentRoomId = roomId; newState.currentRoomId = room_info.room_id;
} }
// if we aren't given an explicit event id, look for one in the // if we aren't given an explicit event id, look for one in the
// scrollStateMap. // scrollStateMap.
if (!eventId) { if (!room_info.event_id) {
var scrollState = this.scrollStateMap[roomId]; var scrollState = this.scrollStateMap[room_info.room_id];
if (scrollState) { if (scrollState) {
newState.initialEventId = scrollState.focussedEvent; newState.initialEventId = scrollState.focussedEvent;
newState.initialEventPixelOffset = scrollState.pixelOffset; newState.initialEventPixelOffset = scrollState.pixelOffset;
@ -536,8 +547,8 @@ module.exports = React.createClass({
// the new screen yet (we won't be showing it yet) // the new screen yet (we won't be showing it yet)
// The normal case where this happens is navigating // The normal case where this happens is navigating
// to the room in the URL bar on page load. // to the room in the URL bar on page load.
var presentedId = roomAlias || roomId; var presentedId = room_info.room_alias || room_info.room_id;
var room = MatrixClientPeg.get().getRoom(roomId); var room = MatrixClientPeg.get().getRoom(room_info.room_id);
if (room) { if (room) {
var theAlias = MatrixTools.getDisplayAliasForRoom(room); var theAlias = MatrixTools.getDisplayAliasForRoom(room);
if (theAlias) presentedId = theAlias; if (theAlias) presentedId = theAlias;
@ -553,15 +564,15 @@ module.exports = React.createClass({
// Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); // Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
} }
if (eventId) { if (room_info.event_id) {
presentedId += "/"+eventId; presentedId += "/"+room_info.event_id;
} }
this.notifyNewScreen('room/'+presentedId); this.notifyNewScreen('room/'+presentedId);
newState.ready = true; newState.ready = true;
} }
this.setState(newState); this.setState(newState);
if (this.refs.roomView && showSettings) { if (this.refs.roomView && room_info.showSettings) {
this.refs.roomView.showSettings(true); this.refs.roomView.showSettings(true);
} }
}, },
@ -1030,6 +1041,7 @@ module.exports = React.createClass({
<RoomView <RoomView
ref="roomView" ref="roomView"
roomAddress={this.state.currentRoomAlias || this.state.currentRoomId} roomAddress={this.state.currentRoomAlias || this.state.currentRoomId}
autoJoin={this.state.autoJoin}
onRoomIdResolved={this.onRoomIdResolved} onRoomIdResolved={this.onRoomIdResolved}
eventId={this.state.initialEventId} eventId={this.state.initialEventId}
thirdPartyInvite={this.state.thirdPartyInvite} thirdPartyInvite={this.state.thirdPartyInvite}
@ -1109,8 +1121,8 @@ module.exports = React.createClass({
email={this.props.startingQueryParams.email} email={this.props.startingQueryParams.email}
username={this.state.upgradeUsername} username={this.state.upgradeUsername}
guestAccessToken={this.state.guestAccessToken} guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.props.config.default_hs_url} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.props.config.default_is_url} defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand} brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
@ -1124,8 +1136,8 @@ module.exports = React.createClass({
} else if (this.state.screen == 'forgot_password') { } else if (this.state.screen == 'forgot_password') {
return ( return (
<ForgotPassword <ForgotPassword
defaultHsUrl={this.props.config.default_hs_url} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.props.config.default_is_url} defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
onComplete={this.onLoginClick} onComplete={this.onLoginClick}
@ -1136,13 +1148,13 @@ module.exports = React.createClass({
<Login <Login
onLoggedIn={this.onLoggedIn} onLoggedIn={this.onLoggedIn}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
defaultHsUrl={this.props.config.default_hs_url} defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.props.config.default_is_url} defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()} customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()} customIsUrl={this.getCurrentIsUrl()}
fallbackHsUrl={this.getFallbackHsUrl()} fallbackHsUrl={this.getFallbackHsUrl()}
onForgotPasswordClick={this.onForgotPasswordClick} onForgotPasswordClick={this.onForgotPasswordClick}
onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.props.config.default_hs_url ? this._registerAsGuest.bind(this, true) : undefined} onLoginAsGuestClick={this.props.enableGuest && this.props.config && this._registerAsGuest.bind(this, true)}
onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null } onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null }
/> />
); );

View file

@ -119,6 +119,11 @@ module.exports = React.createClass({
guestsCanJoin: false, guestsCanJoin: false,
canPeek: false, canPeek: false,
// error object, as from the matrix client/server API
// If we failed to load information about the room,
// store the error here.
roomLoadError: null,
// this is true if we are fully scrolled-down, and are looking at // this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. It has the effect of hiding the // the end of the live timeline. It has the effect of hiding the
// 'scroll to bottom' knob, among a couple of other things. // 'scroll to bottom' knob, among a couple of other things.
@ -161,10 +166,11 @@ module.exports = React.createClass({
roomId: result.room_id, roomId: result.room_id,
roomLoading: !room, roomLoading: !room,
hasUnsentMessages: this._hasUnsentMessages(room), hasUnsentMessages: this._hasUnsentMessages(room),
}, this._updatePeeking); }, this._onHaveRoom);
}, (err) => { }, (err) => {
this.setState({ this.setState({
roomLoading: false, roomLoading: false,
roomLoadError: err,
}); });
}); });
} else { } else {
@ -174,11 +180,11 @@ module.exports = React.createClass({
room: room, room: room,
roomLoading: !room, roomLoading: !room,
hasUnsentMessages: this._hasUnsentMessages(room), hasUnsentMessages: this._hasUnsentMessages(room),
}, this._updatePeeking); }, this._onHaveRoom);
} }
}, },
_updatePeeking: function() { _onHaveRoom: function() {
// if this is an unknown room then we're in one of three states: // if this is an unknown room then we're in one of three states:
// - This is a room we can peek into (search engine) (we can /peek) // - This is a room we can peek into (search engine) (we can /peek)
// - This is a room we can publicly join or were invited to. (we can /join) // - This is a room we can publicly join or were invited to. (we can /join)
@ -189,7 +195,21 @@ module.exports = React.createClass({
// Note that peeking works by room ID and room ID only, as opposed to joining // Note that peeking works by room ID and room ID only, as opposed to joining
// which must be by alias or invite wherever possible (peeking currently does // which must be by alias or invite wherever possible (peeking currently does
// not work over federation). // not work over federation).
if (!this.state.room && this.state.roomId) {
// NB. We peek if we are not in the room, although if we try to peek into
// a room in which we have a member event (ie. we've left) synapse will just
// send us the same data as we get in the sync (ie. the last events we saw).
var user_is_in_room = null;
if (this.state.room) {
user_is_in_room = this.state.room.hasMembershipState(
MatrixClientPeg.get().credentials.userId, 'join'
);
}
if (!user_is_in_room && this.state.roomId) {
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (this.state.roomId) {
console.log("Attempting to peek into room %s", this.state.roomId); console.log("Attempting to peek into room %s", this.state.roomId);
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => { MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
@ -211,7 +231,8 @@ module.exports = React.createClass({
throw err; throw err;
} }
}).done(); }).done();
} else if (this.state.room) { }
} else if (user_is_in_room) {
MatrixClientPeg.get().stopPeeking(); MatrixClientPeg.get().stopPeeking();
this._onRoomLoaded(this.state.room); this._onRoomLoaded(this.state.room);
} }
@ -999,7 +1020,7 @@ module.exports = React.createClass({
this.setState({ this.setState({
rejecting: true rejecting: true
}); });
MatrixClientPeg.get().leave(this.props.roomAddress).done(function() { MatrixClientPeg.get().leave(this.state.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' }); dis.dispatch({ action: 'view_next_room' });
self.setState({ self.setState({
rejecting: false rejecting: false
@ -1274,6 +1295,7 @@ module.exports = React.createClass({
// We have no room object for this room, only the ID. // We have no room object for this room, only the ID.
// We've got to this room by following a link, possibly a third party invite. // We've got to this room by following a link, possibly a third party invite.
var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null;
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<RoomHeader ref="header" <RoomHeader ref="header"
@ -1284,7 +1306,8 @@ module.exports = React.createClass({
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked } <RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onRejectClick={ this.onRejectThreepidInviteButtonClicked } onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canJoin={ true } canPreview={ false } canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias}
spinner={this.state.joining} spinner={this.state.joining}
inviterName={inviterName} inviterName={inviterName}
invitedEmail={invitedEmail} invitedEmail={invitedEmail}
@ -1322,7 +1345,7 @@ module.exports = React.createClass({
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked } <RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onRejectClick={ this.onRejectButtonClicked } onRejectClick={ this.onRejectButtonClicked }
inviterName={ inviterName } inviterName={ inviterName }
canJoin={ true } canPreview={ false } canPreview={ false }
spinner={this.state.joining} spinner={this.state.joining}
room={this.state.room} room={this.state.room}
/> />
@ -1392,7 +1415,7 @@ module.exports = React.createClass({
invitedEmail = this.props.thirdPartyInvite.invitedEmail; invitedEmail = this.props.thirdPartyInvite.invitedEmail;
} }
aux = ( aux = (
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true} <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining} spinner={this.state.joining}
inviterName={inviterName} inviterName={inviterName}

View file

@ -232,7 +232,9 @@ module.exports = React.createClass({displayName: 'Login',
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div> <div>
<h2>Sign in</h2> <h2>Sign in
{ loader }
</h2>
{ this.componentForStep(this._getCurrentFlowStep()) } { this.componentForStep(this._getCurrentFlowStep()) }
<ServerConfig ref="serverConfig" <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
@ -244,7 +246,6 @@ module.exports = React.createClass({displayName: 'Login',
onIsUrlChanged={this.onIsUrlChanged} onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000}/> delayTimeMs={1000}/>
<div className="mx_Login_error"> <div className="mx_Login_error">
{ loader }
{ this.state.errorText } { this.state.errorText }
</div> </div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">

View file

@ -34,10 +34,15 @@ module.exports = React.createClass({
propTypes: { propTypes: {
value: React.PropTypes.number.isRequired, value: React.PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React // if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform. // to reflect the current value, rather than left freeform.
// MemberInfo uses controlled; RoomSettings uses non-controlled. // MemberInfo uses controlled; RoomSettings uses non-controlled.
controlled: React.PropTypes.bool.isRequired, //
// ignored if disabled is truthy. false by default.
controlled: React.PropTypes.bool,
// should the user be able to change the value? false by default.
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
onChange: React.PropTypes.func, onChange: React.PropTypes.func,
}, },

View file

@ -139,7 +139,8 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._suppressReadReceiptAnimation = false; this._suppressReadReceiptAnimation = false;
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified); MatrixClientPeg.get().on("deviceVerificationChanged",
this.onDeviceVerificationChanged);
}, },
componentWillReceiveProps: function (nextProps) { componentWillReceiveProps: function (nextProps) {
@ -163,11 +164,12 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
var client = MatrixClientPeg.get(); var client = MatrixClientPeg.get();
if (client) { if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified); client.removeListener("deviceVerificationChanged",
this.onDeviceVerificationChanged);
} }
}, },
onDeviceVerified: function(userId, device) { onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.mxEvent.getSender()) { if (userId == this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent); this._verifyEvent(this.props.mxEvent);
} }

View file

@ -36,32 +36,73 @@ module.exports = React.createClass({
); );
}, },
render: function() { onBlockClick: function() {
var indicator = null, button = null; MatrixClientPeg.get().setDeviceBlocked(
if (this.props.device.verified) { this.props.userId, this.props.device.id, true
indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
); );
button = ( },
onUnblockClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.id, false
);
},
render: function() {
var indicator = null, blockButton = null, verifyButton = null;
if (this.props.device.blocked) {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
onClick={this.onUnblockClick}>
Unblock
</div>
);
} else {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
onClick={this.onBlockClick}>
Block
</div>
);
}
if (this.props.device.verified) {
verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify" <div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}> onClick={this.onUnverifyClick}>
Unverify Unverify
</div> </div>
); );
} else { } else {
button = ( verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify" <div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}> onClick={this.onVerifyClick}>
Verify Verify
</div> </div>
); );
} }
if (this.props.device.blocked) {
indicator = (
<div className="mx_MemberDeviceInfo_blocked">&#x2716;</div>
);
} else if (this.props.device.verified) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
);
} else {
indicator = (
<div className="mx_MemberDeviceInfo_unverified">?</div>
);
}
return ( return (
<div className="mx_MemberDeviceInfo"> <div className="mx_MemberDeviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div> <div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div>
<div className="mx_MemberDeviceInfo_deviceKey">{this.props.device.key}</div>
{indicator} {indicator}
{button} {verifyButton}
{blockButton}
</div> </div>
); );
}, },

View file

@ -70,7 +70,7 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._updateStateForNewMember(this.props.member); this._updateStateForNewMember(this.props.member);
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified); MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
@ -82,14 +82,14 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
var client = MatrixClientPeg.get(); var client = MatrixClientPeg.get();
if (client) { if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified); client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
} }
if (this._cancelDeviceList) { if (this._cancelDeviceList) {
this._cancelDeviceList(); this._cancelDeviceList();
} }
}, },
onDeviceVerified: function(userId, device) { onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.member.userId) { if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of // no need to re-download the whole thing; just update our copy of
// the list. // the list.
@ -358,10 +358,15 @@ module.exports = React.createClass({
]; ];
var existingRoomId; var existingRoomId;
// roomId can be null here because of a hack in MatrixChat.onUserClick where we
// abuse this to view users rather than room members.
var currentMembers;
if (this.props.member.roomId) {
var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
var currentMembers = currentRoom.getJoinedMembers(); currentMembers = currentRoom.getJoinedMembers();
}
// if we're currently in a 1:1 with this user, start a new chat // if we're currently in a 1:1 with this user, start a new chat
if (currentMembers.length === 2 && if (currentMembers && currentMembers.length === 2 &&
userIds.indexOf(currentMembers[0].userId) !== -1 && userIds.indexOf(currentMembers[0].userId) !== -1 &&
userIds.indexOf(currentMembers[1].userId) !== -1) userIds.indexOf(currentMembers[1].userId) !== -1)
{ {
@ -535,8 +540,10 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<h3>Devices</h3> <h3>Devices</h3>
<div className="mx_MemberInfo_devices">
{devComponents} {devComponents}
</div> </div>
</div>
); );
}, },

View file

@ -425,27 +425,7 @@ module.exports = React.createClass({
// For now, let's just order things by timestamp. It's really annoying // For now, let's just order things by timestamp. It's really annoying
// that a user disappears from sight just because they temporarily go offline // that a user disappears from sight just because they temporarily go offline
/* return userB.getLastActiveTs() - userA.getLastActiveTs();
var presenceMap = {
online: 3,
unavailable: 2,
offline: 1
};
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
if (presenceOrdA != presenceOrdB) {
return presenceOrdB - presenceOrdA;
}
*/
var lastActiveTsA = userA && userA.lastActiveTs ? userA.lastActiveTs : 0;
var lastActiveTsB = userB && userB.lastActiveTs ? userB.lastActiveTs : 0;
// console.log("comparing ts: " + lastActiveTsA + " and " + lastActiveTsB);
return lastActiveTsB - lastActiveTsA;
}, },
onSearchQueryChanged: function(input) { onSearchQueryChanged: function(input) {

View file

@ -33,16 +33,24 @@ module.exports = React.createClass({
// If invited by 3rd party invite, the email address the invite was sent to // If invited by 3rd party invite, the email address the invite was sent to
invitedEmail: React.PropTypes.string, invitedEmail: React.PropTypes.string,
canJoin: React.PropTypes.bool,
// A standard client/server API error object. If supplied, indicates that the
// caller was unable to fetch details about the room for the given reason.
error: React.PropTypes.object,
canPreview: React.PropTypes.bool, canPreview: React.PropTypes.bool,
spinner: React.PropTypes.bool, spinner: React.PropTypes.bool,
room: React.PropTypes.object, room: React.PropTypes.object,
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: React.PropTypes.object,
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
onJoinClick: function() {}, onJoinClick: function() {},
canJoin: false,
canPreview: true, canPreview: true,
}; };
}, },
@ -115,8 +123,24 @@ module.exports = React.createClass({
); );
} }
else if (this.props.canJoin) { else if (this.props.error) {
var name = this.props.room ? this.props.room.name : ""; var name = this.props.roomAlias || "This room";
var error;
if (this.props.error.errcode == 'M_NOT_FOUND') {
error = name + " does not exist";
} else {
error = name + " is not accessible at this time";
}
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
{ error }
</div>
</div>
);
}
else {
var name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
name = name ? <b>{ name }</b> : "a room"; name = name ? <b>{ name }</b> : "a room";
joinBlock = ( joinBlock = (
<div> <div>

View file

@ -21,6 +21,14 @@ var sdk = require('../../../index');
var Modal = require('../../../Modal'); var Modal = require('../../../Modal');
var ObjectUtils = require("../../../ObjectUtils"); var ObjectUtils = require("../../../ObjectUtils");
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var UserSettingsStore = require('../../../UserSettingsStore');
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
function parseIntWithDefault(val, def) {
var res = parseInt(val);
return isNaN(res) ? def : res;
}
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomSettings', displayName: 'RoomSettings',
@ -57,7 +65,7 @@ module.exports = React.createClass({
tags_changed: false, tags_changed: false,
tags: tags, tags: tags,
areNotifsMuted: areNotifsMuted, areNotifsMuted: areNotifsMuted,
isRoomPublished: this._originalIsRoomPublished, // loaded async in componentWillMount isRoomPublished: false, // loaded async in componentWillMount
}; };
}, },
@ -199,11 +207,14 @@ module.exports = React.createClass({
} }
}); });
} }
console.log("Performing %s operations", promises.length);
// color scheme // color scheme
promises.push(this.saveColor()); promises.push(this.saveColor());
// encryption
promises.push(this.saveEncryption());
console.log("Performing %s operations", promises.length);
return q.allSettled(promises); return q.allSettled(promises);
}, },
@ -217,6 +228,19 @@ module.exports = React.createClass({
return this.refs.color_settings.saveSettings(); return this.refs.color_settings.saveSettings();
}, },
saveEncryption: function () {
if (!this.refs.encrypt) { return q(); }
var encrypt = this.refs.encrypt.checked;
if (!encrypt) { return q(); }
var roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.encryption",
{ algorithm: "m.olm.v1.curve25519-aes-sha2" }
);
},
_hasDiff: function(strA, strB) { _hasDiff: function(strA, strB) {
// treat undefined as an empty string because other components may blindly // treat undefined as an empty string because other components may blindly
// call setName("") when there has been no diff made to the name! // call setName("") when there has been no diff made to the name!
@ -359,6 +383,39 @@ module.exports = React.createClass({
roomState.mayClientSendStateEvent("m.room.guest_access", cli)) roomState.mayClientSendStateEvent("m.room.guest_access", cli))
}, },
_renderEncryptionSection: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
return null;
}
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
var text = "Encryption is " + (isEncrypted ? "" : "not ") +
"enabled in this room.";
var button;
if (!isEncrypted &&
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
button = (
<label>
<input type="checkbox" ref="encrypt" />
Enable encryption (warning: cannot be disabled again!)
</label>
);
}
return (
<div className="mx_RoomSettings_toggles">
<h3>Encryption</h3>
<label>{text}</label>
{button}
</div>
);
},
render: function() { render: function() {
// TODO: go through greying out things you don't have permission to change // TODO: go through greying out things you don't have permission to change
// (or turning them into informative stuff) // (or turning them into informative stuff)
@ -368,58 +425,29 @@ module.exports = React.createClass({
var EditableText = sdk.getComponent('elements.EditableText'); var EditableText = sdk.getComponent('elements.EditableText');
var PowerSelector = sdk.getComponent('elements.PowerSelector'); var PowerSelector = sdk.getComponent('elements.PowerSelector');
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
var events_levels = (power_levels ? power_levels.getContent().events : {}) || {};
var cli = MatrixClientPeg.get(); var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState; var roomState = this.props.room.currentState;
var user_id = cli.credentials.userId; var user_id = cli.credentials.userId;
if (power_levels) { var power_level_event = roomState.getStateEvents('m.room.power_levels', '');
power_levels = power_levels.getContent(); var power_levels = power_level_event ? power_level_event.getContent() : {};
var events_levels = power_levels.events || {};
var ban_level = parseInt(power_levels.ban);
var kick_level = parseInt(power_levels.kick);
var redact_level = parseInt(power_levels.redact);
var invite_level = parseInt(power_levels.invite || 0);
var send_level = parseInt(power_levels.events_default || 0);
var state_level = parseInt(power_levels.state_default || 50);
var default_user_level = parseInt(power_levels.users_default || 0);
if (power_levels.ban == undefined) ban_level = 50;
if (power_levels.kick == undefined) kick_level = 50;
if (power_levels.redact == undefined) redact_level = 50;
var user_levels = power_levels.users || {}; var user_levels = power_levels.users || {};
var ban_level = parseIntWithDefault(power_levels.ban, 50);
var kick_level = parseIntWithDefault(power_levels.kick, 50);
var redact_level = parseIntWithDefault(power_levels.redact, 50);
var invite_level = parseIntWithDefault(power_levels.invite, 50);
var send_level = parseIntWithDefault(power_levels.events_default, 0);
var state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0;
var default_user_level = parseIntWithDefault(power_levels.users_default, 0);
var current_user_level = user_levels[user_id]; var current_user_level = user_levels[user_id];
if (current_user_level == undefined) current_user_level = default_user_level; if (current_user_level === undefined) {
current_user_level = default_user_level;
var power_level_level = events_levels["m.room.power_levels"];
if (power_level_level == undefined) {
power_level_level = state_level;
} }
var can_change_levels = current_user_level >= power_level_level; var can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
} else {
var ban_level = 50;
var kick_level = 50;
var redact_level = 50;
var invite_level = 0;
var send_level = 0;
var state_level = 0;
var default_user_level = 0;
var user_levels = [];
var events_levels = [];
var current_user_level = 0;
var power_level_level = 0;
var can_change_levels = false;
}
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
var canSetTag = !cli.isGuest(); var canSetTag = !cli.isGuest();
@ -609,10 +637,6 @@ module.exports = React.createClass({
Members only (since they joined) Members only (since they joined)
</label> </label>
</div> </div>
<label className="mx_RoomSettings_encrypt">
<input type="checkbox" />
Encrypt room
</label>
</div> </div>
@ -677,6 +701,8 @@ module.exports = React.createClass({
{ bannedUsersSection } { bannedUsersSection }
{ this._renderEncryptionSection() }
<h3>Advanced</h3> <h3>Advanced</h3>
<div className="mx_RoomSettings_settings"> <div className="mx_RoomSettings_settings">
This room's internal ID is <code>{ this.props.room.roomId }</code> This room's internal ID is <code>{ this.props.room.roomId }</code>

View file

@ -1,38 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
function enableEncyption(client, roomId, members) {
members = members.slice(0);
members.push(client.credentials.userId);
// TODO: Check the keys actually match what keys the user has.
// TODO: Don't redownload keys each time.
return client.downloadKeys(members, "forceDownload").then(function(res) {
return client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
members: members,
});
})
}
function disableEncryption(client, roomId) {
return client.disableRoomEncryption(roomId);
}
module.exports = {
enableEncryption: enableEncyption,
disableEncryption: disableEncryption,
}

View file

@ -210,7 +210,7 @@ describe('TimelinePanel', function() {
var N_EVENTS = 600; var N_EVENTS = 600;
// sadly, loading all those events takes a while // sadly, loading all those events takes a while
this.timeout(N_EVENTS * 20); this.timeout(N_EVENTS * 40);
// client.getRoom is called a /lot/ in this test, so replace // client.getRoom is called a /lot/ in this test, so replace
// sinon's spy with a fast noop. // sinon's spy with a fast noop.
@ -220,12 +220,14 @@ describe('TimelinePanel', function() {
for (var i = 0; i < N_EVENTS; i++) { for (var i = 0; i < N_EVENTS; i++) {
timeline.addEvent(mkMessage()); timeline.addEvent(mkMessage());
} }
console.log("added events to timeline");
var scrollDefer; var scrollDefer;
var panel = ReactDOM.render( var panel = ReactDOM.render(
<TimelinePanel room={room} onScroll={()=>{scrollDefer.resolve()}} />, <TimelinePanel room={room} onScroll={()=>{scrollDefer.resolve()}} />,
parentDiv parentDiv
); );
console.log("TimelinePanel rendered");
var messagePanel = ReactTestUtils.findRenderedComponentWithType( var messagePanel = ReactTestUtils.findRenderedComponentWithType(
panel, sdk.getComponent('structures.MessagePanel')); panel, sdk.getComponent('structures.MessagePanel'));
@ -246,6 +248,7 @@ describe('TimelinePanel', function() {
// need to go further // need to go further
return backPaginate(); return backPaginate();
} }
console.log("paginated to end.");
// hopefully, we got to the start of the timeline // hopefully, we got to the start of the timeline
expect(messagePanel.props.backPaginating).toBe(false); expect(messagePanel.props.backPaginating).toBe(false);
@ -259,6 +262,7 @@ describe('TimelinePanel', function() {
expect(messagePanel.props.suppressFirstDateSeparator).toBe(true); expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
// back-paginate until we hit the start // back-paginate until we hit the start
console.log("back paginating...");
return backPaginate(); return backPaginate();
}).then(() => { }).then(() => {
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false); expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);