Improve handling of db modifications while conversation view is not observing.

This commit is contained in:
Matthew Chen 2018-02-21 18:37:19 -05:00
parent 31d22e3e35
commit 2ac7716771
1 changed files with 169 additions and 41 deletions

View File

@ -115,13 +115,6 @@ typedef enum : NSUInteger {
kMediaTypeVideo,
} kMediaTypes;
typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// This mode should only be used when initially configuring the range,
// since we want the range to monotonically grow after that.
MessagesRangeSizeMode_Truncate,
MessagesRangeSizeMode_Normal
};
#pragma mark -
@interface ConversationViewController () <AttachmentApprovalViewControllerDelegate,
@ -195,7 +188,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
@property (nonatomic, readonly) UILabel *backButtonUnreadCountLabel;
@property (nonatomic, readonly) NSUInteger backButtonUnreadCount;
@property (nonatomic) NSUInteger page;
@property (nonatomic) NSUInteger lastRangeLength;
@property (nonatomic) BOOL composeOnOpen;
@property (nonatomic) BOOL callOnOpen;
@property (nonatomic) BOOL peek;
@ -235,6 +228,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
@property (nonatomic) BOOL viewHasEverAppeared;
@property (nonatomic) BOOL hasUnreadMessages;
@property (nonatomic) BOOL isPickingMediaAsDocument;
@property (nonatomic, nullable) NSNumber *previousLastTimestamp;
@property (nonatomic, nullable) NSNumber *previousLastUnreadTimestamp;
@end
@ -433,7 +428,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// We need to update the "unread indicator" _before_ we determine the initial range
// size, since it depends on where the unread indicator is placed.
self.page = 0;
self.lastRangeLength = 0;
[self ensureDynamicInteractions];
if (thread.uniqueId.length > 0) {
@ -451,7 +446,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.messageMappings updateWithTransaction:transaction];
}];
[self updateMessageMappingRangeOptions:MessagesRangeSizeMode_Truncate];
[self updateMessageMappingRangeOptions];
[self updateShouldObserveDBModifications];
}
@ -1528,7 +1523,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// the dynamic interactions and re-layout. Here we take a "before" snapshot.
CGFloat scrollDistanceToBottom = self.safeContentHeight - self.collectionView.contentOffset.y;
self.page = MIN(self.page + 1, (NSUInteger)kYapDatabaseMaxPageCount - 1);
self.lastRangeLength = MIN(self.lastRangeLength + kYapDatabasePageSize, (NSUInteger) kYapDatabaseRangeMaxLength);
[self resetMappings];
@ -1546,7 +1541,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
- (void)updateShowLoadMoreHeader
{
if (self.page == kYapDatabaseMaxPageCount - 1) {
if (self.lastRangeLength == kYapDatabaseRangeMaxLength) {
self.showLoadMoreHeader = NO;
return;
}
@ -1595,20 +1590,19 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[self updateBarButtonItems];
}
- (void)updateMessageMappingRangeOptions:(MessagesRangeSizeMode)mode
- (void)updateMessageMappingRangeOptions
{
// The "old" range length may have been increased by insertions of new messages
// at the bottom of the window.
NSUInteger oldLength = [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId];
NSUInteger targetLength = oldLength;
if (mode == MessagesRangeSizeMode_Truncate) {
NSUInteger rangeLength = 0;
if (self.lastRangeLength == 0) {
// If this is the first time we're configuring the range length,
// try to take into account the position of the unread indicator.
OWSAssert(self.dynamicInteractions);
OWSAssert(self.page == 0);
if (self.dynamicInteractions.unreadIndicatorPosition) {
NSUInteger unreadIndicatorPosition
= (NSUInteger)[self.dynamicInteractions.unreadIndicatorPosition longValue];
// If there is an unread indicator, increase the initial load window
// to include it.
OWSAssert(unreadIndicatorPosition > 0);
@ -1617,23 +1611,21 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// We'd like to include at least N seen messages,
// to give the user the context of where they left off the conversation.
const NSUInteger kPreferredSeenMessageCount = 1;
targetLength = unreadIndicatorPosition + kPreferredSeenMessageCount;
} else {
// Default to a single page of messages.
targetLength = kYapDatabasePageSize;
rangeLength = unreadIndicatorPosition + kPreferredSeenMessageCount;
}
}
// The "page-based" range length may have been increased by loading "prev" pages at the
// top of the window.
NSUInteger rangeLength;
while (YES) {
rangeLength = kYapDatabasePageSize * (self.page + 1);
if (rangeLength >= targetLength) {
break;
}
self.page = self.page + 1;
}
// Always try to load at least a single page of messages.
rangeLength = MAX(rangeLength, kYapDatabasePageSize);
// Range size should monotonically increase.
rangeLength = MAX(rangeLength, self.lastRangeLength);
// Enforce max range size.
rangeLength = MIN(rangeLength, kYapDatabaseRangeMaxLength);
self.lastRangeLength = rangeLength;
YapDatabaseViewRangeOptions *rangeOptions =
[YapDatabaseViewRangeOptions flexibleRangeWithLength:rangeLength offset:0 from:YapDatabaseViewEnd];
@ -2195,7 +2187,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
{
OWSAssertIsOnMainThread();
const int currentMaxRangeSize = (int)(self.page + 1) * kYapDatabasePageSize;
const int currentMaxRangeSize = (int)self.lastRangeLength;
const int maxRangeSize = MAX(kConversationInitialMaxRangeSize, currentMaxRangeSize);
// `ensureDynamicInteractionsForThread` should operate on the latest thread contents, so
@ -2828,10 +2820,16 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
// External database modifications can't be converted into incremental updates,
// so rebuild everything. This is expensive and usually isn't necessary, but
// there's no alternative.
[self resetMappings];
if (self.shouldObserveDBModifications) {
// External database modifications can't be converted into incremental updates,
// so rebuild everything. This is expensive and usually isn't necessary, but
// there's no alternative.
//
// There's no need to do this when app is in the background or this view isn't
// visible. We will resetMappings when the app re-enters the foreground or this
// view becomes visible.
[self resetMappings];
}
}
- (void)yapDatabaseModified:(NSNotification *)notification
@ -2905,7 +2903,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// range that are not within the current mapping's contents. We
// may need to extend the mapping's contents to reflect the current
// range.
[self updateMessageMappingRangeOptions:MessagesRangeSizeMode_Normal];
[self updateMessageMappingRangeOptions];
// Calling resetContentAndLayout is a bit expensive.
// Since by definition this won't affect any cells in the previous
// range, it should be sufficient to call invalidateLayout.
@ -4143,6 +4141,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
//
// TODO: have a more fine-grained cache expiration based on rows modified.
[self.viewItemCache removeAllObjects];
// Snapshot the "previousLastTimestamp" value; it will be cleared by resetMappings.
NSNumber *_Nullable previousLastTimestamp = self.previousLastTimestamp;
[self resetMappings];
@ -4154,9 +4155,135 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
CGPoint newContentOffset = CGPointMake(0, MAX(0, newContentHeight - viewTopToContentBottom));
[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 which was previously unread and center it in the view. We don't
// want to make any assumptions, so we select the item to center on based on
// "previous unread timestamp" rather than a specific
ConversationViewItem *_Nullable lastViewItem = self.viewItems.lastObject;
BOOL hasAddedNewItems = (lastViewItem &&
previousLastTimestamp &&
lastViewItem.interaction.timestamp > previousLastTimestamp.unsignedLongLongValue);
DDLogInfo(@"%@ hasAddedNewItems: %d", self.logTag, hasAddedNewItems);
if (hasAddedNewItems) {
NSIndexPath *_Nullable indexPathToShow = [self firstIndexPathAtPreviousLastUnreadTimestamp];
if (indexPathToShow) {
[self.collectionView scrollToItemAtIndexPath:indexPathToShow
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:YES];
}
}
self.previousLastUnreadTimestamp = nil;
} else {
// When stopping observation, try to record the timestamp of the last item.
//
// 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.
ConversationViewItem *_Nullable lastViewItem = self.viewItems.lastObject;
if (lastViewItem) {
self.previousLastTimestamp = @(lastViewItem.interaction.timestamp);
} else {
self.previousLastTimestamp = nil;
}
__block TSInteraction *_Nullable firstUnseenInteraction = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
firstUnseenInteraction =
[[TSDatabaseView unseenDatabaseViewExtension:transaction] firstObjectInGroup:self.thread.uniqueId];
}];
if (firstUnseenInteraction) {
self.previousLastUnreadTimestamp = @(firstUnseenInteraction.timestamp);
} else {
self.previousLastUnreadTimestamp = nil;
}
}
}
- (nullable NSIndexPath *)firstIndexPathAtPreviousLastUnreadTimestamp
{
OWSAssert(self.shouldObserveDBModifications);
if (!self.previousLastUnreadTimestamp) {
return nil;
}
if (self.viewItems.count < 1) {
return nil;
}
uint64_t previousLastUnreadTimestamp = self.previousLastUnreadTimestamp.unsignedLongLongValue;
// Binary search for the first view item whose timestamp >= the "previous last unread" timestamp.
NSUInteger left = 0, right = self.viewItems.count - 1;
while (left != right) {
OWSAssert(left < right);
NSUInteger mid = (left + right) / 2;
OWSAssert(left <= mid);
OWSAssert(mid < right);
ConversationViewItem *viewItem = self.viewItems[mid];
if (viewItem.interaction.timestamp >= previousLastUnreadTimestamp) {
right = mid;
} else {
// This is an optimization; it also ensures that we converge.
left = mid + 1;
}
}
OWSAssert(left == right);
ConversationViewItem *viewItem = self.viewItems[left];
if (viewItem.interaction.timestamp >= previousLastUnreadTimestamp) {
DDLogInfo(@"%@ firstIndexPathAtPreviousLastUnreadTimestamp: %zd / %zd", self.logTag, left, self.viewItems.count);
return [NSIndexPath indexPathForRow:(NSInteger) left inSection:0];
} else {
DDLogInfo(@"%@ firstIndexPathAtPreviousLastUnreadTimestamp: none / %zd", self.logTag, self.viewItems.count);
return nil;
}
}
// We stop observing database modifications when the app or this view is not visible
// (see: shouldObserveDBModifications). When we resume observing db modifications,
// we want to extend the "range" of this view to include any items added to this
// thread while we were not observing.
- (void)extendRangeToIncludeUnobservedItems
{
if (!self.shouldObserveDBModifications) {
return;
}
if (!self.previousLastTimestamp) {
return;
}
uint64_t previousLastTimestamp = self.previousLastTimestamp.unsignedLongLongValue;
__block NSUInteger addedItemCount = 0;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[[transaction ext:TSMessageDatabaseViewExtensionName]
enumerateRowsInGroup:self.thread.uniqueId
withOptions:NSEnumerationReverse
usingBlock:^(NSString *collection,
NSString *key,
id object,
id metadata,
NSUInteger index,
BOOL *stop) {
if (![object isKindOfClass:[TSInteraction class]]) {
OWSFail(@"Expected a TSInteraction: %@", [object class]);
return;
}
TSInteraction *interaction = (TSInteraction *)object;
if (interaction.timestamp <= previousLastTimestamp) {
*stop = YES;
return;
}
addedItemCount++;
}];
}];
DDLogInfo(@"%@ extendRangeToIncludeUnobservedItems: %zd", self.logTag, addedItemCount);
self.lastRangeLength += addedItemCount;
// We only want to do this once, so clear the "previous last timestamp".
self.previousLastTimestamp = nil;
}
- (void)resetMappings
{
// If we're entering "active" mode (e.g. view is visible and app is in foreground),
@ -4168,10 +4295,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// We need to `beginLongLivedReadTransaction` before we update our
// mapping in order to jump to the most recent commit.
[self.uiDatabaseConnection beginLongLivedReadTransaction];
[self extendRangeToIncludeUnobservedItems];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.messageMappings updateWithTransaction:transaction];
}];
[self updateMessageMappingRangeOptions:MessagesRangeSizeMode_Normal];
[self updateMessageMappingRangeOptions];
}
[self reloadViewItems];