Avoid rerendering EventTiles when not necessary

Each individual eventtile isn't particularly expensive, but when you have 500
of them, they start adding up. Shuffle some of the stuff into MessagePanel, so
that we can shouldComponentUpdate EventTiles properly.
This commit is contained in:
Richard van der Hoff 2016-04-19 19:38:54 +01:00
parent 8bb13db7d4
commit 568e7aef8b
2 changed files with 94 additions and 23 deletions

View file

@ -19,6 +19,8 @@ var ReactDOM = require("react-dom");
var dis = require("../../dispatcher"); var dis = require("../../dispatcher");
var sdk = require('../../index'); var sdk = require('../../index');
var MatrixClientPeg = require('../../MatrixClientPeg')
/* (almost) stateless UI component which builds the event tiles in the room timeline. /* (almost) stateless UI component which builds the event tiles in the room timeline.
*/ */
module.exports = React.createClass({ module.exports = React.createClass({
@ -150,7 +152,7 @@ module.exports = React.createClass({
this.refs.scrollPanel.scrollToBottom(); this.refs.scrollPanel.scrollToBottom();
} }
}, },
/** /**
* Page up/down. * Page up/down.
* *
@ -335,13 +337,17 @@ module.exports = React.createClass({
// Local echos have a send "status". // Local echos have a send "status".
var scrollToken = mxEv.status ? undefined : eventId; var scrollToken = mxEv.status ? undefined : eventId;
var readReceipts = this._getReadReceiptsForEvent(mxEv);
ret.push( ret.push(
<li key={eventId} <li key={eventId}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}> data-scroll-token={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation} <EventTile mxEvent={mxEv} continuation={continuation}
onWidgetLoad={this._onWidgetLoad} onWidgetLoad={this._onWidgetLoad}
last={last} isSelectedEvent={highlight} /> readReceipts={readReceipts}
eventSendStatus={mxEv.status}
last={last} isSelectedEvent={highlight}/>
</li> </li>
); );
@ -359,6 +365,30 @@ module.exports = React.createClass({
!== new Date(nextEventTs).toDateString()); !== new Date(nextEventTs).toDateString());
}, },
// get a list of the userids whose read receipts should
// be shown next to this event
_getReadReceiptsForEvent: function(event) {
var myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first
var room = MatrixClientPeg.get().getRoom(event.getRoomId());
if (!room) {
// huh.
return null;
}
return room.getReceiptsForEvent(event).filter(function(r) {
return r.type === "m.read" && r.userId != myUserId;
}).sort(function(r1, r2) {
return r2.data.ts - r1.data.ts;
}).map(function(r) {
return room.getMember(r.userId);
}).filter(function(m) {
// check that the user is a known room member
return m;
});
},
_getReadMarkerTile: function(visible) { _getReadMarkerTile: function(visible) {
var hr; var hr;
if (visible) { if (visible) {
@ -431,7 +461,7 @@ module.exports = React.createClass({
return ( return (
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel mx_fadable" <ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel mx_fadable"
onScroll={ this.props.onScroll } onScroll={ this.props.onScroll }
onResize={ this.onResize } onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest } onFillRequest={ this.props.onFillRequest }
style={ style } style={ style }

View file

@ -29,6 +29,8 @@ var Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce'); require('../../../VelocityBounce');
var dispatcher = require("../../../dispatcher"); var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils');
var bounce = false; var bounce = false;
try { try {
if (global.localStorage) { if (global.localStorage) {
@ -107,12 +109,67 @@ module.exports = React.createClass({
/* callback called when dynamic content in events are loaded */ /* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func, onWidgetLoad: React.PropTypes.func,
/* a list of Room Members whose read-receipts we should show */
readReceipts: React.PropTypes.arrayOf(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,
}, },
getInitialState: function() { getInitialState: function() {
return {menu: false, allReadAvatars: false}; return {menu: false, allReadAvatars: false};
}, },
shouldComponentUpdate: function (nextProps, nextState) {
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
return true;
}
if (!this._propsEqual(this.props, nextProps)) {
return true;
}
return false;
},
_propsEqual: function(objA, objB) {
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (var i = 0; i < keysA.length; i++) {
var key = keysA[i];
if (!objB.hasOwnProperty(key)) {
return false;
}
// need to deep-compare readReceipts
if (key == 'readReceipts') {
var rA = objA[key];
var rB = objB[key];
if (rA.length !== rB.length) {
return false;
}
for (var j = 0; j < rA.length; j++) {
if (rA[j].userId !== rB[j].userId) {
return false;
}
}
} else {
if (objA[key] !== objB[key]) {
return false;
}
}
}
return true;
},
shouldHighlight: function() { shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent); var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; } if (!actions || !actions.tweaks) { return false; }
@ -153,20 +210,6 @@ module.exports = React.createClass({
getReadAvatars: function() { getReadAvatars: function() {
var avatars = []; var avatars = [];
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
if (!room) return [];
var myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first
var receipts = room.getReceiptsForEvent(this.props.mxEvent).filter(function(r) {
return r.type === "m.read" && r.userId != myUserId;
}).sort(function(r1, r2) {
return r2.data.ts - r1.data.ts;
});
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var left = 0; var left = 0;
@ -176,11 +219,9 @@ module.exports = React.createClass({
easing: 'easeOut' easing: 'easeOut'
}; };
var receipts = this.props.readReceipts || [];
for (var i = 0; i < receipts.length; ++i) { for (var i = 0; i < receipts.length; ++i) {
var member = room.getMember(receipts[i].userId); var member = receipts[i];
if (!member) {
continue;
}
// Using react refs here would mean both getting Velociraptor to expose // Using react refs here would mean both getting Velociraptor to expose
// them and making them scoped to the whole RoomView. Not impossible, but // them and making them scoped to the whole RoomView. Not impossible, but
@ -302,9 +343,9 @@ module.exports = React.createClass({
var classes = classNames({ var classes = classNames({
mx_EventTile: true, mx_EventTile: true,
mx_EventTile_sending: ['sending', 'queued'].indexOf( mx_EventTile_sending: ['sending', 'queued'].indexOf(
this.props.mxEvent.status this.props.eventSendStatus
) !== -1, ) !== -1,
mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent', mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent',
mx_EventTile_highlight: this.shouldHighlight(), mx_EventTile_highlight: this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.continuation, mx_EventTile_continuation: this.props.continuation,