diff --git a/patches/react-virtualized+9.21.0.patch b/patches/react-virtualized+9.21.0.patch index 8a064ec0e..da3832c52 100644 --- a/patches/react-virtualized+9.21.0.patch +++ b/patches/react-virtualized+9.21.0.patch @@ -23,8 +23,54 @@ index d9716a0..e7a9f9f 100644 }); } } +diff --git a/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurerCache.js b/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurerCache.js +index 262776b..95d6a80 100644 +--- a/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurerCache.js ++++ b/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurerCache.js +@@ -65,6 +65,7 @@ var CellMeasurerCache = function () { + minWidth = params.minWidth; + + ++ this._highWaterMark = 0; + this._hasFixedHeight = fixedHeight === true; + this._hasFixedWidth = fixedWidth === true; + this._minHeight = minHeight || 0; +@@ -101,6 +102,24 @@ var CellMeasurerCache = function () { + + this._updateCachedColumnAndRowSizes(rowIndex, columnIndex); + } ++ }, { ++ key: 'clearPlus', ++ value: function clearPlus(rowIndex) { ++ var columnIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; ++ ++ if (this._highWaterMark <= rowIndex) { ++ this.clear(rowIndex, columnIndex); ++ return; ++ } ++ ++ for (let i = rowIndex, max = this._highWaterMark; i <= max; i += 1) { ++ var key = this._keyMapper(i, columnIndex); ++ delete this._cellHeightCache[key]; ++ delete this._cellWidthCache[key]; ++ } ++ ++ this._highWaterMark = Math.max(0, rowIndex - 1); ++ } + }, { + key: 'clearAll', + value: function clearAll() { +@@ -168,6 +187,8 @@ var CellMeasurerCache = function () { + this._rowCount = rowIndex + 1; + } + ++ this._highWaterMark = Math.max(this._highWaterMark, rowIndex); ++ + // Size is cached per cell so we don't have to re-measure if cells are re-ordered. + this._cellHeightCache[key] = height; + this._cellWidthCache[key] = width; diff --git a/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js b/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js -index e1b959a..18f8f1d 100644 +index e1b959a..31f3b56 100644 --- a/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js +++ b/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js @@ -132,6 +132,9 @@ var Grid = function (_React$PureComponent) { @@ -50,17 +96,21 @@ index e1b959a..18f8f1d 100644 }); } -@@ -362,6 +369,9 @@ var Grid = function (_React$PureComponent) { - value: function invalidateCellSizeAfterRender(_ref3) { +@@ -363,6 +370,13 @@ var Grid = function (_React$PureComponent) { var columnIndex = _ref3.columnIndex, rowIndex = _ref3.rowIndex; -+ if (!this._disableCellUpdates) { -+ this._cellUpdates.push(_ref3); -+ } ++ if (columnIndex < this._lastColumnStartIndex) { ++ this._cellUpdates.push({ columnIndex, widthChange: _ref3.widthChange }); ++ } ++ if (rowIndex < this._lastRowStartIndex) { ++ this._cellUpdates.push({ rowIndex, heightChange: _ref3.heightChange }); ++ } ++ this._deferredInvalidateColumnIndex = typeof this._deferredInvalidateColumnIndex === 'number' ? Math.min(this._deferredInvalidateColumnIndex, columnIndex) : columnIndex; this._deferredInvalidateRowIndex = typeof this._deferredInvalidateRowIndex === 'number' ? Math.min(this._deferredInvalidateRowIndex, rowIndex) : rowIndex; -@@ -381,8 +391,12 @@ var Grid = function (_React$PureComponent) { + } +@@ -381,8 +395,12 @@ var Grid = function (_React$PureComponent) { rowCount = _props2.rowCount; var instanceProps = this.state.instanceProps; @@ -75,23 +125,24 @@ index e1b959a..18f8f1d 100644 } /** -@@ -415,6 +429,15 @@ var Grid = function (_React$PureComponent) { +@@ -415,6 +433,16 @@ var Grid = function (_React$PureComponent) { this._recomputeScrollLeftFlag = scrollToColumn >= 0 && (this.state.scrollDirectionHorizontal === _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD ? columnIndex <= scrollToColumn : columnIndex >= scrollToColumn); this._recomputeScrollTopFlag = scrollToRow >= 0 && (this.state.scrollDirectionVertical === _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD ? rowIndex <= scrollToRow : rowIndex >= scrollToRow); ++ // Important to ensure that when we, say, change the width of the viewport, ++ // we don't re-render, capture deltas, and move the scroll location around. ++ if (rowIndex === 0 && columnIndex === 0) { ++ this._disableCellUpdates = true; ++ } ++ + // Global notification that we should retry our scroll to props-requested indices + this._hasScrolledToColumnTarget = false; + this._hasScrolledToRowTarget = false; -+ -+ // Disable cell updates for global reset -+ if (rowIndex >= this.props.rowCount - 1 || columnIndex >= this.props.columnCount - 1) { -+ this._disableCellUpdates = true; -+ } + // Clear cell cache in case we are scrolling; // Invalid row heights likely mean invalid cached content as well. this._styleCache = {}; -@@ -526,7 +549,11 @@ var Grid = function (_React$PureComponent) { +@@ -526,7 +554,11 @@ var Grid = function (_React$PureComponent) { scrollLeft: scrollLeft || 0, scrollTop: scrollTop || 0, totalColumnsWidth: instanceProps.columnSizeAndPositionManager.getTotalSize(), @@ -104,10 +155,13 @@ index e1b959a..18f8f1d 100644 }); this._maybeCallOnScrollbarPresenceChange(); -@@ -584,6 +611,59 @@ var Grid = function (_React$PureComponent) { +@@ -584,6 +616,60 @@ var Grid = function (_React$PureComponent) { } } ++ var totalRowsHeight = instanceProps.rowSizeAndPositionManager.getTotalSize(); ++ var totalColumnsWidth = instanceProps.columnSizeAndPositionManager.getTotalSize(); ++ + // We reset our hasScrolled flags if our target has changed, or if target is not longer set + if (scrollToColumn !== prevProps.scrollToColumn || scrollToColumn == null || scrollToColumn < 0) { + this._hasScrolledToColumnTarget = false; @@ -137,7 +191,6 @@ index e1b959a..18f8f1d 100644 + } + + if (isVisible) { -+ const totalColumnsWidth = instanceProps.columnSizeAndPositionManager.getTotalSize(); + const maxScroll = totalColumnsWidth - width; + this._hasScrolledToColumnTarget = (scrollLeft >= maxScroll || targetColumn.offset === scrollLeft); + } @@ -155,7 +208,6 @@ index e1b959a..18f8f1d 100644 + } + + if (isVisible) { -+ const totalRowsHeight = instanceProps.rowSizeAndPositionManager.getTotalSize(); + const maxScroll = totalRowsHeight - height; + this._hasScrolledToRowTarget = (scrollTop >= maxScroll || targetRow.offset === scrollTop); + } @@ -164,7 +216,7 @@ index e1b959a..18f8f1d 100644 // Special case where the previous size was 0: // In this case we don't show any windowed cells at all. // So we should always recalculate offset afterwards. -@@ -594,6 +674,8 @@ var Grid = function (_React$PureComponent) { +@@ -594,6 +680,8 @@ var Grid = function (_React$PureComponent) { if (this._recomputeScrollLeftFlag) { this._recomputeScrollLeftFlag = false; this._updateScrollLeftForScrollToColumn(this.props); @@ -173,7 +225,7 @@ index e1b959a..18f8f1d 100644 } else { (0, _updateScrollIndexHelper2.default)({ cellSizeAndPositionManager: instanceProps.columnSizeAndPositionManager, -@@ -616,6 +698,8 @@ var Grid = function (_React$PureComponent) { +@@ -616,6 +704,8 @@ var Grid = function (_React$PureComponent) { if (this._recomputeScrollTopFlag) { this._recomputeScrollTopFlag = false; this._updateScrollTopForScrollToRow(this.props); @@ -182,7 +234,7 @@ index e1b959a..18f8f1d 100644 } else { (0, _updateScrollIndexHelper2.default)({ cellSizeAndPositionManager: instanceProps.rowSizeAndPositionManager, -@@ -630,11 +714,56 @@ var Grid = function (_React$PureComponent) { +@@ -630,24 +720,55 @@ var Grid = function (_React$PureComponent) { size: height, sizeJustIncreasedFromZero: sizeJustIncreasedFromZero, updateScrollIndexCallback: function updateScrollIndexCallback() { @@ -192,40 +244,25 @@ index e1b959a..18f8f1d 100644 }); } -+ this._disableCellUpdates = false; -+ if (scrollPositionChangeReason !== SCROLL_POSITION_CHANGE_REASONS.OBSERVED) { ++ ++ if (this._disableCellUpdates) { + this._cellUpdates = []; + } -+ if (this._cellUpdates.length) { -+ const currentScrollTop = this.state.scrollTop; -+ const currentScrollBottom = currentScrollTop + height; -+ const currentScrollLeft = this.state.scrollLeft; -+ const currentScrollRight = currentScrollLeft + width; -+ ++ this._disableCellUpdates = false; ++ if (this.props.scrollToRow >= 0 && !this._hasScrolledToRowTarget) { ++ this._cellUpdates = []; ++ } ++ if (this.props.scrollToColumn >= 0 && !this._hasScrolledToColumnTarget) { ++ this._cellUpdates = []; ++ } ++ if (this._cellUpdates.length && scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.OBSERVED) { + let item; + let verticalDelta = 0; + let horizontalDelta = 0; + + while (item = this._cellUpdates.shift()) { -+ const rowData = instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(item.rowIndex); -+ const columnData = instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(item.columnIndex); -+ -+ const bottomOfItem = rowData.offset + rowData.size; -+ const rightSideOfItem = columnData.offset + columnData.size; -+ -+ if (bottomOfItem < currentScrollBottom) { -+ verticalDelta += item.heightChange; -+ } -+ if (rightSideOfItem < currentScrollRight) { -+ horizontalDelta += item.widthChange; -+ } -+ } -+ -+ if (this.props.scrollToRow >= 0 && !this._hasScrolledToRowTarget) { -+ verticalDelta = 0; -+ } -+ if (this.props.scrollToColumn >= 0 && !this._hasScrolledToColumnTarget) { -+ horizontalDelta = 0; ++ verticalDelta += item.heightChange || 0; ++ horizontalDelta += item.widthChange || 0; + } + + if (verticalDelta !== 0 || horizontalDelta !== 0) { @@ -240,7 +277,12 @@ index e1b959a..18f8f1d 100644 // Update onRowsRendered callback if start/stop indices have changed this._invokeOnGridRenderedHelper(); -@@ -647,7 +776,11 @@ var Grid = function (_React$PureComponent) { + // Changes to :scrollLeft or :scrollTop should also notify :onScroll listeners + if (scrollLeft !== prevState.scrollLeft || scrollTop !== prevState.scrollTop) { +- var totalRowsHeight = instanceProps.rowSizeAndPositionManager.getTotalSize(); +- var totalColumnsWidth = instanceProps.columnSizeAndPositionManager.getTotalSize(); +- + this._invokeOnScrollMemoizer({ scrollLeft: scrollLeft, scrollTop: scrollTop, totalColumnsWidth: totalColumnsWidth, @@ -253,7 +295,19 @@ index e1b959a..18f8f1d 100644 }); } -@@ -962,7 +1095,11 @@ var Grid = function (_React$PureComponent) { +@@ -909,6 +1030,11 @@ var Grid = function (_React$PureComponent) { + visibleRowIndices: visibleRowIndices + }); + ++ this._lastColumnStartIndex = this._columnStartIndex; ++ this._lastColumnStopIndex = this._columnStopIndex; ++ this._lastRowStartIndex = this._rowStartIndex; ++ this._lastRowStopIndex = this._rowStopIndex; ++ + // update the indices + this._columnStartIndex = columnStartIndex; + this._columnStopIndex = columnStopIndex; +@@ -962,7 +1088,11 @@ var Grid = function (_React$PureComponent) { var scrollLeft = _ref6.scrollLeft, scrollTop = _ref6.scrollTop, totalColumnsWidth = _ref6.totalColumnsWidth, @@ -266,7 +320,7 @@ index e1b959a..18f8f1d 100644 this._onScrollMemoizer({ callback: function callback(_ref7) { -@@ -973,19 +1110,26 @@ var Grid = function (_React$PureComponent) { +@@ -973,19 +1103,26 @@ var Grid = function (_React$PureComponent) { onScroll = _props7.onScroll, width = _props7.width; diff --git a/ts/components/SearchResults.tsx b/ts/components/SearchResults.tsx index 94b5319a0..d7ca52095 100644 --- a/ts/components/SearchResults.tsx +++ b/ts/components/SearchResults.tsx @@ -228,9 +228,7 @@ export class SearchResults extends React.Component { public resizeAll = () => { this.cellSizeCache.clearAll(); - - const rowCount = this.getRowCount(); - this.recomputeRowHeights(rowCount - 1); + this.recomputeRowHeights(0); }; public getRowCount() { diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index eba70f23e..e1fd63828 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -27,7 +27,7 @@ export type PropsDataType = { isLoadingMessages: boolean; items: Array; loadCountdownStart?: number; - messageHeightChanges: boolean; + messageHeightChangeIndex?: number; oldestUnreadIndex?: number; resetCounter: number; scrollToIndex?: number; @@ -123,7 +123,7 @@ export class Timeline extends React.PureComponent { public mostRecentWidth = 0; public mostRecentHeight = 0; public offsetFromBottom: number | undefined = 0; - public resizeAllFlag = false; + public resizeFlag = false; public listRef = React.createRef(); public visibleRows: VisibleRowsType | undefined; public loadCountdownTimeout: any; @@ -229,13 +229,17 @@ export class Timeline extends React.PureComponent { grid.scrollToPosition({ scrollTop: scrollContainer.scrollTop + delta }); }; - public resizeAll = () => { + public resize = (row?: number) => { this.offsetFromBottom = undefined; - this.resizeAllFlag = false; - this.cellSizeCache.clearAll(); + this.resizeFlag = false; + if (isNumber(row) && row > 0) { + // @ts-ignore + this.cellSizeCache.clearPlus(row, 0); + } else { + this.cellSizeCache.clearAll(); + } - const rowCount = this.getRowCount(); - this.recomputeRowHeights(rowCount - 1); + this.recomputeRowHeights(row || 0); }; public onScroll = (data: OnScrollParamsType) => { @@ -255,8 +259,6 @@ export class Timeline extends React.PureComponent { // pop the user back down to the bottom. const { clientHeight, scrollHeight, scrollTop } = data; if (scrollTop + clientHeight > scrollHeight) { - this.resizeAll(); - return; } @@ -597,8 +599,8 @@ export class Timeline extends React.PureComponent { return itemsCount + extraRows; } - public fromRowToItemIndex(row: number): number | undefined { - const { haveOldest, items } = this.props; + public fromRowToItemIndex(row: number, props?: Props): number | undefined { + const { haveOldest, items } = props || this.props; let subtraction = 0; @@ -619,8 +621,8 @@ export class Timeline extends React.PureComponent { return index; } - public getLastSeenIndicatorRow() { - const { oldestUnreadIndex } = this.props; + public getLastSeenIndicatorRow(props?: Props) { + const { oldestUnreadIndex } = props || this.props; if (!isNumber(oldestUnreadIndex)) { return; } @@ -716,24 +718,26 @@ export class Timeline extends React.PureComponent { this.updateWithVisibleRows(forceFocus); }; - // tslint:disable-next-line cyclomatic-complexity + // tslint:disable-next-line cyclomatic-complexity max-func-body-length public componentDidUpdate(prevProps: Props) { const { id, clearChangedMessages, items, - messageHeightChanges, + messageHeightChangeIndex, oldestUnreadIndex, resetCounter, scrollToIndex, typingContact, } = this.props; - // There are a number of situations which can necessitate that we drop our row height - // cache and start over. It can cause the scroll position to do weird things, so we - // try to minimize those situations. In some cases we could reset a smaller set - // of cached row data, but we currently don't have an API for that. We'd need to - // create it. + // There are a number of situations which can necessitate that we forget about row + // heights previously calculated. We reset the minimum number of rows to minimize + // unexpected changes to the scroll position. Those changes happen because + // react-virtualized doesn't know what to expect (variable row heights) when it + // renders, so it does have a fixed row it's attempting to scroll to, and you ask it + // to render a given point it space, it will do pretty random things. + if ( !prevProps.items || prevProps.items.length === 0 || @@ -748,13 +752,13 @@ export class Timeline extends React.PureComponent { }); if (prevProps.items && prevProps.items.length > 0) { - this.resizeAll(); + this.resize(); } - } else if (!typingContact && prevProps.typingContact) { - this.resizeAll(); - } else if (oldestUnreadIndex !== prevProps.oldestUnreadIndex) { - this.resizeAll(); - } else if ( + + return; + } + + if ( items && items.length > 0 && prevProps.items && @@ -767,7 +771,7 @@ export class Timeline extends React.PureComponent { const newFirstIndex = items.findIndex(item => item === oldFirstId); if (newFirstIndex < 0) { - this.resizeAll(); + this.resize(); return; } @@ -776,7 +780,7 @@ export class Timeline extends React.PureComponent { const delta = newFirstIndex - oldFirstIndex; if (delta > 0) { // We're loading more new messages at the top; we want to stay at the top - this.resizeAll(); + this.resize(); this.setState({ oneTimeScrollRow: newRow }); return; @@ -792,7 +796,7 @@ export class Timeline extends React.PureComponent { const newLastIndex = items.findIndex(item => item === oldLastId); if (newLastIndex < 0) { - this.resizeAll(); + this.resize(); return; } @@ -802,20 +806,59 @@ export class Timeline extends React.PureComponent { // If we've just added to the end of the list, then the index of the last id's // index won't have changed, and we can rely on List's detection that items is // different for the necessary re-render. - if (indexDelta !== 0) { - this.resizeAll(); - } else if (typingContact && prevProps.typingContact) { - // The last row will be off, because it was previously the typing indicator - this.resizeAll(); + if (indexDelta === 0) { + if (typingContact || prevProps.typingContact) { + // The last row will be off, because it was previously the typing indicator + const rowCount = this.getRowCount(); + this.resize(rowCount - 2); + } + + // no resize because we just add to the end + return; } - } else if (messageHeightChanges) { - this.resizeAll(); - clearChangedMessages(id); - } else if (this.resizeAllFlag) { - this.resizeAll(); - } else { - this.updateWithVisibleRows(); + + this.resize(); + + return; } + + if (this.resizeFlag) { + this.resize(); + + return; + } + + if (oldestUnreadIndex !== prevProps.oldestUnreadIndex) { + const prevRow = this.getLastSeenIndicatorRow(prevProps); + const newRow = this.getLastSeenIndicatorRow(); + const rowCount = this.getRowCount(); + const lastRow = rowCount - 1; + + const targetRow = Math.min( + isNumber(prevRow) ? prevRow : lastRow, + isNumber(newRow) ? newRow : lastRow + ); + this.resize(targetRow); + + return; + } + + if (isNumber(messageHeightChangeIndex)) { + const rowIndex = this.fromItemIndexToRow(messageHeightChangeIndex); + this.resize(rowIndex); + clearChangedMessages(id); + + return; + } + + if (Boolean(typingContact) !== Boolean(prevProps.typingContact)) { + const rowCount = this.getRowCount(); + this.resize(rowCount - 2); + + return; + } + + this.updateWithVisibleRows(); } public getScrollTarget = () => { @@ -857,9 +900,9 @@ export class Timeline extends React.PureComponent { {({ height, width }) => { if (this.mostRecentWidth && this.mostRecentWidth !== width) { - this.resizeAllFlag = true; + this.resizeFlag = true; - setTimeout(this.resizeAll, 0); + setTimeout(this.resize, 0); } else if ( this.mostRecentHeight && this.mostRecentHeight !== height diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index bed506eb7..be2bfee0c 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -1,5 +1,5 @@ import memoizee from 'memoizee'; -import { isNumber } from 'lodash'; +import { fromPairs, isNumber } from 'lodash'; import { createSelector } from 'reselect'; import { format } from '../../types/PhoneNumber'; @@ -370,9 +370,15 @@ export function _conversationMessagesSelector( !metrics.oldest || !firstId || firstId === metrics.oldest.id; const items = messageIds; - const messageHeightChanges = Boolean( + + const messageHeightChangeLookup = heightChangeMessageIds && heightChangeMessageIds.length - ); + ? fromPairs(heightChangeMessageIds.map(id => [id, true])) + : null; + const messageHeightChangeIndex = messageHeightChangeLookup + ? messageIds.findIndex(id => messageHeightChangeLookup[id]) + : undefined; + const oldestUnreadIndex = oldestUnread ? messageIds.findIndex(id => id === oldestUnread.id) : undefined; @@ -387,14 +393,17 @@ export function _conversationMessagesSelector( isLoadingMessages, loadCountdownStart, items, - messageHeightChanges, + messageHeightChangeIndex: + isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0 + ? messageHeightChangeIndex + : undefined, oldestUnreadIndex: isNumber(oldestUnreadIndex) && oldestUnreadIndex >= 0 ? oldestUnreadIndex : undefined, resetCounter, scrollToIndex: - scrollToIndex && scrollToIndex >= 0 ? scrollToIndex : undefined, + isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined, scrollToIndexCounter: scrollToMessageCounter, totalUnread, };