diff --git a/Signal/src/UIView+OWS.m b/Signal/src/UIView+OWS.m index 9531b793c..64143fc6e 100644 --- a/Signal/src/UIView+OWS.m +++ b/Signal/src/UIView+OWS.m @@ -167,11 +167,10 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) { OWSAssert(self.superview); - self.frame = CGRectMake( - round(self.superview.bounds.origin.x + (self.superview.bounds.size.width - self.frame.size.width) * 0.5f), - round(self.superview.bounds.origin.y + (self.superview.bounds.size.height - self.frame.size.height) * 0.5f), - self.frame.size.width, - self.frame.size.height); + self.frame = CGRectMake(round(self.superview.left + (self.superview.width - self.width) * 0.5f), + round(self.superview.top + (self.superview.height - self.height) * 0.5f), + self.width, + self.height); } #pragma mark - Debugging diff --git a/Signal/src/ViewControllers/DebugUITableViewController.m b/Signal/src/ViewControllers/DebugUITableViewController.m index b890f92d8..c284b69cf 100644 --- a/Signal/src/ViewControllers/DebugUITableViewController.m +++ b/Signal/src/ViewControllers/DebugUITableViewController.m @@ -1302,9 +1302,13 @@ NS_ASSUME_NONNULL_BEGIN if (counter < 1) { return; } - [ThreadUtil sendMessageWithText:[@(counter) description] - inThread:thread - messageSender:messageSender]; + [ThreadUtil + sendMessageWithText:[[@(counter) description] + stringByAppendingString:@" Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + @"Suspendisse rutrum, nulla vitae pretium hendrerit, tellus " + @"turpis pharetra libero, vitae sodales tortor ante vel sem."] + inThread:thread + messageSender:messageSender]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [self sendTextMessage:counter - 1 thread:thread]; diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index f2c4d846b..f3a310477 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -42,6 +42,7 @@ #import "TSIncomingMessage.h" #import "TSInfoMessage.h" #import "TSInvalidIdentityKeyErrorMessage.h" +#import "TSUnreadIndicatorInteraction.h" #import "ThreadUtil.h" #import "UIFont+OWS.h" #import "UIUtil.h" @@ -98,6 +99,8 @@ typedef enum : NSUInteger { - (void)didPasteAttachment:(SignalAttachment * _Nullable)attachment; +- (void)textViewDidChangeSize; + @end #pragma mark - @@ -146,6 +149,30 @@ typedef enum : NSUInteger { [super paste:sender]; } +- (void)setFrame:(CGRect)frame +{ + BOOL isNonEmpty = (self.width > 0.f && self.height > 0.f); + BOOL didChangeSize = !CGSizeEqualToSize(frame.size, self.frame.size); + + [super setFrame:frame]; + + if (didChangeSize && isNonEmpty) { + [self.textViewPasteDelegate textViewDidChangeSize]; + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL isNonEmpty = (self.width > 0.f && self.height > 0.f); + BOOL didChangeSize = !CGSizeEqualToSize(bounds.size, self.bounds.size); + + [super setBounds:bounds]; + + if (didChangeSize && isNonEmpty) { + [self.textViewPasteDelegate textViewDidChangeSize]; + } +} + @end #pragma mark - @@ -611,6 +638,7 @@ typedef enum : NSUInteger { @property (nonatomic) NSCache *messageAdapterCache; @property (nonatomic) BOOL userHasScrolled; @property (nonatomic) NSDate *lastMessageSentDate; +@property (nonatomic) NSTimer *scrollLaterTimer; @end @@ -782,6 +810,7 @@ typedef enum : NSUInteger { self.senderId = ME_MESSAGE_IDENTIFIER; self.senderDisplayName = ME_MESSAGE_IDENTIFIER; + self.automaticallyScrollsToMostRecentMessage = NO; [self initializeToolbars]; @@ -935,14 +964,6 @@ typedef enum : NSUInteger { [self setBarButtonItemsForDisappearingMessagesConfiguration:configuration]; [self setNavigationTitle]; - NSInteger numberOfMessages = (NSInteger)[self.messageMappings numberOfItemsInGroup:self.thread.uniqueId]; - if (numberOfMessages > 0) { - NSIndexPath *lastCellIndexPath = [NSIndexPath indexPathForRow:numberOfMessages - 1 inSection:0]; - [self.collectionView scrollToItemAtIndexPath:lastCellIndexPath - atScrollPosition:UICollectionViewScrollPositionBottom - animated:NO]; - } - // Other views might change these custom menu items, so we // need to set them every time we enter this view. SEL saveSelector = NSSelectorFromString(@"save:"); @@ -961,6 +982,50 @@ typedef enum : NSUInteger { [self resetContentAndLayout]; [((OWSMessagesToolbarContentView *)self.inputToolbar.contentView)ensureSubviews]; + + [self.collectionView.collectionViewLayout + invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; + + [self.scrollLaterTimer invalidate]; + // We want to scroll to the bottom _after_ the layout has been updated. + self.scrollLaterTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.001f + target:self + selector:@selector(scrollToDefaultPosition) + userInfo:nil + repeats:NO]; +} + +- (void)clearUnreadMessagesIndicator +{ + [ThreadUtil clearUnreadMessagesIndicator:self.thread storageManager:self.storageManager]; +} + +- (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator +{ + int numberOfMessages = (int)[self.messageMappings numberOfItemsInGroup:self.thread.uniqueId]; + for (int i = 0; i < numberOfMessages; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0]; + id message = [self messageAtIndexPath:indexPath]; + if (message.messageType == TSUnreadIndicatorAdapter) { + return indexPath; + } + } + return nil; +} + +- (void)scrollToDefaultPosition +{ + [self.scrollLaterTimer invalidate]; + self.scrollLaterTimer = nil; + + NSIndexPath *_Nullable indexPath = [self indexPathOfUnreadMessagesIndicator]; + if (indexPath) { + [self.collectionView scrollToItemAtIndexPath:indexPath + atScrollPosition:UICollectionViewScrollPositionTop + animated:NO]; + } else { + [self scrollToBottomAnimated:NO]; + } } - (void)resetContentAndLayout @@ -1556,6 +1621,8 @@ typedef enum : NSUInteger { [ThreadUtil sendMessageWithText:text inThread:self.thread messageSender:self.messageSender]; } self.lastMessageSentDate = [NSDate new]; + [self clearUnreadMessagesIndicator]; + if (updateKeyboardState) { [self toggleDefaultKeyboard]; @@ -2314,6 +2381,8 @@ typedef enum : NSUInteger { self.page++; } + [self.scrollLaterTimer invalidate]; + self.scrollLaterTimer = nil; NSInteger item = (NSInteger)[self scrollToItem]; [self updateRangeOptionsForPage:self.page]; @@ -2366,6 +2435,8 @@ typedef enum : NSUInteger { invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; [self.collectionView reloadData]; + [self.scrollLaterTimer invalidate]; + self.scrollLaterTimer = nil; [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:offset inSection:0] atScrollPosition:UICollectionViewScrollPositionTop animated:NO]; @@ -2899,6 +2970,7 @@ typedef enum : NSUInteger { [attachment mimeType]); [ThreadUtil sendMessageWithAttachment:attachment inThread:self.thread messageSender:self.messageSender]; self.lastMessageSentDate = [NSDate new]; + [self clearUnreadMessagesIndicator]; } - (NSURL *)videoTempFolder { @@ -3021,12 +3093,8 @@ typedef enum : NSUInteger { if ([sectionChanges count] == 0 & [messageRowChanges count] == 0) { return; } - - const CGFloat kIsAtBottomTolerancePts = 5; - BOOL wasAtBottom = (self.collectionView.contentOffset.y + - self.collectionView.bounds.size.height + - kIsAtBottomTolerancePts >= - self.collectionView.contentSize.height); + + BOOL wasAtBottom = [self isScrolledToBottom]; // We want sending messages to feel snappy. So, if the only // update is a new outgoing message AND we're already scrolled to // the bottom of the conversation, skip the scroll animation. @@ -3052,11 +3120,11 @@ typedef enum : NSUInteger { } case YapDatabaseViewChangeInsert: { [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]]; - scrollToBottom = YES; TSInteraction *interaction = [self interactionAtIndexPath:rowChange.newIndexPath]; - if (![interaction isKindOfClass:[TSOutgoingMessage class]]) { - shouldAnimateScrollToBottom = YES; + if ([interaction isKindOfClass:[TSOutgoingMessage class]]) { + scrollToBottom = YES; + shouldAnimateScrollToBottom = NO; } break; } @@ -3083,11 +3151,20 @@ typedef enum : NSUInteger { [self.collectionView reloadData]; } if (scrollToBottom) { + [self.scrollLaterTimer invalidate]; + self.scrollLaterTimer = nil; [self scrollToBottomAnimated:shouldAnimateScrollToBottom]; } }]; } +- (BOOL)isScrolledToBottom +{ + const CGFloat kIsAtBottomTolerancePts = 5; + return (self.collectionView.contentOffset.y + self.collectionView.bounds.size.height + kIsAtBottomTolerancePts + >= self.collectionView.contentSize.height); +} + #pragma mark - UICollectionView DataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { @@ -3608,6 +3685,32 @@ typedef enum : NSUInteger { completion:nil]; } +- (void)textViewDidChangeSize +{ + OWSAssert([NSThread isMainThread]); + + BOOL wasAtBottom = [self isScrolledToBottom]; + if (wasAtBottom) { + [self.scrollLaterTimer invalidate]; + // We want to scroll to the bottom _after_ the layout has been updated. + self.scrollLaterTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.001f + target:self + selector:@selector(scrollToBottomImmediately) + userInfo:nil + repeats:NO]; + } +} + +- (void)scrollToBottomImmediately +{ + OWSAssert([NSThread isMainThread]); + + [self.scrollLaterTimer invalidate]; + self.scrollLaterTimer = nil; + + [self scrollToBottomAnimated:NO]; +} + #pragma mark - OWSMessagesToolbarContentDelegate - (void)voiceMemoGestureDidStart diff --git a/Signal/src/util/ThreadUtil.h b/Signal/src/util/ThreadUtil.h index 7af2cbef1..cb1fcceaa 100644 --- a/Signal/src/util/ThreadUtil.h +++ b/Signal/src/util/ThreadUtil.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN blockingManager:(OWSBlockingManager *)blockingManager; + (void)createUnreadMessagesIndicatorIfNecessary:(TSThread *)thread storageManager:(TSStorageManager *)storageManager; ++ (void)clearUnreadMessagesIndicator:(TSThread *)thread storageManager:(TSStorageManager *)storageManager; @end diff --git a/Signal/src/util/ThreadUtil.m b/Signal/src/util/ThreadUtil.m index 744382c30..09d246b8e 100644 --- a/Signal/src/util/ThreadUtil.m +++ b/Signal/src/util/ThreadUtil.m @@ -230,6 +230,30 @@ NS_ASSUME_NONNULL_BEGIN }]; } ++ (void)clearUnreadMessagesIndicator:(TSThread *)thread storageManager:(TSStorageManager *)storageManager +{ + OWSAssert(thread); + OWSAssert(storageManager); + + [storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + + NSMutableArray *indicators = [NSMutableArray new]; + [[transaction ext:TSMessageDatabaseViewExtensionName] + enumerateRowsInGroup:thread.uniqueId + usingBlock:^( + NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { + + if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { + [indicators addObject:object]; + } + }]; + + for (TSUnreadIndicatorInteraction *indicator in indicators) { + [indicator removeWithTransaction:transaction]; + } + }]; +} + #pragma mark - Logging + (NSString *)tag