diff --git a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m index 3ce9d8c7a..31391808b 100644 --- a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m +++ b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m @@ -219,6 +219,10 @@ typedef enum : NSUInteger { @property (nonatomic) UIView *scrollDownButton; +@property (nonatomic) BOOL isViewVisible; +@property (nonatomic) BOOL isAppInBackground; +@property (nonatomic) BOOL shouldObserveDBModifications; + @end #pragma mark - @@ -294,6 +298,30 @@ typedef enum : NSUInteger { selector:@selector(identityStateDidChange:) name:kNSNotificationName_IdentityStateDidChange object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didChangePreferredContentSize:) + name:UIContentSizeCategoryDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModified:) + name:YapDatabaseModifiedNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResignActive:) + name:UIApplicationWillResignActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(cancelReadTimer) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; } - (void)blockedPhoneNumbersDidChange:(id)notification @@ -336,16 +364,16 @@ typedef enum : NSUInteger { _composeOnOpen = keyboardOnViewAppearing; _callOnOpen = callOnViewAppearing; - // We need to create the "unread indicator" before we mark - // all messages as read. - [self ensureDynamicInteractions]; - [self.uiDatabaseConnection beginLongLivedReadTransaction]; self.messageMappings = [[YapDatabaseViewMappings alloc] initWithGroups:@[ thread.uniqueId ] view:TSMessageDatabaseViewExtensionName]; + // We need to impose the range restrictions on the mappings immediately to avoid + // doing a great deal of unnecessary work and causing a perf hotspot. + [self updateMessageMappingRangeOptions]; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.messageMappings updateWithTransaction:transaction]; }]; + [self updateShouldObserveDBModifications]; self.page = 0; if (self.dynamicInteractions.unreadIndicatorPosition != nil) { @@ -362,10 +390,6 @@ typedef enum : NSUInteger { MIN(kYapDatabaseMaxInitialPageCount - 1, (unreadIndicatorPosition + kPreferredSeenMessageCount) / kYapDatabasePageSize)); } - - [self updateMessageMappingRangeOptions]; - [self updateLoadEarlierVisible]; - [self.collectionView reloadData]; } - (BOOL)userLeftGroup @@ -460,60 +484,16 @@ typedef enum : NSUInteger { forCellWithReuseIdentifier:[OWSIncomingMessageCollectionViewCell mediaCellReuseIdentifier]]; } -- (void)toggleObservers:(BOOL)shouldObserve -{ - if (shouldObserve) { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(didChangePreferredContentSize:) - name:UIContentSizeCategoryDidChangeNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(yapDatabaseModified:) - name:YapDatabaseModifiedNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillResignActive:) - name:UIApplicationWillResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(cancelReadTimer) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - } else { - [[NSNotificationCenter defaultCenter] removeObserver:self - name:UIContentSizeCategoryDidChangeNotification - object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:YapDatabaseModifiedNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self - name:UIApplicationWillEnterForegroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self - name:UIApplicationWillResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - } -} - - (void)applicationWillEnterForeground:(NSNotification *)notification { - [self resetContentAndLayout]; [self startReadTimer]; [self startExpirationTimerAnimations]; - [self ensureDynamicInteractions]; + self.isAppInBackground = NO; } - (void)applicationDidEnterBackground:(NSNotification *)notification { + self.isAppInBackground = YES; if (self.hasClearedUnreadMessagesIndicator) { self.hasClearedUnreadMessagesIndicator = NO; [self.dynamicInteractions clearUnreadIndicatorState]; @@ -565,19 +545,7 @@ typedef enum : NSUInteger { // or on another device. [self hideInputIfNeeded]; - self.messageAdapterCache = [[NSCache alloc] init]; - - // We need to `beginLongLivedReadTransaction` before we update our - // mapping in order to jump to the most recent commit. - [self.uiDatabaseConnection beginLongLivedReadTransaction]; - [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - [self.messageMappings updateWithTransaction:transaction]; - }]; - [self updateMessageMappingRangeOptions]; - - [self resetContentAndLayout]; - - [self toggleObservers:YES]; + self.isViewVisible = YES; // restart any animations that were stopped e.g. while inspecting the contact info screens. [self startExpirationTimerAnimations]; @@ -1009,7 +977,8 @@ typedef enum : NSUInteger { DDLogDebug(@"%@ viewWillDisappear", self.tag); [super viewWillDisappear:animated]; - [self toggleObservers:NO]; + + self.isViewVisible = NO; // Since we're using a custom back button, we have to do some extra work to manage the // interactivePopGestureRecognizer @@ -1821,7 +1790,7 @@ typedef enum : NSUInteger { - (void)didChangePreferredContentSize:(NSNotification *)notification { [self.collectionView.collectionViewLayout setMessageBubbleFont:[UIFont ows_dynamicTypeBodyFont]]; - [self.collectionView reloadData]; + [self resetContentAndLayout]; [self reloadInputToolbarSizeIfNeeded]; } @@ -1889,8 +1858,8 @@ typedef enum : NSUInteger { { NSInteger rowCount = [self.collectionView numberOfItemsInSection:indexPath.section]; for (NSInteger row = indexPath.row + 1; row < rowCount; row++) { - id nextMessage = - [self messageAtIndexPath:[NSIndexPath indexPathForRow:row inSection:indexPath.section]]; + NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:row inSection:indexPath.section]; + TSInteraction *nextMessage = [self interactionAtIndexPath:nextIndexPath]; if ([nextMessage isKindOfClass:[TSOutgoingMessage class]]) { return (TSOutgoingMessage *)nextMessage; } @@ -2270,41 +2239,8 @@ typedef enum : NSUInteger { self.page = MIN(self.page + 1, (NSUInteger)kYapDatabaseMaxPageCount - 1); - // To update a YapDatabaseViewMappings, you can call either: - // - // * [YapDatabaseViewMappings updateWithTransaction] - // * [YapDatabaseViewMappings getSectionChanges:rowChanges:forNotifications:withMappings:] - // - // ...but you can't call both. - // - // If ensureDynamicInteractionsForThread modifies the database, - // the mappings will be updated by yapDatabaseModified. - // This will leave the mapping range in a bad state. - // Therefore we temporarily disable observation of YapDatabaseModifiedNotification - // while updating the range and the dynamic interactions. - [[NSNotificationCenter defaultCenter] removeObserver:self name:YapDatabaseModifiedNotification object:nil]; - - // We need to update the dynamic interactions after loading earlier messages, - // since the unseen indicator may need to move or change. - [self ensureDynamicInteractions]; - - [self updateMessageMappingRangeOptions]; - - // We need to `beginLongLivedReadTransaction` before we update our - // mapping in order to jump to the most recent commit. - [self.uiDatabaseConnection beginLongLivedReadTransaction]; - [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - [self.messageMappings updateWithTransaction:transaction]; - }]; + [self resetMappings]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(yapDatabaseModified:) - name:YapDatabaseModifiedNotification - object:nil]; - - [self.collectionView.collectionViewLayout - invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; - [self.collectionView reloadData]; [self.collectionView layoutSubviews]; self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - scrollDistanceToBottom); @@ -3282,6 +3218,10 @@ typedef enum : NSUInteger { // the database is modified. That doesn't seem optimal, but // in practice it's efficient enough. + if (!self.shouldObserveDBModifications) { + return; + } + // We need to `beginLongLivedReadTransaction` before we update our // models in order to jump to the most recent commit. NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction]; @@ -3330,7 +3270,6 @@ typedef enum : NSUInteger { // range. [self updateMessageMappingRangeOptions]; [self resetContentAndLayout]; - return; } @@ -3388,9 +3327,7 @@ typedef enum : NSUInteger { } completion:^(BOOL success) { if (!success) { - [self.collectionView.collectionViewLayout - invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; - [self.collectionView reloadData]; + [self resetContentAndLayout]; } [self updateLastVisibleTimestamp]; @@ -3439,6 +3376,8 @@ typedef enum : NSUInteger { - (id)messageAtIndexPath:(NSIndexPath *)indexPath { + OWSAssert(self.messageAdapterCache); + TSInteraction *interaction = [self interactionAtIndexPath:indexPath]; id messageAdapter = [self.messageAdapterCache objectForKey:interaction.uniqueId]; @@ -4182,9 +4121,7 @@ typedef enum : NSUInteger { [groupMemberIds addObject:[TSAccountManager localNumber]]; groupModel.groupMemberIds = [NSMutableArray arrayWithArray:[groupMemberIds allObjects]]; [self updateGroupModelTo:groupModel successCompletion:nil]; - [self.collectionView.collectionViewLayout - invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; - [self.collectionView reloadData]; + [self resetContentAndLayout]; } - (void)popAllConversationSettingsViews @@ -4210,6 +4147,65 @@ typedef enum : NSUInteger { return ![interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]; } +#pragma mark - Database Observation + +- (void)setIsViewVisible:(BOOL)isViewVisible +{ + _isViewVisible = isViewVisible; + + [self updateShouldObserveDBModifications]; +} + +- (void)setIsAppInBackground:(BOOL)isAppInBackground +{ + _isAppInBackground = isAppInBackground; + + [self updateShouldObserveDBModifications]; +} + +- (void)updateShouldObserveDBModifications +{ + self.shouldObserveDBModifications = self.isViewVisible && !self.isAppInBackground; +} + +- (void)setShouldObserveDBModifications:(BOOL)shouldObserveDBModifications +{ + if (_shouldObserveDBModifications == shouldObserveDBModifications) { + return; + } + + _shouldObserveDBModifications = shouldObserveDBModifications; + + if (self.shouldObserveDBModifications) { + [self resetMappings]; + } +} + +- (void)resetMappings +{ + // If we're entering "active" mode (e.g. view is visible and app is in foreground), + // reset all state updated by yapDatabaseModified:. + if (self.messageMappings != nil) { + // Before we begin observing database modifications, make sure + // our mapping and table state is up-to-date. + // + // We need to `beginLongLivedReadTransaction` before we update our + // mapping in order to jump to the most recent commit. + [self.uiDatabaseConnection beginLongLivedReadTransaction]; + [self updateMessageMappingRangeOptions]; + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self.messageMappings updateWithTransaction:transaction]; + }]; + } + + self.messageAdapterCache = [[NSCache alloc] init]; + [self resetContentAndLayout]; + [self updateLoadEarlierVisible]; + [self ensureDynamicInteractions]; + [self updateBackButtonUnreadCount]; + [self updateNavigationBarSubtitleLabel]; +} + #pragma mark - Class methods + (UINib *)nib diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index bed389f25..8758a42ed 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -432,17 +432,15 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; return; } - DDLogDebug(@"%@ shouldObserveDBModifications: %d -> %d", - self.tag, - _shouldObserveDBModifications, - shouldObserveDBModifications); - _shouldObserveDBModifications = shouldObserveDBModifications; - if (!self.shouldObserveDBModifications) { - return; + if (self.shouldObserveDBModifications) { + [self resetMappings]; } +} +- (void)resetMappings +{ // If we're entering "active" mode (e.g. view is visible and app is in foreground), // reset all state updated by yapDatabaseModified:. if (self.threadMappings != nil) { @@ -460,6 +458,12 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; [[self tableView] reloadData]; [self checkIfEmptyView]; [self updateInboxCountLabel]; + + // If the user hasn't already granted contact access + // we don't want to request until they receive a message. + if ([TSThread numberOfKeysInCollection] > 0) { + [self.contactsManager requestSystemContactsOnce]; + } } - (void)applicationWillEnterForeground:(NSNotification *)notification @@ -825,23 +829,27 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; _viewingThreadsIn = viewingThreadsIn; self.segmentedControl.selectedSegmentIndex = (viewingThreadsIn == kInboxState ? 0 : 1); if (didChange || !self.threadMappings) { - [self changeToGrouping:(viewingThreadsIn == kInboxState ? TSInboxGroup : TSArchiveGroup)]; + [self updateMappings]; } else { [self checkIfEmptyView]; [self updateReminderViews]; } } -- (void)changeToGrouping:(NSString *)grouping { - OWSAssert([NSThread isMainThread]); +- (NSString *)currentGrouping +{ + return self.viewingThreadsIn == kInboxState ? TSInboxGroup : TSArchiveGroup; +} - self.shouldObserveDBModifications = NO; +- (void)updateMappings +{ + OWSAssert([NSThread isMainThread]); - self.threadMappings = - [[YapDatabaseViewMappings alloc] initWithGroups:@[ grouping ] view:TSThreadDatabaseViewExtensionName]; - [self.threadMappings setIsReversed:YES forGroup:grouping]; + self.threadMappings = [[YapDatabaseViewMappings alloc] initWithGroups:@[ self.currentGrouping ] + view:TSThreadDatabaseViewExtensionName]; + [self.threadMappings setIsReversed:YES forGroup:self.currentGrouping]; - [self updateShouldObserveDBModifications]; + [self resetMappings]; [[self tableView] reloadData]; [self checkIfEmptyView]; @@ -861,9 +869,19 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } - (void)yapDatabaseModified:(NSNotification *)notification { + if (!self.shouldObserveDBModifications) { + return; + } + NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction]; - NSArray *sectionChanges = nil; - NSArray *rowChanges = nil; + + if (![[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] hasChangesForGroup:self.currentGrouping + inNotifications:notifications]) { + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self.self.threadMappings updateWithTransaction:transaction]; + }]; + return; + } // If the user hasn't already granted contact access // we don't want to request until they receive a message. @@ -871,10 +889,8 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; [self.contactsManager requestSystemContactsOnce]; } - if (!self.shouldObserveDBModifications) { - return; - } - + NSArray *sectionChanges = nil; + NSArray *rowChanges = nil; [[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] getSectionChanges:§ionChanges rowChanges:&rowChanges forNotifications:notifications