diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9086006376..262d55c6da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,29 @@
+Changes in [0.5.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.2) (2016-04-22)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.1...v0.5.2)
+
+Performance improvements:
+
+ * Reduce number of events shown in a room to 250
+ [afb301f](https://github.com/matrix-org/matrix-react-sdk/commit/afb301ffb78c019a50e40caa5d9042ad39c117fe)
+ * add heuristics to hide URL previews...
+ [\#284](https://github.com/matrix-org/matrix-react-sdk/pull/284)
+ * Fix bug which stopped us scrolling down after we scrolled up
+ [\#283](https://github.com/matrix-org/matrix-react-sdk/pull/283)
+ * Don't relayout scrollpanels every time something changes
+ [\#280](https://github.com/matrix-org/matrix-react-sdk/pull/280)
+ * Reduce number of renders on received events
+ [\#279](https://github.com/matrix-org/matrix-react-sdk/pull/279)
+ * Avoid rerendering EventTiles when not necessary
+ [\#278](https://github.com/matrix-org/matrix-react-sdk/pull/278)
+ * Speed up processing of TimelinePanel updates on new events
+ [\#277](https://github.com/matrix-org/matrix-react-sdk/pull/277)
+
+Other bug fixes:
+ * Fix read-receipt animation
+ [\#282](https://github.com/matrix-org/matrix-react-sdk/pull/282),
+ [\#281](https://github.com/matrix-org/matrix-react-sdk/pull/281)
+
Changes in [0.5.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.1) (2016-04-19)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.4.0...v0.5.1)
diff --git a/karma.conf.js b/karma.conf.js
index 52eea45f12..1ae2494add 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -51,8 +51,17 @@ module.exports = function (config) {
files: [
testFile,
gsCss,
+
+ // some images to reduce noise from the tests
+ {pattern: 'test/img/*', watched: false, included: false,
+ served: true, nocache: false},
],
+ // redirect img links to the karma server
+ proxies: {
+ "/img/": "/base/test/img/",
+ },
+
// list of files to exclude
//
// This doesn't work. It turns out that it's webpack which does the
diff --git a/package.json b/package.json
index c956560739..3f4a862f6f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.5.1",
+ "version": "0.5.2",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -49,6 +49,7 @@
"babel": "^5.8.23",
"babel-core": "^5.8.38",
"babel-loader": "^5.4.0",
+ "babel-polyfill": "^6.5.0",
"expect": "^1.16.0",
"json-loader": "^0.5.3",
"karma": "^0.13.22",
diff --git a/src/Velociraptor.js b/src/Velociraptor.js
index 066b1e2d05..ad12d1323b 100644
--- a/src/Velociraptor.js
+++ b/src/Velociraptor.js
@@ -13,25 +13,30 @@ module.exports = React.createClass({
displayName: 'Velociraptor',
propTypes: {
- children: React.PropTypes.array,
+ // either a list of child nodes, or a single child.
+ children: React.PropTypes.any,
+
+ // optional transition information for changing existing children
transition: React.PropTypes.object,
- container: React.PropTypes.string
},
componentWillMount: function() {
- this.children = {};
this.nodes = {};
- var self = this;
- React.Children.map(this.props.children, function(c) {
- self.children[c.key] = c;
- });
+ this._updateChildren(this.props.children);
},
componentWillReceiveProps: function(nextProps) {
+ this._updateChildren(nextProps.children);
+ },
+
+ /**
+ * update `this.children` according to the new list of children given
+ */
+ _updateChildren: function(newChildren) {
var self = this;
- var oldChildren = this.children;
+ var oldChildren = this.children || {};
this.children = {};
- React.Children.map(nextProps.children, function(c) {
+ React.Children.toArray(newChildren).forEach(function(c) {
if (oldChildren[c.key]) {
var old = oldChildren[c.key];
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
@@ -92,22 +97,23 @@ module.exports = React.createClass({
//console.log("start: "+JSON.stringify(startStyles[i]));
}
// and then we animate to the resting state
- Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]);
+ Velocity(domNode, node.props._restingStyle,
+ transitionOpts[i-1])
+ .then(() => {
+ // once we've reached the resting state, hide the element if
+ // appropriate
+ domNode.style.visibility = node.props._restingStyle.visibility;
+ });
+
//console.log("enter: "+JSON.stringify(node.props._restingStyle));
}
this.nodes[k] = node;
},
render: function() {
- var self = this;
- var childList = Object.keys(this.children).map(function(k) {
- return React.cloneElement(self.children[k], {
- ref: self.collectNode.bind(self, self.children[k].key)
- });
- });
return (
- {childList}
+ {Object.values(this.children)}
);
},
diff --git a/src/component-index.js b/src/component-index.js
index 0cb7e257a0..967cc5d685 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -64,11 +64,11 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie
module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin');
module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm');
module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig');
+module.exports.components['views.messages.MAudioBody'] = require('./components/views/messages/MAudioBody');
module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody');
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
-module.exports.components['views.messages.MAudioBody'] = require('./components/views/messages/MAudioBody');
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
@@ -85,6 +85,7 @@ module.exports.components['views.rooms.MemberTile'] = require('./components/view
module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer');
module.exports.components['views.rooms.MessageComposerInput'] = require('./components/views/rooms/MessageComposerInput');
module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel');
+module.exports.components['views.rooms.ReadReceiptMarker'] = require('./components/views/rooms/ReadReceiptMarker');
module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader');
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
module.exports.components['views.rooms.RoomNameEditor'] = require('./components/views/rooms/RoomNameEditor');
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index defc8151a9..49f4783eaa 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -81,6 +81,10 @@ module.exports = React.createClass({
// the event after which we are showing a disappearing read marker
// animation
this.currentGhostEventId = null;
+
+ // opaque readreceipt info for each userId; used by ReadReceiptMarker
+ // to manage its animations
+ this._readReceiptMap = {};
},
/* get the DOM node representing the given event */
@@ -346,6 +350,7 @@ module.exports = React.createClass({
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 5367b4208a..289dd4be25 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -31,7 +31,7 @@ var KeyCode = require('../../KeyCode');
var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20;
-var TIMELINE_CAP = 500; // the most events to show in a timeline
+var TIMELINE_CAP = 250; // the most events to show in a timeline
var DEBUG = false;
@@ -241,11 +241,25 @@ var TimelinePanel = React.createClass({
if (this.unmounted) { return; }
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
- this.setState({
+
+ var newState = {
[paginatingKey]: false,
[canPaginateKey]: r,
events: this._getEvents(),
- });
+ };
+
+ // moving the window in this direction may mean that we can now
+ // paginate in the other where we previously could not.
+ var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
+ var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
+ if (!this.state[canPaginateOtherWayKey] &&
+ this._timelineWindow.canPaginate(otherDirection)) {
+ debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
+ newState[canPaginateOtherWayKey] = true;
+ }
+
+ this.setState(newState);
+
return r;
});
},
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 631caadac2..223eabdc36 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -84,11 +84,12 @@ module.exports = React.createClass({
findLink: function(nodes) {
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
- if (node.tagName === "A" && node.getAttribute("href") &&
- (node.getAttribute("href").startsWith("http://") ||
- node.getAttribute("href").startsWith("https://")))
+ if (node.tagName === "A" && node.getAttribute("href"))
{
- return node;
+ return this.isLinkPreviewable(node) ? node : undefined;
+ }
+ else if (node.tagName === "PRE" || node.tagName === "CODE") {
+ return;
}
else if (node.children && node.children.length) {
return this.findLink(node.children)
@@ -96,6 +97,37 @@ module.exports = React.createClass({
}
},
+ isLinkPreviewable: function(node) {
+ // don't try to preview relative links
+ if (!node.getAttribute("href").startsWith("http://") &&
+ !node.getAttribute("href").startsWith("https://"))
+ {
+ return false;
+ }
+
+ // as a random heuristic to avoid highlighting things like "foo.pl"
+ // we require the linked text to either include a / (either from http://
+ // or from a full foo.bar/baz style schemeless URL) - or be a markdown-style
+ // link, in which case we check the target text differs from the link value.
+ // TODO: make this configurable?
+ if (node.textContent.indexOf("/") > -1)
+ {
+ return node;
+ }
+ else {
+ var url = node.getAttribute("href");
+ var host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
+ if (node.textContent.trim().startsWith(host)) {
+ // it's a "foo.pl" style link
+ return;
+ }
+ else {
+ // it's a [foo bar](http://foo.com) style link
+ return node;
+ }
+ }
+ },
+
onCancelClick: function(event) {
this.setState({ widgetHidden: true });
// FIXME: persist this somewhere smarter than local storage
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 3fe6a08f97..5c70c9da10 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -17,7 +17,6 @@ limitations under the License.
'use strict';
var React = require('react');
-var ReactDom = require('react-dom');
var classNames = require("classnames");
var sdk = require('../../../index');
@@ -25,8 +24,6 @@ var MatrixClientPeg = require('../../../MatrixClientPeg')
var TextForEvent = require('../../../TextForEvent');
var ContextualMenu = require('../../../ContextualMenu');
-var Velociraptor = require('../../../Velociraptor');
-require('../../../VelocityBounce');
var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils');
@@ -113,6 +110,12 @@ module.exports = React.createClass({
/* a list of Room Members whose read-receipts we should show */
readReceipts: React.PropTypes.arrayOf(React.PropTypes.object),
+ /* opaque readreceipt info for each userId; used by ReadReceiptMarker
+ * to manage its animations. Should be an empty object when the room
+ * first loads
+ */
+ readReceiptMap: React.PropTypes.object,
+
/* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */
eventSendStatus: React.PropTypes.string,
@@ -122,6 +125,15 @@ module.exports = React.createClass({
return {menu: false, allReadAvatars: false};
},
+ componentWillMount: function() {
+ // don't do RR animations until we are mounted
+ this._suppressReadReceiptAnimation = true;
+ },
+
+ componentDidMount: function() {
+ this._suppressReadReceiptAnimation = false;
+ },
+
shouldComponentUpdate: function (nextProps, nextState) {
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
return true;
@@ -217,80 +229,53 @@ module.exports = React.createClass({
},
getReadAvatars: function() {
+ var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
var avatars = [];
- var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var left = 0;
- var reorderTransitionOpts = {
- duration: 100,
- easing: 'easeOut'
- };
-
var receipts = this.props.readReceipts || [];
for (var i = 0; i < receipts.length; ++i) {
var member = receipts[i];
- // Using react refs here would mean both getting Velociraptor to expose
- // them and making them scoped to the whole RoomView. Not impossible, but
- // getElementById seems simpler at least for a first cut.
- var oldAvatarDomNode = document.getElementById('mx_readAvatar'+member.userId);
- var startStyles = [];
- var enterTransitionOpts = [];
- var oldNodeTop = -15; // For avatars that weren't on screen, act as if they were just off the top
- if (oldAvatarDomNode) {
- oldNodeTop = oldAvatarDomNode.getBoundingClientRect().top;
+ var hidden = true;
+ if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
+ hidden = false;
}
- if (this.readAvatarNode) {
- var topOffset = oldNodeTop - this.readAvatarNode.getBoundingClientRect().top;
+ var userId = member.userId;
+ var readReceiptInfo;
- if (oldAvatarDomNode && oldAvatarDomNode.style.left !== '0px') {
- var leftOffset = oldAvatarDomNode.style.left;
- // start at the old height and in the old h pos
- startStyles.push({ top: topOffset, left: leftOffset });
- enterTransitionOpts.push(reorderTransitionOpts);
+ if (this.props.readReceiptMap) {
+ readReceiptInfo = this.props.readReceiptMap[userId];
+ if (!readReceiptInfo) {
+ readReceiptInfo = {};
+ this.props.readReceiptMap[userId] = readReceiptInfo;
}
-
- // then shift to the rightmost column,
- // and then it will drop down to its resting position
- startStyles.push({ top: topOffset, left: '0px' });
- enterTransitionOpts.push({
- duration: bounce ? Math.min(Math.log(Math.abs(topOffset)) * 200, 3000) : 300,
- easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
- });
}
- var style = {
- left: left+'px',
- top: '0px',
- visibility: ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) ? 'visible' : 'hidden'
- };
-
//console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility);
// add to the start so the most recent is on the end (ie. ends up rightmost)
avatars.unshift(
-
);
+
// TODO: we keep the extra read avatars in the dom to make animation simpler
// we could optimise this to reduce the dom size.
- if (i < MAX_READ_AVATARS - 1 || this.state.allReadAvatars) { // XXX: where does this -1 come from? is it to make the max'th avatar animate properly?
+ if (!hidden) {
left -= 15;
}
}
var editButton;
+ var remText;
if (!this.state.allReadAvatars) {
var remainder = receipts.length - MAX_READ_AVATARS;
- var remText;
- if (i >= MAX_READ_AVATARS - 1) left -= 15;
if (remainder > 0) {
remText =
+ return
{ editButton }
{ remText }
-
- { avatars }
-
+ { avatars }
;
},
- collectReadAvatarNode: function(node) {
- this.readAvatarNode = ReactDom.findDOMNode(node);
- },
-
onMemberAvatarClick: function(event) {
dispatcher.dispatch({
action: 'view_user',
diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
new file mode 100644
index 0000000000..bc433b8bd0
--- /dev/null
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -0,0 +1,166 @@
+/*
+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.
+*/
+
+'use strict';
+
+var React = require('react');
+var ReactDOM = require('react-dom');
+
+var sdk = require('../../../index');
+
+var Velociraptor = require('../../../Velociraptor');
+require('../../../VelocityBounce');
+
+var bounce = false;
+try {
+ if (global.localStorage) {
+ bounce = global.localStorage.getItem('avatar_bounce') == 'true';
+ }
+} catch (e) {
+}
+
+module.exports = React.createClass({
+ displayName: 'ReadReceiptMarker',
+
+ propTypes: {
+ // the RoomMember to show the RR for
+ member: React.PropTypes.object.isRequired,
+
+ // number of pixels to offset the avatar from the right of its parent;
+ // typically a negative value.
+ leftOffset: React.PropTypes.number,
+
+ // true to hide the avatar (it will still be animated)
+ hidden: React.PropTypes.bool,
+
+ // don't animate this RR into position
+ suppressAnimation: React.PropTypes.bool,
+
+ // an opaque object for storing information about this user's RR in
+ // this room
+ readReceiptInfo: React.PropTypes.object,
+
+ // callback for clicks on this RR
+ onClick: React.PropTypes.func,
+ },
+
+ getDefaultProps: function() {
+ return {
+ leftOffset: 0,
+ }
+ },
+
+ getInitialState: function() {
+ // if we are going to animate the RR, we don't show it on first render,
+ // and instead just add a placeholder to the DOM; once we've been
+ // mounted, we start an animation which moves the RR from its old
+ // position.
+ return {
+ suppressDisplay: !this.props.suppressAnimation,
+ }
+ },
+
+ componentWillUnmount: function() {
+ // before we remove the rr, store its location in the map, so that if
+ // it reappears, it can be animated from the right place.
+ var rrInfo = this.props.readReceiptInfo;
+ if (!rrInfo) {
+ return;
+ }
+
+ var avatarNode = ReactDOM.findDOMNode(this);
+ rrInfo.top = avatarNode.offsetTop;
+ rrInfo.left = avatarNode.offsetLeft;
+ rrInfo.parent = avatarNode.offsetParent;
+ },
+
+ componentDidMount: function() {
+ if (!this.state.suppressDisplay) {
+ // we've already done our display - nothing more to do.
+ return;
+ }
+
+ // treat new RRs as though they were off the top of the screen
+ var oldTop = -15;
+
+ var oldInfo = this.props.readReceiptInfo;
+ if (oldInfo && oldInfo.parent) {
+ oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
+ }
+
+ var newElement = ReactDOM.findDOMNode(this);
+ var startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
+
+ var startStyles = [];
+ var enterTransitionOpts = [];
+
+ if (oldInfo && oldInfo.left) {
+ // start at the old height and in the old h pos
+
+ var leftOffset = oldInfo.left;
+ startStyles.push({ top: startTopOffset+"px",
+ left: oldInfo.left+"px" });
+
+ var reorderTransitionOpts = {
+ duration: 100,
+ easing: 'easeOut'
+ };
+
+ enterTransitionOpts.push(reorderTransitionOpts);
+ }
+
+ // then shift to the rightmost column,
+ // and then it will drop down to its resting position
+ startStyles.push({ top: startTopOffset+'px', left: '0px' });
+ enterTransitionOpts.push({
+ duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
+ easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
+ });
+
+ this.setState({
+ suppressDisplay: false,
+ startStyles: startStyles,
+ enterTransitionOpts: enterTransitionOpts,
+ });
+ },
+
+
+ render: function() {
+ var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
+ if (this.state.suppressDisplay) {
+ return ;
+ }
+
+ var style = {
+ left: this.props.leftOffset+'px',
+ top: '0px',
+ visibility: this.props.hidden ? 'hidden' : 'visible',
+ };
+
+ return (
+
+
+
+ );
+ },
+});
diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js
index 671d5c7774..c201c647c6 100644
--- a/test/components/structures/TimelinePanel-test.js
+++ b/test/components/structures/TimelinePanel-test.js
@@ -76,7 +76,8 @@ describe('TimelinePanel', function() {
afterEach(function() {
if (parentDiv) {
- document.body.removeChild(parentDiv);
+ ReactDOM.unmountComponentAtNode(parentDiv);
+ parentDiv.remove();
parentDiv = null;
}
sandbox.restore();
@@ -204,4 +205,72 @@ describe('TimelinePanel', function() {
}, 0);
}, 0);
});
+
+ it("should let you scroll down again after you've scrolled up", function(done) {
+ var N_EVENTS = 600;
+
+ // sadly, loading all those events takes a while
+ this.timeout(N_EVENTS * 20);
+
+ // client.getRoom is called a /lot/ in this test, so replace
+ // sinon's spy with a fast noop.
+ client.getRoom = function(id) { return null; };
+
+ // fill the timeline with lots of events
+ for (var i = 0; i < N_EVENTS; i++) {
+ timeline.addEvent(mkMessage());
+ }
+
+ var scrollDefer;
+ var panel = ReactDOM.render(
+ {scrollDefer.resolve()}} />,
+ parentDiv
+ );
+
+ var messagePanel = ReactTestUtils.findRenderedComponentWithType(
+ panel, sdk.getComponent('structures.MessagePanel'));
+ var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
+ panel, "gm-scroll-view");
+
+ // helper function which will return a promise which resolves when
+ // the TimelinePanel fires a scroll event
+ var awaitScroll = function() {
+ scrollDefer = q.defer();
+ return scrollDefer.promise;
+ };
+
+ function backPaginate() {
+ scrollingDiv.scrollTop = 0;
+ return awaitScroll().then(() => {
+ if(scrollingDiv.scrollTop > 0) {
+ // need to go further
+ return backPaginate();
+ }
+
+ // hopefully, we got to the start of the timeline
+ expect(messagePanel.props.backPaginating).toBe(false);
+ });
+ }
+
+ // let the first round of pagination finish off
+ awaitScroll().then(() => {
+ // we should now have loaded the first few events
+ expect(messagePanel.props.backPaginating).toBe(false);
+ expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
+
+ // back-paginate until we hit the start
+ return backPaginate();
+ }).then(() => {
+ expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
+ var events = scryEventTiles(panel);
+ expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0])
+
+ // we should now be able to scroll down, and paginate in the other
+ // direction.
+ scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
+ return awaitScroll();
+ }).then(() => {
+ console.log("done");
+ }).done(done, done);
+ });
});
diff --git a/test/img/edit.png b/test/img/edit.png
new file mode 100644
index 0000000000..6f373d3f3d
Binary files /dev/null and b/test/img/edit.png differ
diff --git a/test/skinned-sdk.js b/test/skinned-sdk.js
index 5d3526b797..869902dcf6 100644
--- a/test/skinned-sdk.js
+++ b/test/skinned-sdk.js
@@ -5,6 +5,14 @@
* application to provide
*/
+/* this is a convenient place to ensure we load the compatibility libraries we expect our
+ * app to provide
+ */
+
+// for ES6 stuff like startsWith() and Object.values() that babel doesn't do by
+// default
+require('babel-polyfill');
+
var sdk = require("../src/index");
var skin = require('../src/component-index.js');
diff --git a/test/test-utils.js b/test/test-utils.js
index a077722678..817af31d45 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -37,13 +37,14 @@ module.exports.stubClient = function() {
getRoom: sinon.stub(),
loginFlows: sinon.stub(),
on: sinon.stub(),
+ removeListener: sinon.stub(),
paginateEventTimeline: sinon.stub().returns(q()),
sendReadReceipt: sinon.stub().returns(q()),
};
- // create the peg
-
+ // stub out the methods in MatrixClientPeg
+ //
// 'sandbox.restore()' doesn't work correctly on inherited methods,
// so we do this for each method
var methods = ['get', 'unset', 'replaceUsingUrls',
@@ -51,7 +52,9 @@ module.exports.stubClient = function() {
for (var i = 0; i < methods.length; i++) {
sandbox.stub(peg, methods[i]);
}
- peg.get.returns(client);
+ // MatrixClientPeg.get() is called a /lot/, so implement it with our own
+ // fast stub function rather than a sinon stub
+ peg.get = function() { return client; };
return sandbox;
}