Improve handling of db modifications while conversation view is not observing.
This commit is contained in:
parent
31d22e3e35
commit
2ac7716771
|
@ -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];
|
||||
|
||||
|
|
Loading…
Reference in New Issue