diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 47f17eedd..ea7c32648 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2820,14 +2820,14 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { return; } - NSArray *messageRowChanges = nil; - NSArray *sectionChanges = nil; + NSArray *sectionChanges = nil; + NSArray *rowChanges = nil; [[self.uiDatabaseConnection ext:TSMessageDatabaseViewExtensionName] getSectionChanges:§ionChanges - rowChanges:&messageRowChanges + rowChanges:&rowChanges forNotifications:notifications withMappings:self.messageMappings]; - if ([sectionChanges count] == 0 && [messageRowChanges count] == 0) { + if ([sectionChanges count] == 0 && [rowChanges count] == 0) { // YapDatabase will ignore insertions within the message mapping's // range that are not within the current mapping's contents. We // may need to extend the mapping's contents to reflect the current @@ -2844,7 +2844,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { // We need to reload any modified interactions _before_ we call // reloadViewItems. - for (YapDatabaseViewRowChange *rowChange in messageRowChanges) { + for (YapDatabaseViewRowChange *rowChange in rowChanges) { switch (rowChange.type) { case YapDatabaseViewChangeUpdate: { YapCollectionKey *collectionKey = rowChange.collectionKey; @@ -2860,6 +2860,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { } } + NSUInteger oldViewItemCount = self.viewItems.count; NSMutableSet *rowsThatChangedSize = [[self reloadViewItems] mutableCopy]; BOOL wasAtBottom = [self isScrolledToBottom]; @@ -2873,8 +2874,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { // b) is inserting new interactions. __block BOOL scrollToBottom = wasAtBottom; - [self.collectionView performBatchUpdates:^{ - for (YapDatabaseViewRowChange *rowChange in messageRowChanges) { + BOOL shouldAnimateUpdates = [self shouldAnimateRowUpdates:rowChanges oldViewItemCount:oldViewItemCount]; + + void (^batchUpdates)() = ^{ + for (YapDatabaseViewRowChange *rowChange in rowChanges) { switch (rowChange.type) { case YapDatabaseViewChangeDelete: { DDLogVerbose(@"YapDatabaseViewChangeDelete: %@, %@", rowChange.collectionKey, rowChange.indexPath); @@ -2925,20 +2928,86 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [rowsToReload addObject:[NSIndexPath indexPathForRow:row.integerValue inSection:0]]; } [self.collectionView reloadItemsAtIndexPaths:rowsToReload]; - } - completion:^(BOOL finished) { - OWSAssert([NSThread isMainThread]); + }; + void (^batchUpdatesCompletion)(BOOL) = ^(BOOL finished) { + OWSAssert([NSThread isMainThread]); - if (!finished) { - DDLogInfo(@"%@ performBatchUpdates did not finish", self.tag); - } + if (!finished) { + DDLogInfo(@"%@ performBatchUpdates did not finish", self.tag); + } - [self updateLastVisibleTimestamp]; + [self updateLastVisibleTimestamp]; - if (scrollToBottom) { - [self scrollToBottomAnimated:shouldAnimateScrollToBottom]; - } + if (scrollToBottom) { + [self scrollToBottomAnimated:shouldAnimateScrollToBottom]; + } + }; + + if (shouldAnimateUpdates) { + [self.collectionView performBatchUpdates:batchUpdates completion:batchUpdatesCompletion]; + } else { + [UIView performWithoutAnimation:^{ + [self.collectionView performBatchUpdates:batchUpdates completion:batchUpdatesCompletion]; }]; + } +} + +- (BOOL)shouldAnimateRowUpdates:(NSArray *)rowChanges + oldViewItemCount:(NSUInteger)oldViewItemCount +{ + OWSAssert(rowChanges); + + // If user sends a new outgoing message, don't animate the change. + BOOL isOnlyInsertingNewOutgoingMessages = YES; + BOOL isOnlyUpdatingLastOutgoingMessage = YES; + NSNumber *_Nullable lastUpdateRow = nil; + NSNumber *_Nullable lastNonUpdateRow = nil; + for (YapDatabaseViewRowChange *rowChange in rowChanges) { + switch (rowChange.type) { + case YapDatabaseViewChangeDelete: + isOnlyInsertingNewOutgoingMessages = NO; + isOnlyUpdatingLastOutgoingMessage = NO; + if (!lastNonUpdateRow || lastNonUpdateRow.integerValue < rowChange.indexPath.row) { + lastNonUpdateRow = @(rowChange.indexPath.row); + } + break; + case YapDatabaseViewChangeInsert: { + isOnlyUpdatingLastOutgoingMessage = NO; + ConversationViewItem *_Nullable viewItem = [self viewItemForIndex:rowChange.newIndexPath.row]; + if ([viewItem.interaction isKindOfClass:[TSOutgoingMessage class]] + && rowChange.newIndexPath.row >= (NSInteger)oldViewItemCount) { + continue; + } + if (!lastNonUpdateRow || lastNonUpdateRow.integerValue < rowChange.newIndexPath.row) { + lastNonUpdateRow = @(rowChange.newIndexPath.row); + } + } + case YapDatabaseViewChangeMove: + isOnlyInsertingNewOutgoingMessages = NO; + isOnlyUpdatingLastOutgoingMessage = NO; + if (!lastNonUpdateRow || lastNonUpdateRow.integerValue < rowChange.indexPath.row) { + lastNonUpdateRow = @(rowChange.indexPath.row); + } + if (!lastNonUpdateRow || lastNonUpdateRow.integerValue < rowChange.newIndexPath.row) { + lastNonUpdateRow = @(rowChange.newIndexPath.row); + } + break; + case YapDatabaseViewChangeUpdate: { + isOnlyInsertingNewOutgoingMessages = NO; + ConversationViewItem *_Nullable viewItem = [self viewItemForIndex:rowChange.indexPath.row]; + if (![viewItem.interaction isKindOfClass:[TSOutgoingMessage class]] + || rowChange.indexPath.row != (NSInteger)(oldViewItemCount - 1)) { + isOnlyUpdatingLastOutgoingMessage = NO; + } + if (!lastUpdateRow || lastUpdateRow.integerValue < rowChange.indexPath.row) { + lastUpdateRow = @(rowChange.indexPath.row); + } + break; + } + } + } + BOOL shouldAnimateRowUpdates = !(isOnlyInsertingNewOutgoingMessages || isOnlyUpdatingLastOutgoingMessage); + return shouldAnimateRowUpdates; } - (BOOL)isScrolledToBottom