From ee07490d3e1b5eb08a395c7a8fbe36600502d550 Mon Sep 17 00:00:00 2001 From: dtsbourg Date: Wed, 31 Dec 2014 13:22:40 +0100 Subject: [PATCH] MessagesVC: Paging and fix scrolling bug. Reviewed-by: @FredericJacobs --- Signal.xcodeproj/project.pbxproj | 6 + Signal/src/util/TSAdapterCacheManager.h | 27 ++++ Signal/src/util/TSAdapterCacheManager.m | 58 +++++++ .../view controllers/MessagesViewController.m | 146 ++++++++++++++++-- 4 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 Signal/src/util/TSAdapterCacheManager.h create mode 100644 Signal/src/util/TSAdapterCacheManager.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 325531081..df6c8e6d7 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -505,6 +505,7 @@ FC4FA0331A1D46AE00DA100A /* InitialViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FC4FA0321A1D46AE00DA100A /* InitialViewController.m */; }; FC5CDF391A3393DD00B47253 /* error_white@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FC5CDF371A3393DD00B47253 /* error_white@2x.png */; }; FC5CDF3A1A3393DD00B47253 /* warning_white@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FC5CDF381A3393DD00B47253 /* warning_white@2x.png */; }; + FC7C7A961A581AF40091823B /* TSAdapterCacheManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FC7C7A951A581AF40091823B /* TSAdapterCacheManager.m */; }; FC9120411A39EFB70074545C /* qr@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FC91203F1A39EFB70074545C /* qr@2x.png */; }; FC9120431A39F9E00074545C /* qr_scan_fingerprint@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FC9120421A39F9E00074545C /* qr_scan_fingerprint@2x.png */; }; FCA52AE61A2B676C00CCADFA /* call_canceled@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FCA52ADE1A2B676C00CCADFA /* call_canceled@2x.png */; }; @@ -1211,6 +1212,8 @@ FC4FA0321A1D46AE00DA100A /* InitialViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InitialViewController.m; sourceTree = ""; }; FC5CDF371A3393DD00B47253 /* error_white@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "error_white@2x.png"; sourceTree = ""; }; FC5CDF381A3393DD00B47253 /* warning_white@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "warning_white@2x.png"; sourceTree = ""; }; + FC7C7A941A581AF40091823B /* TSAdapterCacheManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAdapterCacheManager.h; sourceTree = ""; }; + FC7C7A951A581AF40091823B /* TSAdapterCacheManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAdapterCacheManager.m; sourceTree = ""; }; FC91203F1A39EFB70074545C /* qr@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "qr@2x.png"; sourceTree = ""; }; FC9120421A39F9E00074545C /* qr_scan_fingerprint@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "qr_scan_fingerprint@2x.png"; sourceTree = ""; }; FCA52ADE1A2B676C00CCADFA /* call_canceled@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "call_canceled@2x.png"; sourceTree = ""; }; @@ -1833,6 +1836,8 @@ 76EB04FD18170B33006006FC /* Zid.m */, FCC81A961A44558300DFEC7D /* UIDevice+TSHardwareVersion.h */, FCC81A971A44558300DFEC7D /* UIDevice+TSHardwareVersion.m */, + FC7C7A941A581AF40091823B /* TSAdapterCacheManager.h */, + FC7C7A951A581AF40091823B /* TSAdapterCacheManager.m */, ); path = util; sourceTree = ""; @@ -3272,6 +3277,7 @@ 7038632718F70C0700D4A43F /* CryptoTools.m in Sources */, 76EB058C18170B33006006FC /* DnsManager.m in Sources */, B63AF5CB1A1F757900D01AAD /* TSRegisterPrekeysRequest.m in Sources */, + FC7C7A961A581AF40091823B /* TSAdapterCacheManager.m in Sources */, B6FAAAE81A41BC6C007FEC1D /* TSAttachmentPointer.m in Sources */, B6B096881A1D25ED008BFAA6 /* TSStorageManager+keyFromIntLong.m in Sources */, B6B50AAB1A4192C500F8F607 /* TSMessagesManager+attachments.m in Sources */, diff --git a/Signal/src/util/TSAdapterCacheManager.h b/Signal/src/util/TSAdapterCacheManager.h new file mode 100644 index 000000000..eafef6009 --- /dev/null +++ b/Signal/src/util/TSAdapterCacheManager.h @@ -0,0 +1,27 @@ +// +// TSAdapterCacheManager.h +// Signal +// +// Created by Dylan Bourgeois on 03/01/15. +// Copyright (c) 2015 Open Whisper Systems. All rights reserved. +// + +#import + +@class TSMessageAdapter; + +@interface TSAdapterCacheManager : NSObject { + NSCache * messageAdaptersCache; +} + +@property (nonatomic, retain) NSCache *messageAdaptersCache; + ++ (id)sharedManager; + +- (void)cacheAdapter:(TSMessageAdapter*)adapter forInteractionId:(NSString*)identifier; +- (void)clearCacheEntryForInteractionId:(NSString*)identifier; +- (TSMessageAdapter*)adapterForInteractionId:(NSString*)identifier; +- (BOOL)containsCacheEntryForInteractionId:(NSString*)identifier; + + +@end diff --git a/Signal/src/util/TSAdapterCacheManager.m b/Signal/src/util/TSAdapterCacheManager.m new file mode 100644 index 000000000..037ce636b --- /dev/null +++ b/Signal/src/util/TSAdapterCacheManager.m @@ -0,0 +1,58 @@ +// +// TSAdapterCacheManager.m +// Signal +// +// Created by Dylan Bourgeois on 03/01/15. +// Copyright (c) 2015 Open Whisper Systems. All rights reserved. +// + +#import "TSAdapterCacheManager.h" +#import "TSMessageAdapter.h" + +@implementation TSAdapterCacheManager + +@synthesize messageAdaptersCache; + ++ (id)sharedManager { + static TSAdapterCacheManager *sharedManager = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedManager = [[self alloc] init]; + }); + + return sharedManager; +} + +- (id)init { + if (self = [super init]) { + messageAdaptersCache = [[NSCache alloc]init]; + } + return self; +} + +- (void)cacheAdapter:(TSMessageAdapter*)adapter forInteractionId:(NSString*)identifier +{ + NSParameterAssert(adapter); + NSParameterAssert(identifier); + [messageAdaptersCache setObject:adapter forKey:identifier]; +} + +-(void)clearCacheEntryForInteractionId:(NSString*)identifier +{ + NSParameterAssert(identifier); + [messageAdaptersCache removeObjectForKey:identifier]; +} + +-(TSMessageAdapter*)adapterForInteractionId:(NSString*)identifier +{ + NSParameterAssert(identifier); + return [messageAdaptersCache objectForKey:identifier]; +} + +-(BOOL)containsCacheEntryForInteractionId:(NSString*)identifier +{ + return [messageAdaptersCache objectForKey:identifier] != nil; +} + +@end diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m index 559ddc333..f8f86fb7c 100644 --- a/Signal/src/view controllers/MessagesViewController.m +++ b/Signal/src/view controllers/MessagesViewController.m @@ -54,6 +54,12 @@ #import "ContactsManager.h" #import "PreferencesUtil.h" +#import "TSAdapterCacheManager.h" + +#define kYapDatabaseRangeLength 50 +#define kYapDatabaseRangeMaxLength 300 +#define kYapDatabaseRangeMinLength 20 + static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60; static NSString *const kUpdateGroupSegueIdentifier = @"updateGroupSegue"; static NSString *const kFingerprintSegueIdentifier = @"fingerprintSegue"; @@ -82,6 +88,8 @@ typedef enum : NSUInteger { @property (nonatomic, retain) NSIndexPath *lastDeliveredMessageIndexPath; +@property NSUInteger page; + @end @implementation MessagesViewController @@ -115,6 +123,7 @@ typedef enum : NSUInteger { - (void)viewDidLoad { [super viewDidLoad]; + [self markAllMessagesAsRead]; [self initializeBubbles]; @@ -122,14 +131,17 @@ typedef enum : NSUInteger { self.messageMappings = [[YapDatabaseViewMappings alloc] initWithGroups:@[self.thread.uniqueId] view:TSMessageDatabaseViewExtensionName]; + self.page = 0; + + [self updateRangeOptionsForPage:self.page]; + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.messageMappings updateWithTransaction:transaction]; }]; - + [self initializeToolbars]; [self initializeCollectionViewLayout]; - self.senderId = ME_MESSAGE_IDENTIFIER self.senderDisplayName = ME_MESSAGE_IDENTIFIER @@ -142,8 +154,10 @@ typedef enum : NSUInteger { -(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - self.automaticallyScrollsToMostRecentMessage = YES; - [self scrollToBottomAnimated:NO]; + + NSIndexPath * lastCellIndexPath = [NSIndexPath indexPathForRow:(NSInteger)[self.messageMappings numberOfItemsInGroup:self.thread.uniqueId]-1 inSection:0]; + [self.collectionView scrollToItemAtIndexPath:lastCellIndexPath atScrollPosition:UICollectionViewScrollPositionBottom animated:NO]; + } - (void)startReadTimer{ @@ -157,7 +171,6 @@ typedef enum : NSUInteger { - (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [self startReadTimer]; - [self scrollToBottomAnimated:YES]; } - (void)viewWillDisappear:(BOOL)animated{ @@ -219,7 +232,7 @@ typedef enum : NSUInteger { self.collectionView.showsVerticalScrollIndicator = NO; self.collectionView.showsHorizontalScrollIndicator = NO; - self.automaticallyScrollsToMostRecentMessage = NO; + [self updateLoadEarlierVisible]; self.collectionView.collectionViewLayout.incomingAvatarViewSize = CGSizeZero; self.collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero; @@ -426,7 +439,7 @@ typedef enum : NSUInteger { -(BOOL)shouldShowMessageStatusAtIndexPath:(NSIndexPath*)indexPath { - + TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath]; if([self.thread isKindOfClass:[TSGroupThread class]]) { return currentMessage.messageType == TSIncomingMessageAdapter; @@ -513,8 +526,7 @@ typedef enum : NSUInteger { { TSMessageAdapter *messageItem = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath]; TSInteraction *interaction = [self interactionAtIndexPath:indexPath]; - - + switch (messageItem.messageType) { case TSOutgoingMessageAdapter: if (messageItem.messageState == TSOutgoingMessageStateUnsent) { @@ -558,6 +570,81 @@ typedef enum : NSUInteger { } } +-(void)collectionView:(JSQMessagesCollectionView *)collectionView header:(JSQMessagesLoadEarlierHeaderView *)headerView didTapLoadEarlierMessagesButton:(UIButton *)sender +{ + if ([self shouldShowLoadEarlierMessages]) { + self.page++; + } + + NSInteger item = (NSInteger)[self scrollToItem]; + + [self updateRangeOptionsForPage:self.page]; + + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self.messageMappings updateWithTransaction:transaction]; + }]; + + [self updateLayoutForEarlierMessagesWithOffset:item]; + +} + +-(BOOL)shouldShowLoadEarlierMessages +{ + __block BOOL show = YES; + + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){ + show = [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId] < [[transaction ext:TSMessageDatabaseViewExtensionName] numberOfItemsInGroup:self.thread.uniqueId]; + }]; + + return show; +} + +-(NSUInteger)scrollToItem +{ + __block NSUInteger item = kYapDatabaseRangeLength*(self.page+1) - [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId]; + + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + + NSUInteger numberOfVisibleMessages = [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId] ; + NSUInteger numberOfTotalMessages = [[transaction ext:TSMessageDatabaseViewExtensionName] numberOfItemsInGroup:self.thread.uniqueId] ; + NSUInteger numberOfMessagesToLoad = numberOfTotalMessages - numberOfVisibleMessages ; + + BOOL canLoadFullRange = numberOfMessagesToLoad >= kYapDatabaseRangeLength; + + if (!canLoadFullRange) { + item = numberOfMessagesToLoad; + } + }]; + + return item == 0 ? item : item - 1; +} + +-(void)updateLoadEarlierVisible +{ + [self setShowLoadEarlierMessagesHeader:[self shouldShowLoadEarlierMessages]]; +} + +-(void)updateLayoutForEarlierMessagesWithOffset:(NSInteger)offset +{ + [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; + [self.collectionView reloadData]; + + [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:offset inSection:0] atScrollPosition:UICollectionViewScrollPositionTop animated:NO]; + + [self updateLoadEarlierVisible]; +} + +-(void)updateRangeOptionsForPage:(NSUInteger)page +{ + YapDatabaseViewRangeOptions *rangeOptions = [YapDatabaseViewRangeOptions flexibleRangeWithLength:kYapDatabaseRangeLength*(page+1) offset:0 from:YapDatabaseViewEnd]; + + rangeOptions.maxLength = kYapDatabaseRangeMaxLength; + rangeOptions.minLength = kYapDatabaseRangeMinLength; + + [self.messageMappings setRangeOptions:rangeOptions forGroup:self.thread.uniqueId]; + +} + #pragma mark Bubble User Actions - (void)handleUnsentMessageTap:(TSOutgoingMessage*)message{ @@ -823,48 +910,65 @@ typedef enum : NSUInteger { forNotifications:notifications withMappings:self.messageMappings]; + __block BOOL containsInsertion = NO; + if (!messageRowChanges) { return; } - [self.collectionView performBatchUpdates:^{ - + for (YapDatabaseViewRowChange *rowChange in messageRowChanges) { switch (rowChange.type) { case YapDatabaseViewChangeDelete : { + TSInteraction * interaction = [self interactionAtIndexPath:rowChange.indexPath]; + [[TSAdapterCacheManager sharedManager] clearCacheEntryForInteractionId:interaction.uniqueId]; [self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath ]]; break; } case YapDatabaseViewChangeInsert : { + TSInteraction * interaction = [self interactionAtIndexPath:rowChange.newIndexPath]; + [[TSAdapterCacheManager sharedManager] cacheAdapter:[TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread] forInteractionId:interaction.uniqueId]; [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]]; + containsInsertion = YES; break; } case YapDatabaseViewChangeMove : { - [self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath]]; - [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath]]; + [self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath ]]; + [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]]; break; } case YapDatabaseViewChangeUpdate : { NSMutableArray *rowsToUpdate = [@[rowChange.indexPath] mutableCopy]; + if (_lastDeliveredMessageIndexPath) { [rowsToUpdate addObject:_lastDeliveredMessageIndexPath]; } + for (NSIndexPath* indexPath in rowsToUpdate) { + TSInteraction * interaction = [self interactionAtIndexPath:indexPath]; + [[TSAdapterCacheManager sharedManager] cacheAdapter:[TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread] forInteractionId:interaction.uniqueId]; + } + [self.collectionView reloadItemsAtIndexPaths:rowsToUpdate]; break; } } } - - } completion:^(BOOL finished) { - [self scrollToBottomAnimated:YES]; + } completion:^(BOOL success) { + if (success) { + [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; + [self.collectionView reloadData]; + } + if (containsInsertion) { + [self scrollToBottomAnimated:YES]; + } }]; } @@ -897,8 +1001,16 @@ typedef enum : NSUInteger { - (TSMessageAdapter*)messageAtIndexPath:(NSIndexPath *)indexPath { TSInteraction *interaction = [self interactionAtIndexPath:indexPath]; - return [TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread]; + TSAdapterCacheManager * manager = [TSAdapterCacheManager sharedManager]; + + if (![manager containsCacheEntryForInteractionId:interaction.uniqueId]) { + [manager cacheAdapter:[TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread] forInteractionId:interaction.uniqueId]; + } + + return [manager adapterForInteractionId:interaction.uniqueId]; } + + #pragma mark group action view -(void)didPressGroupMenuButton:(UIButton *)sender {