diff --git a/src/components/withBatchedRendering.js b/src/components/withBatchedRendering.js new file mode 100644 index 0000000000000..97b81ed05b2b9 --- /dev/null +++ b/src/components/withBatchedRendering.js @@ -0,0 +1,81 @@ +/** + * This HOC is used to incrementally display a collection of data in batches. + * Example: in MainView.js only the visible reports should be rendered in the first batch, + * then all other reports are rendered in the second batch + */ +import React, {Component} from 'react'; +import _ from 'underscore'; + +/** + * Returns the display name of a component + * + * @param {object} component + * @returns {string} + */ +function getDisplayName(component) { + return component.displayName || component.name || 'Component'; +} + +export default function (propNameToBatch, batches) { + return (WrappedComponent) => { + class withBatchedRendering extends Component { + constructor(props) { + super(props); + this.timers = []; + this.state = {}; + } + + componentDidMount() { + _.each(batches, (batch) => { + this.timers.push( + setTimeout(() => { + this.setItemsToRender(batch.items(this.props)); + }, batch.delay || 0) + ); + }); + } + + componentDidUpdate(prevProps) { + // We need this to allow the flow of props down from a parent component + // to work normally after all the batches have finished rendering + if (_.size(prevProps[propNameToBatch]) !== _.size(this.props[propNameToBatch])) { + this.setItemsToRender(this.props[propNameToBatch]); + } + } + + componentWillUnmount() { + // We need to clean up any timers when the component unmounts or else + // we'll call set state on an unmounting component. + _.each(this.timers, timerID => clearTimeout(timerID)); + } + + /** + * Sets items to the state key that matches the defined propNameToBatch + * + * @param {Object|Array} items - typically a collection of some kind + */ + setItemsToRender(items) { + this.setState({ + [propNameToBatch]: items, + }); + } + + render() { + // We must remove the original prop that we are splitting into chunks + // since we only want our processed versions to be passed as a prop. + const propsToPass = _.omit(this.props, propNameToBatch); + return ( + + ); + } + } + + withBatchedRendering.displayName = `withBatchedRendering(${getDisplayName(WrappedComponent)})`; + return withBatchedRendering; + }; +} diff --git a/src/pages/home/MainView.js b/src/pages/home/MainView.js index d23a00a5a516b..f72b4c91a9ca9 100644 --- a/src/pages/home/MainView.js +++ b/src/pages/home/MainView.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import ReportView from './report/ReportView'; import withIon from '../../components/withIon'; import IONKEYS from '../../IONKEYS'; +import withBatchedRendering from '../../components/withBatchedRendering'; import styles from '../../styles/StyleSheet'; import {withRouter} from '../../libs/Router'; import compose from '../../libs/compose'; @@ -27,6 +28,19 @@ const defaultProps = { }; class MainView extends Component { + /** + * Looks to see if the reportID matches the report ID in the URL because that + * report needs to be the one that is visible while all the other reports are hidden + * + * @param {number} reportID + * + * @returns {boolean} + */ + isReportIDMatchingURL(reportID) { + const reportIDInURL = parseInt(this.props.match.params.reportID, 10); + return reportID === reportIDInURL; + } + render() { const reportIDInUrl = parseInt(this.props.match.params.reportID, 10); @@ -82,4 +96,26 @@ export default compose( key: IONKEYS.COLLECTION.REPORT, }, }), + + // The rendering of report views is done in batches. + // The first batch are all the reports that are visible in the LHN. + // The second batch are all the reports. + withBatchedRendering('reports', [ + { + items: (props) => { + const reportIDInURL = parseInt(props.match.params.reportID, 10); + const isReportVisible = report => ( + report.isUnread + || report.pinnedReport + || report.reportID === reportIDInURL + ); + return _.pick(props.reports, isReportVisible); + }, + delay: 0, + }, + { + items: props => props.reports, + delay: 5000, + }, + ]), )(MainView); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8c2770c241b43..81cc4d29d45ba 100644 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -10,6 +10,8 @@ import IONKEYS from '../../../IONKEYS'; import ReportActionItem from './ReportActionItem'; import styles from '../../../styles/StyleSheet'; import ReportActionPropTypes from './ReportActionPropTypes'; +import compose from '../../../libs/compose'; +import withBatchedRendering from '../../../components/withBatchedRendering'; import InvertedFlatList from '../../../components/InvertedFlatList'; import {lastItem} from '../../../libs/CollectionUtils'; @@ -216,11 +218,33 @@ class ReportActionsView extends React.Component { ReportActionsView.propTypes = propTypes; ReportActionsView.defaultProps = defaultProps; -export default withIon({ - reportActions: { - key: ({reportID}) => `${IONKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - }, - session: { - key: IONKEYS.SESSION, - }, -})(ReportActionsView); +export default compose( + withIon({ + reportActions: { + key: ({reportID}) => `${IONKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + }, + session: { + key: IONKEYS.SESSION, + }, + }), + + // The rendering of report actions happens in batches. + // The first batch of actions is limited to the 100 most recent actions. + // The second batch is all of the rest of the actions. + withBatchedRendering('reportActions', [ + { + items: (props) => { + const sortedReportActions = _.sortBy(props.reportActions, 'sequenceNumber'); + return _.chain(sortedReportActions).last(100).indexBy('sequenceNumber').value(); + }, + delay: 0, + }, + { + items: (props) => { + const sortedReportActions = _.sortBy(props.reportActions, 'sequenceNumber'); + return _.indexBy(sortedReportActions, 'sequenceNumber'); + }, + delay: 7000, + }, + ]), +)(ReportActionsView); diff --git a/src/pages/home/sidebar/ChatSwitcherView.js b/src/pages/home/sidebar/ChatSwitcherView.js index 45a935009bc0d..3af194c5c975d 100644 --- a/src/pages/home/sidebar/ChatSwitcherView.js +++ b/src/pages/home/sidebar/ChatSwitcherView.js @@ -296,7 +296,7 @@ class ChatSwitcherView extends React.Component { } }} onChangeText={this.updateSearch} - onClearButtonClick={this.reset} + onClearButtonClick={() => this.reset()} onFocus={this.triggerOnFocusCallback} onKeyPress={this.handleKeyPress} />