6161@import MediaPlayer;
6262
6363static NSTimeInterval const kOTRMessageSentDateShowTimeInterval = 5 * 60 ;
64+ static NSUInteger const kOTRMessagePageSize = 50 ;
6465
6566typedef NS_ENUM (int , OTRDropDownType) {
6667 OTRDropDownTypeNone = 0 ,
6768 OTRDropDownTypeEncryption = 1 ,
6869 OTRDropDownTypePush = 2
6970};
7071
71- @interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPickerDelegate, OTRYapViewHandlerDelegateProtocol, OTRMessagesCollectionViewFlowLayoutSizeProtocol>
72+ @interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPickerDelegate, OTRYapViewHandlerDelegateProtocol, OTRMessagesCollectionViewFlowLayoutSizeProtocol> {
73+ JSQMessagesAvatarImage *_warningAvatarImage;
74+ JSQMessagesAvatarImage *_accountAvatarImage;
75+ JSQMessagesAvatarImage *_buddyAvatarImage;
76+ }
7277
7378@property (nonatomic , strong ) OTRYapViewHandler *viewHandler;
7479
@@ -91,6 +96,11 @@ @interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPicker
9196@property (nonatomic , strong ) NSTimer *lastSeenRefreshTimer;
9297@property (nonatomic , strong ) UIView *jidForwardingHeaderView;
9398
99+ @property (nonatomic ) BOOL loadingMessages;
100+ @property (nonatomic , strong ) NSIndexPath *currentIndexPath;
101+ @property (nonatomic , strong ) id currentMessage;
102+ @property (nonatomic , strong ) NSCache *messageSizeCache;
103+
94104@end
95105
96106@implementation OTRMessagesViewController
@@ -101,6 +111,8 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
101111 self.senderId = @" " ;
102112 self.senderDisplayName = @" " ;
103113 _state = [[MessagesViewControllerState alloc ] init ];
114+ self.messageSizeCache = [NSCache new ];
115+ self.messageSizeCache .countLimit = kOTRMessagePageSize ;
104116 }
105117 return self;
106118}
@@ -166,7 +178,12 @@ - (void)viewDidLoad
166178 OTRMessagesCollectionViewFlowLayout *layout = [[OTRMessagesCollectionViewFlowLayout alloc ] init ];
167179 layout.sizeDelegate = self;
168180 self.collectionView .collectionViewLayout = layout;
169-
181+
182+ // /"Loading Earlier" header view
183+ [self .collectionView registerNib: [UINib nibWithNibName: @" OTRMessagesLoadingView" bundle: OTRAssets.resourcesBundle]
184+ forSupplementaryViewOfKind: UICollectionElementKindSectionHeader
185+ withReuseIdentifier: [JSQMessagesLoadEarlierHeaderView headerReuseIdentifier ]];
186+
170187 // Subscribe to changes in encryption state
171188 __weak typeof (self)weakSelf = self;
172189 [self .KVOController observe: self .state keyPath: NSStringFromSelector (@selector (messageSecurity )) options: NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block: ^(id _Nullable observer, id _Nonnull object, NSDictionary <NSString *,id > * _Nonnull change) {
@@ -207,6 +224,7 @@ - (void)viewDidAppear:(BOOL)animated
207224 dispatch_after (dispatch_time (DISPATCH_TIME_NOW, (int64_t )(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue (), ^{
208225 [self scrollToBottomAnimated: animated];
209226 });
227+ self.loadingMessages = NO ;
210228}
211229
212230- (void )viewWillAppear : (BOOL )animated
@@ -262,8 +280,8 @@ - (void)viewWillAppear:(BOOL)animated
262280 [self moveLastComposingTextForThreadKey: self .threadKey colleciton: self .threadCollection toTextView: self .inputToolbar.contentView.textView];
263281 }
264282 }
265-
266-
283+
284+ self. loadingMessages = YES ;
267285 [self .collectionView reloadData ];
268286}
269287
@@ -282,6 +300,15 @@ - (void)viewWillDisappear:(BOOL)animated
282300 // [self.inputToolbar.contentView.textView resignFirstResponder];
283301}
284302
303+ - (void )viewDidDisappear : (BOOL )animated
304+ {
305+ [super viewDidDisappear: animated];
306+
307+ _warningAvatarImage = nil ;
308+ _accountAvatarImage = nil ;
309+ _buddyAvatarImage = nil ;
310+ }
311+
285312#pragma - mark Setters & getters
286313
287314- (OTRAttachmentPicker *)attachmentPicker
@@ -1026,7 +1053,82 @@ - (void)moveLastComposingTextForThreadKey:(NSString *)key colleciton:(NSString *
10261053
10271054- (id <OTRMessageProtocol,JSQMessageData>)messageAtIndexPath : (NSIndexPath *)indexPath
10281055{
1029- return [self .viewHandler object: indexPath];
1056+ // Multiple invocations with the same indexPath tend to come in groups, no need to hit the DB each time.
1057+ // Even though the object is cached, the row ID calculation still takes time
1058+ if (![indexPath isEqual: self .currentIndexPath]) {
1059+ self.currentIndexPath = indexPath;
1060+ self.currentMessage = [self .viewHandler object: indexPath];
1061+ }
1062+ return self.currentMessage ;
1063+ }
1064+
1065+ /* *
1066+ * Updates the flexible range of the DB connection.
1067+ * @param reset When NO, adds kOTRMessagePageSize to the range length, when YES resets the length to the kOTRMessagePageSize
1068+ */
1069+ - (void )updateRangeOptions : (BOOL )reset
1070+ {
1071+ YapDatabaseViewRangeOptions *options = [self .viewHandler.mappings rangeOptionsForGroup: self .threadKey];
1072+ if (reset) {
1073+ if (options != nil && options.length <= kOTRMessagePageSize ) {
1074+ return ;
1075+ }
1076+ options = [YapDatabaseViewRangeOptions flexibleRangeWithLength: kOTRMessagePageSize
1077+ offset: 0
1078+ from: YapDatabaseViewEnd];
1079+ self.messageSizeCache .countLimit = kOTRMessagePageSize ;
1080+ } else {
1081+ options = [YapDatabaseViewRangeOptions flexibleRangeWithLength: options.length + kOTRMessagePageSize
1082+ offset: 0
1083+ from: YapDatabaseViewEnd];
1084+ self.messageSizeCache .countLimit += kOTRMessagePageSize ;
1085+ }
1086+ [self .viewHandler.mappings setRangeOptions: options forGroup: self .threadKey];
1087+
1088+ self.loadingMessages = YES ;
1089+
1090+ CGFloat distanceToBottom = self.collectionView .contentSize .height - self.collectionView .contentOffset .y ;
1091+
1092+ void (^doReload)() = ^{
1093+ [self .collectionView reloadData ];
1094+
1095+ [self .readOnlyDatabaseConnection readWithBlock: ^(YapDatabaseReadTransaction *_Nonnull transaction) {
1096+ NSUInteger shownCount = [self .viewHandler.mappings numberOfItemsInGroup: self .threadKey];
1097+ NSUInteger totalCount = [[transaction ext: OTRFilteredChatDatabaseViewExtensionName] numberOfItemsInGroup: self .threadKey];
1098+ [self setShowLoadEarlierMessagesHeader: shownCount < totalCount];
1099+ }];
1100+
1101+ if (!reset) {
1102+ [self .collectionView layoutSubviews ];
1103+ self.collectionView .contentOffset = CGPointMake (0 , self.collectionView .contentSize .height - distanceToBottom);
1104+ }
1105+
1106+ self.loadingMessages = NO ;
1107+ };
1108+
1109+ if (reset) {
1110+ doReload ();
1111+ }
1112+ else {
1113+ JSQMessagesCollectionViewFlowLayout *layout = self.collectionView .collectionViewLayout ;
1114+
1115+ dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
1116+ NSMutableArray *objects = [NSMutableArray arrayWithCapacity: kOTRMessagePageSize ];
1117+ for (NSUInteger i = 0 ; i < kOTRMessagePageSize ; i++) {
1118+ // Populating connection's cache in background, so when we call "reloadData" in the UI thread, the objects are returned much faster
1119+ [self .viewHandler object: [NSIndexPath indexPathForRow: i inSection: 0 ]];
1120+ }
1121+ [objects enumerateObjectsWithOptions: NSEnumerationConcurrent
1122+ usingBlock: ^(id <JSQMessageData> obj, NSUInteger idx, BOOL *stop) {
1123+ // The result of the heaviest calculation will remain in the calculator's internal cache, so the
1124+ // collectionView:layout:sizeForItemAtIndexPath: will work faster on the UI thread
1125+ [layout.bubbleSizeCalculator messageBubbleSizeForMessageData: obj
1126+ atIndexPath: nil
1127+ withLayout: layout];
1128+ }];
1129+ dispatch_async (dispatch_get_main_queue (), doReload);
1130+ });
1131+ }
10301132}
10311133
10321134- (BOOL )showDateAtIndexPath : (NSIndexPath *)indexPath
@@ -1323,6 +1425,22 @@ - (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)act
13231425 }
13241426}
13251427
1428+ - (CGSize)collectionView : (UICollectionView *)collectionView layout : (UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath : (NSIndexPath *)indexPath
1429+ {
1430+ id <OTRMessageProtocol, JSQMessageData> message = [self messageAtIndexPath: indexPath];
1431+
1432+ NSNumber *key = @(message.messageHash );
1433+ NSValue *sizeValue = [self .messageSizeCache objectForKey: key];
1434+ if (sizeValue != nil ) {
1435+ return [sizeValue CGSizeValue ];
1436+ }
1437+
1438+ // Although JSQMessagesBubblesSizeCalculator has its own cache, its size is fixed and quite small, so it quickly chokes on scrolling into the past
1439+ CGSize size = [super collectionView: collectionView layout: collectionViewLayout sizeForItemAtIndexPath: indexPath];
1440+ [self .messageSizeCache setObject: [NSValue valueWithCGSize: size] forKey: key];
1441+ return size;
1442+ }
1443+
13261444#pragma - mark UIPopoverPresentationControllerDelegate Methods
13271445
13281446- (void )prepareForPopoverPresentation : (UIPopoverPresentationController *)popoverPresentationController {
@@ -1404,6 +1522,22 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
14041522 [self hideDropdownAnimated: YES completion: nil ];
14051523}
14061524
1525+ - (void )scrollViewDidScroll : (UIScrollView *)scrollView
1526+ {
1527+ if (!self.loadingMessages ) {
1528+ UIEdgeInsets insets = scrollView.contentInset ;
1529+ CGFloat highestOffset = -insets.top ;
1530+ CGFloat lowestOffset = scrollView.contentSize .height - scrollView.frame .size .height + insets.bottom ;
1531+ CGFloat pos = scrollView.contentOffset .y ;
1532+
1533+ if (self.showLoadEarlierMessagesHeader && (pos == highestOffset || pos < 0 && (scrollView.isDecelerating || scrollView.isDragging ))) {
1534+ [self updateRangeOptions: NO ];
1535+ } else if (pos == lowestOffset) {
1536+ [self updateRangeOptions: YES ];
1537+ }
1538+ }
1539+ }
1540+
14071541#pragma mark - UICollectionView DataSource
14081542- (NSInteger )collectionView : (UICollectionView *)collectionView numberOfItemsInSection : (NSInteger )section
14091543{
@@ -1457,32 +1591,59 @@ - (NSString *)senderDisplayName
14571591 return nil ;
14581592 }
14591593
1460- UIImage *avatarImage = nil ;
14611594 NSError *messageError = [message messageError ];
14621595 if ((messageError && !messageError.isAutomaticDownloadError ) ||
14631596 ![self isMessageTrusted: message]) {
1464- avatarImage = [OTRImages circleWarningWithColor: [OTRColors warnColor ] ];
1597+ return [ self warningAvatarImage ];
14651598 }
1466- else if ([message isMessageIncoming ]) {
1467- __block OTRBuddy *buddy = nil ;
1468- [self .readOnlyDatabaseConnection readWithBlock: ^(YapDatabaseReadTransaction * _Nonnull transaction) {
1469- buddy = [self buddyWithTransaction: transaction];
1599+ if ([message isMessageIncoming ]) {
1600+ return [self buddyAvatarImage ];
1601+ }
1602+
1603+ return [self accountAvatarImage ];
1604+ }
1605+
1606+ - (JSQMessagesAvatarImage *)createAvatarImage : (UIImage *(^)(YapDatabaseReadTransaction *))getImage
1607+ {
1608+ __block UIImage *avatarImage;
1609+ [self .readOnlyDatabaseConnection readWithBlock: ^(YapDatabaseReadTransaction *_Nonnull transaction) {
1610+ avatarImage = getImage (transaction);
1611+ }];
1612+ if (avatarImage != nil ) {
1613+ NSUInteger diameter = (NSUInteger ) MIN (avatarImage.size .width , avatarImage.size .height );
1614+ return [JSQMessagesAvatarImageFactory avatarImageWithImage: avatarImage diameter: diameter];
1615+ }
1616+ return nil ;
1617+ }
1618+
1619+ - (JSQMessagesAvatarImage *)warningAvatarImage
1620+ {
1621+ if (_warningAvatarImage == nil ) {
1622+ _warningAvatarImage = [self createAvatarImage: ^(YapDatabaseReadTransaction *transaction) {
1623+ return [OTRImages circleWarningWithColor: [OTRColors warnColor ]];
14701624 }];
1471- avatarImage = [buddy avatarImage ];
14721625 }
1473- else {
1474- __block OTRAccount *account = nil ;
1475- [self .readOnlyDatabaseConnection readWithBlock: ^(YapDatabaseReadTransaction * _Nonnull transaction) {
1476- account = [self accountWithTransaction: transaction];
1626+ return _warningAvatarImage;
1627+ }
1628+
1629+ - (JSQMessagesAvatarImage *)accountAvatarImage
1630+ {
1631+ if (_accountAvatarImage == nil ) {
1632+ _accountAvatarImage = [self createAvatarImage: ^(YapDatabaseReadTransaction *transaction) {
1633+ return [[self accountWithTransaction: transaction] avatarImage ];
14771634 }];
1478- avatarImage = [account avatarImage ];
14791635 }
1480-
1481- if (avatarImage) {
1482- NSUInteger diameter = MIN (avatarImage.size .width , avatarImage.size .height );
1483- return [JSQMessagesAvatarImageFactory avatarImageWithImage: avatarImage diameter: diameter];
1636+ return _accountAvatarImage;
1637+ }
1638+
1639+ - (JSQMessagesAvatarImage *)buddyAvatarImage
1640+ {
1641+ if (_buddyAvatarImage == nil ) {
1642+ _buddyAvatarImage = [self createAvatarImage: ^(YapDatabaseReadTransaction *transaction) {
1643+ return [[self buddyWithTransaction: transaction] avatarImage ];
1644+ }];
14841645 }
1485- return nil ;
1646+ return _buddyAvatarImage ;
14861647}
14871648
14881649// //// Optional //////
@@ -1745,6 +1906,7 @@ - (void)didSetupMappings:(OTRYapViewHandler *)handler
17451906{
17461907 // The databse view is setup now so refresh from there
17471908 [self updateViewWithKey: self .threadKey collection: self .threadCollection];
1909+ [self updateRangeOptions: YES ];
17481910 [self .collectionView reloadData ];
17491911}
17501912
0 commit comments