diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js
index a385df0401..0f93f20407 100644
--- a/src/components/structures/AutoHideScrollbar.js
+++ b/src/components/structures/AutoHideScrollbar.js
@@ -114,10 +114,15 @@ export default class AutoHideScrollbar extends React.Component {
}
}
+ getScrollTop() {
+ return this.containerRef.scrollTop;
+ }
+
render() {
return (
{ this.props.children }
diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js
index c3e54ee900..e1516d1f64 100644
--- a/src/components/structures/IndicatorScrollbar.js
+++ b/src/components/structures/IndicatorScrollbar.js
@@ -59,6 +59,10 @@ export default class IndicatorScrollbar extends React.Component {
}
}
+ getScrollTop() {
+ return this._autoHideScrollbar.getScrollTop();
+ }
+
componentWillUnmount() {
if (this._scrollElement) {
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index ca2be85b35..f7f74da728 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -27,7 +27,8 @@ import IndicatorScrollbar from './IndicatorScrollbar';
import { KeyCode } from '../../Keyboard';
import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
-
+import RoomTile from "../views/rooms/RoomTile";
+import LazyRenderList from "../views/elements/LazyRenderList";
// turn this on for drop & drag console debugging galore
const debug = false;
@@ -60,6 +61,9 @@ const RoomSubList = React.createClass({
getInitialState: function() {
return {
hidden: this.props.startAsHidden || false,
+ // some values to get LazyRenderList starting
+ scrollerHeight: 800,
+ scrollTop: 0,
};
},
@@ -134,24 +138,21 @@ const RoomSubList = React.createClass({
this.setState(this.state);
},
- makeRoomTiles: function() {
- const RoomTile = sdk.getComponent("rooms.RoomTile");
- return this.props.list.map((room, index) => {
- return 0 || this.props.isInvite}
- notificationCount={room.getUnreadNotificationCount()}
- isInvite={this.props.isInvite}
- refreshSubList={this._updateSubListCount}
- incomingCall={null}
- onClick={this.onRoomTileClick}
- />;
- });
+ makeRoomTile: function(room) {
+ return 0 || this.props.isInvite}
+ notificationCount={room.getUnreadNotificationCount()}
+ isInvite={this.props.isInvite}
+ refreshSubList={this._updateSubListCount}
+ incomingCall={null}
+ onClick={this.onRoomTileClick}
+ />;
},
_onNotifBadgeClick: function(e) {
@@ -270,6 +271,29 @@ const RoomSubList = React.createClass({
if (this.refs.subList) {
this.refs.subList.style.height = `${height}px`;
}
+ this._updateLazyRenderHeight(height);
+ },
+
+ _updateLazyRenderHeight: function(height) {
+ this.setState({scrollerHeight: height});
+ },
+
+ _onScroll: function() {
+ this.setState({scrollTop: this.refs.scroller.getScrollTop()});
+ },
+
+ _getRenderItems: function() {
+ // try our best to not create a new array
+ // because LazyRenderList rerender when the items prop
+ // is not the same object as the previous value
+ const {list, extraTiles} = this.props;
+ if (!extraTiles || !extraTiles.length) {
+ return list;
+ }
+ if (!list || list.length) {
+ return extraTiles;
+ }
+ return list.concat(extraTiles);
},
render: function() {
@@ -287,12 +311,15 @@ const RoomSubList = React.createClass({
{this._getHeaderJsx(isCollapsed)}
;
} else {
- const tiles = this.makeRoomTiles();
- tiles.push(...this.props.extraTiles);
return
{this._getHeaderJsx(isCollapsed)}
-
- { tiles }
+
+
;
}
diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.js
new file mode 100644
index 0000000000..b7916510a4
--- /dev/null
+++ b/src/components/views/elements/LazyRenderList.js
@@ -0,0 +1,92 @@
+/*
+Copyright 2019 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+
+const OVERFLOW_ITEMS = 20;
+const OVERFLOW_MARGIN = 5;
+
+class ItemRange {
+ constructor(topCount, renderCount, bottomCount) {
+ this.topCount = topCount;
+ this.renderCount = renderCount;
+ this.bottomCount = bottomCount;
+ }
+
+ contains(range) {
+ return range.topCount >= this.topCount &&
+ (range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
+ }
+
+ expand(amount) {
+ const topGrow = Math.min(amount, this.topCount);
+ const bottomGrow = Math.min(amount, this.bottomCount);
+ return new ItemRange(
+ this.topCount - topGrow,
+ this.renderCount + topGrow + bottomGrow,
+ this.bottomCount - bottomGrow,
+ );
+ }
+}
+
+export default class LazyRenderList extends React.Component {
+ constructor(props) {
+ super(props);
+ const renderRange = LazyRenderList.getVisibleRangeFromProps(props).expand(OVERFLOW_ITEMS);
+ this.state = {renderRange};
+ }
+
+ static getVisibleRangeFromProps(props) {
+ const {items, itemHeight, scrollTop, height} = props;
+ const length = items ? items.length : 0;
+ const topCount = Math.max(0, Math.floor(scrollTop / itemHeight));
+ const itemsAfterTop = length - topCount;
+ const renderCount = Math.min(Math.ceil(height / itemHeight), itemsAfterTop);
+ const bottomCount = itemsAfterTop - renderCount;
+ return new ItemRange(topCount, renderCount, bottomCount);
+ }
+
+ componentWillReceiveProps(props) {
+ const state = this.state;
+ const range = LazyRenderList.getVisibleRangeFromProps(props);
+ // only update state if the new range isn't contained by the old anymore
+ if (!state.renderRange || !state.renderRange.contains(range.expand(OVERFLOW_MARGIN))) {
+ this.setState({renderRange: range.expand(OVERFLOW_ITEMS)});
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const itemsChanged = nextProps.items !== this.props.items;
+ const rangeChanged = nextState.renderRange !== this.state.renderRange;
+ return itemsChanged || rangeChanged;
+ }
+
+ render() {
+ const {itemHeight, items, renderItem} = this.props;
+
+ const {renderRange} = this.state;
+ const paddingTop = renderRange.topCount * itemHeight;
+ const paddingBottom = renderRange.bottomCount * itemHeight;
+ const renderedItems = (items || []).slice(
+ renderRange.topCount,
+ renderRange.topCount + renderRange.renderCount,
+ );
+
+ return (
+ { renderedItems.map(renderItem) }
+
);
+ }
+}