From 254c87a1acf45798627037dae450fb53945c0ed6 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Tue, 2 Nov 2021 16:42:35 -0700 Subject: [PATCH] Fix row height recomputation in Timeline --- ts/components/conversation/Timeline.tsx | 146 ++++++++++++++---------- 1 file changed, 85 insertions(+), 61 deletions(-) diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 7ee29904bd..8f8d8a34e4 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -1084,15 +1084,24 @@ export class Timeline extends React.PureComponent { return; } + let resizeStartRow: number | undefined; + + if (isNumber(messageHeightChangeIndex)) { + resizeStartRow = this.fromItemIndexToRow(messageHeightChangeIndex); + clearChangedMessages(id); + } + if ( - items && - items.length > 0 && - prevProps.items && - prevProps.items.length > 0 && - items !== prevProps.items + items !== prevProps.items || + oldestUnreadIndex !== prevProps.oldestUnreadIndex || + Boolean(typingContact) !== Boolean(prevProps.typingContact) ) { const { atTop } = this.state; + // This clause handles prepended messages when user scrolls up. New + // messages are added to `items`, but we want to keep the scroll position + // at the first previously visible message even though the row numbers + // have now changed. if (atTop) { const oldFirstIndex = 0; const oldFirstId = prevProps.items[oldFirstIndex]; @@ -1117,39 +1126,55 @@ export class Timeline extends React.PureComponent { } } - // We continue on after our atTop check; because if we're not loading new messages - // we still have to check for all the other situations which might require a - // resize. + // Compare current rows against previous rows to identify the number of + // consecutive rows (from start of the list) the are the same in both + // lists. + const rowsIterator = Timeline.getEphemeralRows({ + items, + oldestUnreadIndex, + typingContact: Boolean(typingContact), + haveOldest, + }); + const prevRowsIterator = Timeline.getEphemeralRows({ + items: prevProps.items, + oldestUnreadIndex: prevProps.oldestUnreadIndex, + typingContact: Boolean(prevProps.typingContact), + haveOldest: prevProps.haveOldest, + }); - const oldLastIndex = prevProps.items.length - 1; - const oldLastId = prevProps.items[oldLastIndex]; - - const newLastIndex = items.findIndex(item => item === oldLastId); - if (newLastIndex < 0) { - this.resize(); - - return; - } - - const indexDelta = newLastIndex - oldLastIndex; - - // 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) { - 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); + let firstChangedRow = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const row = rowsIterator.next(); + if (row.done) { + break; } - // no resize because we just add to the end - return; + const prevRow = prevRowsIterator.next(); + if (prevRow.done) { + break; + } + + if (prevRow.value !== row.value) { + break; + } + + firstChangedRow += 1; } - this.resize(); - - return; + // If either: + // + // - Row count has changed after props update + // - There are some different rows (and the loop above was interrupted) + // + // Recompute heights of all rows starting from the first changed row or + // the last row in the previous row list. + if (!rowsIterator.next().done || !prevRowsIterator.next().done) { + resizeStartRow = Math.min( + resizeStartRow ?? firstChangedRow, + firstChangedRow + ); + } } if (this.resizeFlag) { @@ -1158,34 +1183,8 @@ export class Timeline extends React.PureComponent { 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; + if (resizeStartRow !== undefined) { + this.resize(resizeStartRow); } this.updateWithVisibleRows(); @@ -1575,6 +1574,31 @@ export class Timeline extends React.PureComponent { ); } + private static *getEphemeralRows({ + items, + typingContact, + oldestUnreadIndex, + haveOldest, + }: { + items: ReadonlyArray; + typingContact: boolean; + oldestUnreadIndex?: number; + haveOldest: boolean; + }): Iterator { + yield haveOldest ? 'hero' : 'loading'; + + for (let i = 0; i < items.length; i += 1) { + if (i === oldestUnreadIndex) { + yield 'oldest-unread'; + } + yield `item:${items[i]}`; + } + + if (typingContact) { + yield 'typing-contact'; + } + } + private static getWarning( { warning }: PropsType, state: StateType