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) } +
); + } +}