Revert "Conversation view always observes view model."

This reverts commit 9d39e829a44f28f324f79e0b74a6c8692678d788.
pull/2/head
Matthew Chen 6 years ago
parent 6ed4045fbe
commit 7711ee92a7

@ -108,6 +108,25 @@ typedef enum : NSUInteger {
#pragma mark - #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<id<ConversationViewItem>> *viewItems;
@property (nonatomic) ThreadDynamicInteractions *dynamicInteractions;
@property (nonatomic) BOOL canLoadMoreItems;
@end
#pragma mark -
@implementation ConversationSnapshot
@end
#pragma mark -
@interface ConversationViewController () <AttachmentApprovalViewControllerDelegate, @interface ConversationViewController () <AttachmentApprovalViewControllerDelegate,
ContactShareApprovalViewControllerDelegate, ContactShareApprovalViewControllerDelegate,
AVAudioPlayerDelegate, AVAudioPlayerDelegate,
@ -141,6 +160,7 @@ typedef enum : NSUInteger {
@property (nonatomic) TSThread *thread; @property (nonatomic) TSThread *thread;
@property (nonatomic, readonly) ConversationViewModel *conversationViewModel; @property (nonatomic, readonly) ConversationViewModel *conversationViewModel;
@property (nonatomic, readonly) ConversationSnapshot *conversationSnapshot;
@property (nonatomic, readonly) OWSAudioActivity *recordVoiceNoteAudioActivity; @property (nonatomic, readonly) OWSAudioActivity *recordVoiceNoteAudioActivity;
@property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt; @property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt;
@ -189,9 +209,13 @@ typedef enum : NSUInteger {
@property (nonatomic) BOOL isViewCompletelyAppeared; @property (nonatomic) BOOL isViewCompletelyAppeared;
@property (nonatomic) BOOL isViewVisible; @property (nonatomic) BOOL isViewVisible;
@property (nonatomic) BOOL shouldObserveVMUpdates;
@property (nonatomic) BOOL viewHasEverAppeared; @property (nonatomic) BOOL viewHasEverAppeared;
@property (nonatomic) BOOL hasUnreadMessages; @property (nonatomic) BOOL hasUnreadMessages;
@property (nonatomic) BOOL isPickingMediaAsDocument; @property (nonatomic) BOOL isPickingMediaAsDocument;
@property (nonatomic, nullable) NSNumber *previousLastTimestamp;
@property (nonatomic, nullable) NSNumber *previousViewItemCount;
@property (nonatomic, nullable) NSNumber *previousViewTopToContentBottom;
@property (nonatomic, nullable) NSNumber *viewHorizonTimestamp; @property (nonatomic, nullable) NSNumber *viewHorizonTimestamp;
@property (nonatomic) ContactShareViewHelper *contactShareViewHelper; @property (nonatomic) ContactShareViewHelper *contactShareViewHelper;
@property (nonatomic) NSTimer *reloadTimer; @property (nonatomic) NSTimer *reloadTimer;
@ -482,6 +506,10 @@ typedef enum : NSUInteger {
_conversationViewModel = _conversationViewModel =
[[ConversationViewModel alloc] initWithThread:thread focusMessageIdOnOpen:focusMessageId delegate:self]; [[ConversationViewModel alloc] initWithThread:thread focusMessageIdOnOpen:focusMessageId delegate:self];
[self updateConversationSnapshot];
[self updateShouldObserveVMUpdates];
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
target:self target:self
selector:@selector(reloadTimerDidFire) selector:@selector(reloadTimerDidFire)
@ -499,9 +527,8 @@ typedef enum : NSUInteger {
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
if (self.isUserScrolling || !self.isViewCompletelyAppeared || !self.isViewVisible if (self.isUserScrolling || !self.isViewCompletelyAppeared || !self.isViewVisible || !self.shouldObserveVMUpdates
|| !CurrentAppContext().isAppForegroundAndActive || !self.viewHasEverAppeared || !self.viewHasEverAppeared) {
|| OWSWindowManager.sharedManager.isPresentingMenuActions) {
return; return;
} }
@ -670,6 +697,7 @@ typedef enum : NSUInteger {
- (void)applicationWillResignActive:(NSNotification *)notification - (void)applicationWillResignActive:(NSNotification *)notification
{ {
[self updateShouldObserveVMUpdates];
[self cancelVoiceMemo]; [self cancelVoiceMemo];
self.isUserScrolling = NO; self.isUserScrolling = NO;
[self saveDraft]; [self saveDraft];
@ -681,6 +709,7 @@ typedef enum : NSUInteger {
- (void)applicationDidBecomeActive:(NSNotification *)notification - (void)applicationDidBecomeActive:(NSNotification *)notification
{ {
[self updateShouldObserveVMUpdates];
[self startReadTimer]; [self startReadTimer];
} }
@ -746,12 +775,12 @@ typedef enum : NSUInteger {
- (NSArray<id<ConversationViewItem>> *)viewItems - (NSArray<id<ConversationViewItem>> *)viewItems
{ {
return self.conversationViewModel.viewItems; return self.conversationSnapshot.viewItems;
} }
- (ThreadDynamicInteractions *)dynamicInteractions - (ThreadDynamicInteractions *)dynamicInteractions
{ {
return self.conversationViewModel.dynamicInteractions; return self.conversationSnapshot.dynamicInteractions;
} }
- (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator - (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator
@ -840,6 +869,7 @@ typedef enum : NSUInteger {
// Avoid layout corrupt issues and out-of-date message subtitles. // Avoid layout corrupt issues and out-of-date message subtitles.
self.lastReloadDate = [NSDate new]; self.lastReloadDate = [NSDate new];
[self.conversationViewModel viewDidResetContentAndLayout]; [self.conversationViewModel viewDidResetContentAndLayout];
[self tryToUpdateConversationSnapshot];
[self.collectionView.collectionViewLayout invalidateLayout]; [self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData]; [self.collectionView reloadData];
@ -1690,7 +1720,7 @@ typedef enum : NSUInteger {
{ {
OWSAssertDebug(self.conversationViewModel); OWSAssertDebug(self.conversationViewModel);
self.showLoadMoreHeader = self.conversationViewModel.canLoadMoreItems; self.showLoadMoreHeader = self.conversationSnapshot.canLoadMoreItems;
} }
- (void)setShowLoadMoreHeader:(BOOL)showLoadMoreHeader - (void)setShowLoadMoreHeader:(BOOL)showLoadMoreHeader
@ -1931,6 +1961,8 @@ typedef enum : NSUInteger {
- (void)menuActionsDidHide:(MenuActionsViewController *)menuActionsViewController - (void)menuActionsDidHide:(MenuActionsViewController *)menuActionsViewController
{ {
[[OWSWindowManager sharedManager] hideMenuActionsWindow]; [[OWSWindowManager sharedManager] hideMenuActionsWindow];
[self updateShouldObserveVMUpdates];
} }
- (void)menuActions:(MenuActionsViewController *)menuActionsViewController - (void)menuActions:(MenuActionsViewController *)menuActionsViewController
@ -2037,6 +2069,8 @@ typedef enum : NSUInteger {
self.conversationViewModel.mostRecentMenuActionsViewItem = cell.viewItem; self.conversationViewModel.mostRecentMenuActionsViewItem = cell.viewItem;
[[OWSWindowManager sharedManager] showMenuActionsWindow:menuActionsViewController]; [[OWSWindowManager sharedManager] showMenuActionsWindow:menuActionsViewController];
[self updateShouldObserveVMUpdates];
} }
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId - (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
@ -4083,6 +4117,7 @@ typedef enum : NSUInteger {
{ {
_isViewVisible = isViewVisible; _isViewVisible = isViewVisible;
[self updateShouldObserveVMUpdates];
[self updateCellsVisible]; [self updateCellsVisible];
} }
@ -4095,8 +4130,134 @@ typedef enum : NSUInteger {
} }
} }
- (void)updateShouldObserveVMUpdates
{
if (!CurrentAppContext().isAppForegroundAndActive) {
self.shouldObserveVMUpdates = NO;
return;
}
if (!self.isViewVisible) {
self.shouldObserveVMUpdates = NO;
return;
}
if (OWSWindowManager.sharedManager.isPresentingMenuActions) {
self.shouldObserveVMUpdates = NO;
return;
}
self.shouldObserveVMUpdates = YES;
}
- (void)setShouldObserveVMUpdates:(BOOL)shouldObserveVMUpdates
{
if (_shouldObserveVMUpdates == shouldObserveVMUpdates) {
return;
}
_shouldObserveVMUpdates = shouldObserveVMUpdates;
if (self.shouldObserveVMUpdates) {
OWSLogVerbose(@"resume observation of view model.");
[self updateConversationSnapshot];
[self resetContentAndLayout];
[self updateBackButtonUnreadCount];
[self updateNavigationBarSubtitleLabel];
[self updateDisappearingMessagesConfiguration];
// Detect changes in the mapping's "window" size.
if (self.previousViewTopToContentBottom && self.previousViewItemCount
&& self.previousViewItemCount.unsignedIntegerValue != self.viewItems.count) {
CGFloat newContentHeight = self.safeContentHeight;
CGPoint newContentOffset
= CGPointMake(0, MAX(0, newContentHeight - self.previousViewTopToContentBottom.floatValue));
[self.collectionView setContentOffset:newContentOffset animated:NO];
}
// When we resume observing database changes, we want to scroll to show the user
// any new items inserted while we were not observing. We therefore find the
// first item at or after the "view horizon". See the comments below which explain
// the "view horizon".
id<ConversationViewItem> _Nullable lastViewItem = self.viewItems.lastObject;
BOOL hasAddedNewItems = (lastViewItem && self.previousLastTimestamp
&& lastViewItem.interaction.timestamp > self.previousLastTimestamp.unsignedLongLongValue);
OWSLogInfo(@"hasAddedNewItems: %d", hasAddedNewItems);
if (hasAddedNewItems) {
NSIndexPath *_Nullable indexPathToShow = [self firstIndexPathAtViewHorizonTimestamp];
if (indexPathToShow) {
// The goal is to show _both_ the last item before the "view horizon" and the
// first item after the "view horizon". We can't do "top on first item after"
// or "bottom on last item before" or we won't see the other. Unfortunately,
// this gets tricky if either is huge. The largest cells are oversize text,
// which should be rare. Other cells are considerably smaller than a screenful.
[self.collectionView scrollToItemAtIndexPath:indexPathToShow
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:NO];
}
}
self.viewHorizonTimestamp = nil;
OWSLogVerbose(@"resumed observation of view model.");
} else {
OWSLogVerbose(@"pausing observation of view model.");
// When stopping observation, try to record the timestamp of the "view horizon".
// The "view horizon" is where we'll want to focus the users when we resume
// observation if any changes have happened while we weren't observing.
// Ideally, we'll focus on those changes. But we can't skip over unread
// interactions, so we prioritize those, if any.
//
// We'll use this later to update the view to reflect any changes made while
// we were not observing the database. See extendRangeToIncludeUnobservedItems
// and the logic above.
id<ConversationViewItem> _Nullable lastViewItem = self.viewItems.lastObject;
if (lastViewItem) {
self.previousLastTimestamp = @(lastViewItem.interaction.timestamp);
self.previousViewItemCount = @(self.viewItems.count);
} else {
self.previousLastTimestamp = nil;
self.previousViewItemCount = nil;
}
__block TSInteraction *_Nullable firstUnseenInteraction = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
firstUnseenInteraction =
[[TSDatabaseView unseenDatabaseViewExtension:transaction] firstObjectInGroup:self.thread.uniqueId];
}];
if (firstUnseenInteraction) {
// If there are any unread interactions, focus on the first one.
self.viewHorizonTimestamp = @(firstUnseenInteraction.timestamp);
} else if (lastViewItem) {
// Otherwise, focus _just after_ the last interaction.
self.viewHorizonTimestamp = @(lastViewItem.interaction.timestamp + 1);
} else {
self.viewHorizonTimestamp = nil;
}
// Snapshot the scroll state by measuring the "distance from top of view to
// bottom of content"; if the mapping's "window" size grows, it will grow
// _upward_.
OWSAssertDebug([self.collectionView.collectionViewLayout isKindOfClass:[ConversationViewLayout class]]);
ConversationViewLayout *conversationViewLayout
= (ConversationViewLayout *)self.collectionView.collectionViewLayout;
// To avoid laying out the collection view during initial view
// presentation, don't trigger layout here (via safeContentHeight)
// until layout has been done at least once.
if (conversationViewLayout.hasEverHadLayout) {
self.previousViewTopToContentBottom = @(self.safeContentHeight - self.collectionView.contentOffset.y);
} else {
self.previousViewTopToContentBottom = nil;
}
OWSLogVerbose(@"paused observation of view model.");
}
}
- (nullable NSIndexPath *)firstIndexPathAtViewHorizonTimestamp - (nullable NSIndexPath *)firstIndexPathAtViewHorizonTimestamp
{ {
OWSAssertDebug(self.shouldObserveVMUpdates);
if (!self.viewHorizonTimestamp) { if (!self.viewHorizonTimestamp) {
return nil; return nil;
} }
@ -4443,6 +4604,11 @@ typedef enum : NSUInteger {
#pragma mark - ConversationViewModelDelegate #pragma mark - ConversationViewModelDelegate
- (BOOL)isObservingVMUpdates
{
return self.shouldObserveVMUpdates;
}
- (void)conversationViewModelWillUpdate - (void)conversationViewModelWillUpdate
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
@ -4466,6 +4632,11 @@ typedef enum : NSUInteger {
OWSAssertDebug(conversationUpdate); OWSAssertDebug(conversationUpdate);
OWSAssertDebug(self.conversationViewModel); OWSAssertDebug(self.conversationViewModel);
if (!self.shouldObserveVMUpdates) {
return;
}
[self updateConversationSnapshot];
[self updateBackButtonUnreadCount]; [self updateBackButtonUnreadCount];
[self updateNavigationBarSubtitleLabel]; [self updateNavigationBarSubtitleLabel];
@ -4763,6 +4934,26 @@ typedef enum : NSUInteger {
[self.inputToolbar updateLayoutWithSafeAreaInsets:safeAreaInsets]; [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 @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -76,6 +76,8 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
- (void)conversationViewModelDidDeleteMostRecentMenuActionsViewItem; - (void)conversationViewModelDidDeleteMostRecentMenuActionsViewItem;
- (BOOL)isObservingVMUpdates;
- (ConversationStyle *)conversationStyle; - (ConversationStyle *)conversationStyle;
@end @end

@ -542,12 +542,19 @@ static const int kYapDatabaseRangeMaxLength = 25000;
OWSLogVerbose(@""); OWSLogVerbose(@"");
if (!self.delegate.isObservingVMUpdates) {
return;
}
// External database modifications (e.g. changes from another process such as the SAE) // External database modifications (e.g. changes from another process such as the SAE)
// are "flushed" using touchDbAsync when the app re-enters the foreground. // are "flushed" using touchDbAsync when the app re-enters the foreground.
} }
- (void)uiDatabaseWillUpdate:(NSNotification *)notification - (void)uiDatabaseWillUpdate:(NSNotification *)notification
{ {
if (!self.delegate.isObservingVMUpdates) {
return;
}
[self.delegate conversationViewModelWillUpdate]; [self.delegate conversationViewModelWillUpdate];
} }
@ -698,6 +705,13 @@ static const int kYapDatabaseRangeMaxLength = 25000;
OWSAssertDebug(oldItemIdList); OWSAssertDebug(oldItemIdList);
OWSAssertDebug(updatedItemSetParam); OWSAssertDebug(updatedItemSetParam);
if (!self.delegate.isObservingVMUpdates) {
OWSLogVerbose(@"Skipping VM update.");
// We fire this event, but it will be ignored.
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.minorUpdate];
return;
}
if (oldItemIdList.count != [NSSet setWithArray:oldItemIdList].count) { if (oldItemIdList.count != [NSSet setWithArray:oldItemIdList].count) {
OWSFailDebug(@"Old view item list has duplicates."); OWSFailDebug(@"Old view item list has duplicates.");
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate]; [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];

Loading…
Cancel
Save