diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 788a68e65..8c2f534f7 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -108,6 +108,25 @@ typedef enum : NSUInteger { #pragma mark - +// We use snapshots to ensure that the view has a consistent +// representation of view model state which is not updated +// when the view is not observing view model changes. +@interface ConversationSnapshot : NSObject + +@property (nonatomic) NSArray> *viewItems; +@property (nonatomic) ThreadDynamicInteractions *dynamicInteractions; +@property (nonatomic) BOOL canLoadMoreItems; + +@end + +#pragma mark - + +@implementation ConversationSnapshot + +@end + +#pragma mark - + @interface ConversationViewController () 0 && [self.thread.recipientIdentifiers containsObject:recipientId]) { - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } else if (groupId.length > 0 && self.thread.isGroupThread) { TSGroupThread *groupThread = (TSGroupThread *)self.thread; if ([groupThread.groupModel.groupId isEqualToData:groupId]) { - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; [self ensureBannerState]; } } @@ -486,6 +506,8 @@ typedef enum : NSUInteger { _conversationViewModel = [[ConversationViewModel alloc] initWithThread:thread focusMessageIdOnOpen:focusMessageId delegate:self]; + [self updateConversationSnapshot]; + [self updateShouldObserveVMUpdates]; self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f @@ -753,12 +775,12 @@ typedef enum : NSUInteger { - (NSArray> *)viewItems { - return self.conversationViewModel.viewItems; + return self.conversationSnapshot.viewItems; } - (ThreadDynamicInteractions *)dynamicInteractions { - return self.conversationViewModel.dynamicInteractions; + return self.conversationSnapshot.dynamicInteractions; } - (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator @@ -847,6 +869,7 @@ typedef enum : NSUInteger { // Avoid layout corrupt issues and out-of-date message subtitles. self.lastReloadDate = [NSDate new]; [self.conversationViewModel viewDidResetContentAndLayout]; + [self tryToUpdateConversationSnapshot]; [self.collectionView.collectionViewLayout invalidateLayout]; [self.collectionView reloadData]; @@ -1697,7 +1720,7 @@ typedef enum : NSUInteger { { OWSAssertDebug(self.conversationViewModel); - self.showLoadMoreHeader = self.conversationViewModel.canLoadMoreItems; + self.showLoadMoreHeader = self.conversationSnapshot.canLoadMoreItems; } - (void)setShowLoadMoreHeader:(BOOL)showLoadMoreHeader @@ -2424,7 +2447,7 @@ typedef enum : NSUInteger { - (void)contactsViewHelperDidUpdateContacts { - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } - (void)createConversationScrollButtons @@ -2462,7 +2485,7 @@ typedef enum : NSUInteger { _hasUnreadMessages = hasUnreadMessages; self.scrollDownButton.hasUnreadMessages = hasUnreadMessages; - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } - (void)scrollDownButtonTapped @@ -2607,7 +2630,7 @@ typedef enum : NSUInteger { [self showApprovalDialogForAttachment:attachment]; [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } - (void)messageWasSent:(TSOutgoingMessage *)message @@ -2967,7 +2990,7 @@ typedef enum : NSUInteger { [self messageWasSent:message]; if (didAddToProfileWhitelist) { - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } }]; } @@ -3619,7 +3642,7 @@ typedef enum : NSUInteger { [self messageWasSent:message]; if (didAddToProfileWhitelist) { - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } }); } @@ -4009,7 +4032,7 @@ typedef enum : NSUInteger { [self clearDraft]; if (didAddToProfileWhitelist) { - [self.conversationViewModel ensureDynamicInteractions]; + [self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } } @@ -4138,6 +4161,7 @@ typedef enum : NSUInteger { if (self.shouldObserveVMUpdates) { OWSLogVerbose(@"resume observation of view model."); + [self updateConversationSnapshot]; [self resetContentAndLayout]; [self updateBackButtonUnreadCount]; [self updateNavigationBarSubtitleLabel]; @@ -4612,6 +4636,7 @@ typedef enum : NSUInteger { return; } + [self updateConversationSnapshot]; [self updateBackButtonUnreadCount]; [self updateNavigationBarSubtitleLabel]; @@ -4909,6 +4934,26 @@ typedef enum : NSUInteger { [self.inputToolbar updateLayoutWithSafeAreaInsets:safeAreaInsets]; } +#pragma mark - Conversation Snapshot + +- (void)tryToUpdateConversationSnapshot +{ + if (!self.isObservingVMUpdates) { + return; + } + + [self updateConversationSnapshot]; +} + +- (void)updateConversationSnapshot +{ + ConversationSnapshot *conversationSnapshot = [ConversationSnapshot new]; + conversationSnapshot.viewItems = self.conversationViewModel.viewItems; + conversationSnapshot.dynamicInteractions = self.conversationViewModel.dynamicInteractions; + conversationSnapshot.canLoadMoreItems = self.conversationViewModel.canLoadMoreItems; + _conversationSnapshot = conversationSnapshot; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.h b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.h index a855c6f4c..d13a38c36 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.h @@ -96,7 +96,7 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) { focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen delegate:(id)delegate NS_DESIGNATED_INITIALIZER; -- (void)ensureDynamicInteractions; +- (void)ensureDynamicInteractionsAndUpdateIfNecessary:(BOOL)updateIfNecessary; - (void)clearUnreadMessagesIndicator; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m index 602a4fc0d..75428952d 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m @@ -273,7 +273,7 @@ static const int kYapDatabaseRangeMaxLength = 25000; { OWSAssertIsOnMainThread(); - [self ensureDynamicInteractions]; + [self ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } - (void)profileWhitelistDidChange:(NSNotification *)notification @@ -308,7 +308,7 @@ static const int kYapDatabaseRangeMaxLength = 25000; self.typingIndicatorsSender = [self.typingIndicators typingRecipientIdForThread:self.thread]; self.collapseCutoffDate = [NSDate new]; - [self ensureDynamicInteractions]; + [self ensureDynamicInteractionsAndUpdateIfNecessary:NO]; [self.primaryStorage updateUIDatabaseConnectionToLatest]; [self createNewMessageMapping]; @@ -464,21 +464,32 @@ static const int kYapDatabaseRangeMaxLength = 25000; self.collapseCutoffDate = [NSDate new]; } -- (void)ensureDynamicInteractions +- (void)ensureDynamicInteractionsAndUpdateIfNecessary:(BOOL)updateIfNecessary { OWSAssertIsOnMainThread(); const int currentMaxRangeSize = (int)self.messageMapping.desiredLength; const int maxRangeSize = MAX(kConversationInitialMaxRangeSize, currentMaxRangeSize); - self.dynamicInteractions = [ThreadUtil ensureDynamicInteractionsForThread:self.thread - contactsManager:self.contactsManager - blockingManager:self.blockingManager - dbConnection:self.editingDatabaseConnection - hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator - lastUnreadIndicator:self.dynamicInteractions.unreadIndicator - focusMessageId:self.focusMessageIdOnOpen - maxRangeSize:maxRangeSize]; + ThreadDynamicInteractions *dynamicInteractions = + [ThreadUtil ensureDynamicInteractionsForThread:self.thread + contactsManager:self.contactsManager + blockingManager:self.blockingManager + dbConnection:self.editingDatabaseConnection + hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator + lastUnreadIndicator:self.dynamicInteractions.unreadIndicator + focusMessageId:self.focusMessageIdOnOpen + maxRangeSize:maxRangeSize]; + BOOL didChange = ![NSObject isNullableObject:self.dynamicInteractions equalTo:dynamicInteractions]; + self.dynamicInteractions = dynamicInteractions; + + if (didChange && updateIfNecessary) { + if (![self reloadViewItems]) { + OWSFailDebug(@"Failed to reload view items."); + } + + [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate]; + } } - (nullable id)viewItemForUnreadMessagesIndicator @@ -519,7 +530,7 @@ static const int kYapDatabaseRangeMaxLength = 25000; if (self.dynamicInteractions.unreadIndicator) { // If we've just cleared the "unread messages" indicator, // update the dynamic interactions. - [self ensureDynamicInteractions]; + [self ensureDynamicInteractionsAndUpdateIfNecessary:YES]; } } @@ -968,7 +979,7 @@ static const int kYapDatabaseRangeMaxLength = 25000; self.collapseCutoffDate = [NSDate new]; - [self ensureDynamicInteractions]; + [self ensureDynamicInteractionsAndUpdateIfNecessary:NO]; if (![self reloadViewItems]) { OWSFailDebug(@"failed to reload view items in resetMapping."); @@ -1590,7 +1601,7 @@ static const int kYapDatabaseRangeMaxLength = 25000; self.collapseCutoffDate = [NSDate new]; - [self ensureDynamicInteractions]; + [self ensureDynamicInteractionsAndUpdateIfNecessary:NO]; if (![self reloadViewItems]) { OWSFailDebug(@"failed to reload view items in resetMapping."); diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 099e56448..c7c0dcf24 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -46,6 +46,21 @@ NS_ASSUME_NONNULL_BEGIN self.unreadIndicator = nil; } +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[ThreadDynamicInteractions class]]) { + return NO; + } + + ThreadDynamicInteractions *other = (ThreadDynamicInteractions *)object; + return ([NSObject isNullableObject:self.focusMessagePosition equalTo:other.focusMessagePosition] && + [NSObject isNullableObject:self.unreadIndicator equalTo:other.unreadIndicator]); +} + @end #pragma mark -