diff --git a/Signal/src/UIView+OWS.h b/Signal/src/UIView+OWS.h index d8287e78b..1c1e89aea 100644 --- a/Signal/src/UIView+OWS.h +++ b/Signal/src/UIView+OWS.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import @@ -83,6 +83,9 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); - (NSLayoutConstraint *)autoPinLeadingToSuperviewWithMargin:(CGFloat)margin; - (NSLayoutConstraint *)autoPinTrailingToSuperview; - (NSLayoutConstraint *)autoPinTrailingToSuperviewWithMargin:(CGFloat)margin; +- (NSLayoutConstraint *)autoPinTopToSuperviewWithMargin:(CGFloat)margin; +- (NSLayoutConstraint *)autoPinBottomToSuperviewWithMargin:(CGFloat)margin; + - (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view; - (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view margin:(CGFloat)margin; - (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view; diff --git a/Signal/src/UIView+OWS.m b/Signal/src/UIView+OWS.m index cc0943649..0b5724d7e 100644 --- a/Signal/src/UIView+OWS.m +++ b/Signal/src/UIView+OWS.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "UIView+OWS.h" @@ -299,6 +299,32 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) } } +- (NSLayoutConstraint *)autoPinBottomToSuperviewWithMargin:(CGFloat)margin +{ + if (@available(iOS 9.0, *)) { + NSLayoutConstraint *constraint = + [self.bottomAnchor constraintEqualToAnchor:self.superview.layoutMarginsGuide.bottomAnchor + constant:-margin]; + constraint.active = YES; + return constraint; + } else { + return [self autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:margin]; + } +} + +- (NSLayoutConstraint *)autoPinTopToSuperviewWithMargin:(CGFloat)margin +{ + if (@available(iOS 9.0, *)) { + NSLayoutConstraint *constraint = + [self.topAnchor constraintEqualToAnchor:self.superview.layoutMarginsGuide.topAnchor + constant:margin]; + constraint.active = YES; + return constraint; + } else { + return [self autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:margin]; + } +} + - (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view { OWSAssert(view); diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index c1a0798c7..802d31d02 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -26,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)didApproveAttachment:(SignalAttachment *)attachment; +- (void)toolbarHeightDidChange:(CGFloat)newHeight; + @end #pragma mark - diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 0753c686d..461c288c5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "ConversationInputToolbar.h" @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext; - +static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; @interface ConversationInputToolbar () @property (nonatomic, readonly) UIView *contentView; @@ -30,6 +30,8 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex @property (nonatomic) NSArray *contentContraints; @property (nonatomic) NSValue *lastTextContentSize; +@property (nonatomic) CGFloat toolbarHeight; +@property (nonatomic) CGFloat textViewHeight; #pragma mark - Voice Memo Recording UI @@ -68,18 +70,34 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex [self removeKVOObservers]; } +- (CGSize)intrinsicContentSize +{ + CGSize newSize = CGSizeMake(self.bounds.size.width, self.toolbarHeight + ConversationInputToolbarBorderViewHeight); + return newSize; +} + +- (void)setToolbarHeight:(CGFloat)toolbarHeight +{ + if (toolbarHeight == _toolbarHeight) { + return; + } + + _toolbarHeight = toolbarHeight; +} + - (void)createContents { self.layoutMargins = UIEdgeInsetsZero; self.backgroundColor = [UIColor ows_inputToolbarBackgroundColor]; + self.autoresizingMask = UIViewAutoresizingFlexibleHeight; UIView *borderView = [UIView new]; borderView.backgroundColor = [UIColor colorWithWhite:238 / 255.f alpha:1.f]; [self addSubview:borderView]; [borderView autoPinWidthToSuperview]; [borderView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [borderView autoSetDimension:ALDimensionHeight toSize:0.5f]; + [borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight]; _contentView = [UIView containerView]; [self addSubview:self.contentView]; @@ -223,12 +241,16 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight + self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom + self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom); - const CGFloat kMaxTextViewHeight = 100.f; + // Exactly 4 lines of text with default sizing. + const CGFloat kMaxTextViewHeight = 98.f; const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom); const CGFloat textViewHeight = ceil(Clamp(textViewDesiredHeight, kMinTextViewHeight, kMaxTextViewHeight)); const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2; + self.textViewHeight = textViewHeight; + self.toolbarHeight = textViewHeight + textViewVInset * 2; + if (self.attachmentToApprove) { OWSAssert(self.attachmentView); @@ -247,14 +269,14 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex self.contentContraints = @[ [self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset], - [self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset], + [self.attachmentView autoPinBottomToSuperviewWithMargin:textViewVInset], [self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:contentHInset], [self.attachmentView autoSetDimension:ALDimensionHeight toSize:150.f], [self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.attachmentView], [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight], [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop], - [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom], + [self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0], [rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight], [rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing], @@ -316,7 +338,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex self.contentContraints = @[ [self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeLeft], [self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop], - [self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom], + [self.leftButtonWrapper autoPinBottomToSuperviewWithMargin:0], [leftButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight], [leftButton autoPinLeadingToSuperviewWithMargin:contentHInset], @@ -325,18 +347,18 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex [self.inputTextView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.leftButtonWrapper], [self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset], - [self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset], + [self.inputTextView autoPinBottomToSuperviewWithMargin:textViewVInset], [self.inputTextView autoSetDimension:ALDimensionHeight toSize:textViewHeight], [self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.inputTextView], [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight], [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop], - [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom], + [self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0], [rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight], [rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing], [rightButton autoPinTrailingToSuperviewWithMargin:contentHInset], - [rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom], + [rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom] ]; // Layout immediately, unless the input toolbar hasn't even been laid out yet. @@ -709,6 +731,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex if (!lastTextContentSize || fabs(lastTextContentSize.CGSizeValue.width - textContentSize.width) > 0.1f || fabs(lastTextContentSize.CGSizeValue.height - textContentSize.height) > 0.1f) { [self ensureContentConstraints]; + [self invalidateIntrinsicContentSize]; } } } @@ -811,8 +834,6 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex - (void)viewWillDisappear:(BOOL)animated { [self.attachmentView viewWillDisappear:animated]; - - [self endEditingTextMessage]; } - (nullable NSString *)textInputPrimaryLanguage diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 99606fc8e..0ada12975 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "ConversationViewController.h" @@ -64,7 +64,6 @@ #import #import #import -#import #import #import #import @@ -215,17 +214,18 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic, readonly) BOOL isGroupConversation; @property (nonatomic) BOOL isUserScrolling; +@property (nonatomic) NSLayoutConstraint *scrollDownButtonButtomConstraint; + @property (nonatomic) ConversationScrollButton *scrollDownButton; #ifdef DEBUG @property (nonatomic) ConversationScrollButton *scrollUpButton; #endif +@property (nonatomic) BOOL isViewCompletelyAppeared; @property (nonatomic) BOOL isViewVisible; @property (nonatomic) BOOL isAppInBackground; @property (nonatomic) BOOL shouldObserveDBModifications; @property (nonatomic) BOOL viewHasEverAppeared; -@property (nonatomic) BOOL wasScrolledToBottomBeforeKeyboardShow; -@property (nonatomic) BOOL wasScrolledToBottomBeforeLayoutChange; @property (nonatomic) BOOL hasUnreadMessages; @end @@ -319,6 +319,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { selector:@selector(signalAccountsDidChange:) name:OWSContactsManagerSignalAccountsDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillChangeFrame:) + name:UIKeyboardWillChangeFrameNotification + object:nil]; } - (void)signalAccountsDidChange:(NSNotification *)notification @@ -452,13 +456,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { { if (_peek) { self.inputToolbar.hidden = YES; - [self.inputToolbar endEditing:TRUE]; + [self dismissKeyBoard]; return; } if (self.userLeftGroup) { self.inputToolbar.hidden = YES; // user has requested they leave the group. further sends disallowed - [self.inputToolbar endEditing:TRUE]; + [self dismissKeyBoard]; } else { self.inputToolbar.hidden = NO; } @@ -501,6 +505,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { self.collectionView.dataSource = self; self.collectionView.showsVerticalScrollIndicator = YES; self.collectionView.showsHorizontalScrollIndicator = NO; + self.collectionView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; self.collectionView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:self.collectionView]; [self.collectionView autoPinWidthToSuperview]; @@ -517,10 +522,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { _inputToolbar = [ConversationInputToolbar new]; self.inputToolbar.inputToolbarDelegate = self; self.inputToolbar.inputTextViewDelegate = self; - [self.view addSubview:self.inputToolbar]; - [self.inputToolbar autoPinWidthToSuperview]; - [self.inputToolbar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.collectionView]; - [self autoPinViewToBottomGuideOrKeyboard:self.inputToolbar]; + [self.collectionView autoPinToBottomLayoutGuideOfViewController:self withInset:0]; self.loadMoreHeader = [UILabel new]; self.loadMoreHeader.text = NSLocalizedString(@"CONVERSATION_VIEW_LOADING_MORE_MESSAGES", @@ -534,6 +536,16 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self.loadMoreHeader autoSetDimension:ALDimensionHeight toSize:kLoadMoreHeaderHeight]; } +- (BOOL)canBecomeFirstResponder +{ + return YES; +} + +- (nullable UIView *)inputAccessoryView +{ + return self.inputToolbar; +} + - (void)registerCellClasses { [self.collectionView registerClass:[OWSSystemMessageCell class] @@ -883,6 +895,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { }]; [actionSheetController addAction:dismissAction]; + [self dismissKeyBoard]; [self presentViewController:actionSheetController animated:YES completion:nil]; } } @@ -999,6 +1012,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { _callOnOpen = NO; } + self.isViewCompletelyAppeared = YES; self.viewHasEverAppeared = YES; } @@ -1012,6 +1026,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [super viewWillDisappear:animated]; + self.isViewCompletelyAppeared = NO; [self.inputToolbar viewWillDisappear:animated]; } @@ -1029,7 +1044,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self markVisibleMessagesAsRead]; [self cancelVoiceMemo]; [self.cellMediaCache removeAllObjects]; - [self.inputToolbar endEditingTextMessage]; self.isUserScrolling = NO; } @@ -1388,11 +1402,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { return; } + // FIXME inputAccessoryView - if using numeric keyboard, switch back to alpha after + // sending. // The JSQ event listeners cause a bounce animation, so we temporarily disable them. - [self setShouldIgnoreKeyboardChanges:YES]; - [self dismissKeyBoard]; - [self popKeyBoard]; - [self setShouldIgnoreKeyboardChanges:NO]; + // [self setShouldIgnoreKeyboardChanges:YES]; + // [self dismissKeyBoard]; + // [self popKeyBoard]; + // [self setShouldIgnoreKeyboardChanges:NO]; } #pragma mark - Dynamic Text @@ -1635,6 +1651,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [actionSheetController addAction:resendMessageAction]; + [self dismissKeyBoard]; [self presentViewController:actionSheetController animated:YES completion:nil]; } @@ -1669,6 +1686,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [actionSheetController addAction:resendMessageAction]; + [self dismissKeyBoard]; [self presentViewController:actionSheetController animated:YES completion:nil]; } @@ -1796,6 +1814,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { }]; [alertController addAction:resetSessionAction]; + [self dismissKeyBoard]; [self presentViewController:alertController animated:YES completion:nil]; } @@ -1836,6 +1855,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { }]; [actionSheetController addAction:acceptSafetyNumberAction]; + [self dismissKeyBoard]; [self presentViewController:actionSheetController animated:YES completion:nil]; } @@ -1865,9 +1885,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [alertController addAction:callAction]; [alertController addAction:[OWSAlerts cancelAction]]; - [[UIApplication sharedApplication].frontmostViewController presentViewController:alertController - animated:YES - completion:nil]; + [self dismissKeyBoard]; + [self presentViewController:alertController animated:YES completion:nil]; } #pragma mark - ConversationViewCellDelegate @@ -1916,6 +1935,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { }]; [actionSheetController addAction:blockAction]; + [self dismissKeyBoard]; [self presentViewController:actionSheetController animated:YES completion:nil]; } @@ -2018,6 +2038,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { // MPMoviePlayerController will animate a crop of its // contents rather than scaling them. _videoPlayer.view.frame = self.view.bounds; + + // FIXME inputAccessoryView - we lose and regain first responder here, causing keyboard to appear above video + // Approaches: + // - put the video player in a separate VC (like the full image view controller) + // - some kind of "showing video" flag to supress first responder. [_videoPlayer setFullscreen:YES animated:NO]; } @@ -2235,7 +2260,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self.view addSubview:self.scrollDownButton]; [self.scrollDownButton autoSetDimension:ALDimensionWidth toSize:ConversationScrollButton.buttonSize]; [self.scrollDownButton autoSetDimension:ALDimensionHeight toSize:ConversationScrollButton.buttonSize]; - [self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:self.inputToolbar]; + + self.scrollDownButtonButtomConstraint = + [self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.collectionView]; [self.scrollDownButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; #ifdef DEBUG @@ -2351,6 +2378,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [[UIDocumentMenuViewController alloc] initWithDocumentTypes:documentTypes inMode:pickerMode]; menuController.delegate = self; + [self dismissKeyBoard]; [self presentViewController:menuController animated:YES completion:nil]; } @@ -2362,6 +2390,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [[GifPickerViewController alloc] initWithThread:self.thread messageSender:self.messageSender]; view.delegate = self; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:view]; + + [self dismissKeyBoard]; [self presentViewController:navigationController animated:YES completion:nil]; } @@ -2401,6 +2431,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { // post iOS11, document picker has no blue header. [UIUtil applyDefaultSystemAppearence]; } + + [self dismissKeyBoard]; [self presentViewController:documentPicker animated:YES completion:nil]; } @@ -2497,8 +2529,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ]; picker.allowsEditing = NO; picker.delegate = self; - + dispatch_async(dispatch_get_main_queue(), ^{ + [self dismissKeyBoard]; [self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; }); }]; @@ -2518,6 +2551,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { picker.delegate = self; picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ]; + [self dismissKeyBoard]; [self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]]; } @@ -3081,7 +3115,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { const CGFloat kIsAtBottomTolerancePts = 5; // Note the usage of MAX() to handle the case where there isn't enough // content to fill the collection view at its current size. - CGFloat contentOffsetYBottom = MAX(0.f, contentHeight - self.collectionView.bounds.size.height); + CGFloat contentOffsetYBottom + = MAX(0.f, contentHeight + self.collectionView.contentInset.bottom - self.collectionView.bounds.size.height); BOOL isScrolledToBottom = (self.collectionView.contentOffset.y > contentOffsetYBottom - kIsAtBottomTolerancePts); return isScrolledToBottom; @@ -3274,6 +3309,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)attachmentButtonPressed { + [self dismissKeyBoard]; __weak ConversationViewController *weakSelf = self; if ([self isBlockedContactConversation]) { @@ -3349,6 +3385,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [gifAction setValue:gifImage forKey:@"image"]; [actionSheetController addAction:gifAction]; + [self dismissKeyBoard]; [self presentViewController:actionSheetController animated:true completion:nil]; } @@ -3658,6 +3695,100 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { }); } +- (void)keyboardWillChangeFrame:(NSNotification *)notification +{ + // `willChange` is the correct keyboard notifiation to observe when adjusting contentInset + // in lockstep with the keyboard presentation animation. `didChange` results in the contentInset + // not adjusting until after the keyboard is fully up. + DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + [self handleKeyboardNotification:notification]; +} + +- (void)handleKeyboardNotification:(NSNotification *)notification +{ + AssertIsOnMainThread(); + + NSDictionary *userInfo = [notification userInfo]; + + NSValue *_Nullable keyboardBeginFrameValue = userInfo[UIKeyboardFrameBeginUserInfoKey]; + if (!keyboardBeginFrameValue) { + OWSFail(@"%@ Missing keyboard begin frame", self.logTag); + return; + } + + NSValue *_Nullable keyboardEndFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey]; + if (!keyboardEndFrameValue) { + OWSFail(@"%@ Missing keyboard end frame", self.logTag); + return; + } + CGRect keyboardEndFrame = [keyboardEndFrameValue CGRectValue]; + + // DDLogVerbose(@"%@ keyboard change. Old Frame: %@, New Frame: %@", + // self.logTag, + // NSStringFromCGRect(keyboardBeginFrame), + // NSStringFromCGRect(keyboardEndFrame)); + + UIEdgeInsets oldInsets = self.collectionView.contentInset; + UIEdgeInsets newInsets = oldInsets; + + // bottomLayoutGuide accounts for extra offset needed on iPhoneX + newInsets.bottom = keyboardEndFrame.size.height - self.bottomLayoutGuide.length; + + BOOL wasScrolledToBottom = [self isScrolledToBottom]; + + void (^adjustInsets)(void) = ^(void) { + self.collectionView.contentInset = newInsets; + self.collectionView.scrollIndicatorInsets = newInsets; + + // Note there is a bug in iOS11.2 which where switching to the emoji keyboard + // does not fire a UIKeyboardFrameWillChange notification. In that case, the scroll + // down button gets mostly obscured by the keyboard. + // RADAR: #36297652 + self.scrollDownButtonButtomConstraint.constant = -1 * newInsets.bottom; + [self.scrollDownButton setNeedsLayout]; + [self.scrollDownButton layoutIfNeeded]; + // HACK: I've made the assumption that we are already in the context of an animation, in which case the + // above should be sufficient to smoothly move the scrollDown button in step with the keyboard presentation + // animation. Yet, setting the constraint doesn't animate the movement of the button - it "jumps" to it's final + // position. So here we manually lay out the scroll down button frame (seemingly redundantly), which allows it + // to be smoothly animated. + CGRect newButtonFrame = self.scrollDownButton.frame; + newButtonFrame.origin.y + = self.scrollDownButton.superview.height - (newInsets.bottom + self.scrollDownButton.height); + self.scrollDownButton.frame = newButtonFrame; + + // Adjust content offset to prevent the presented keyboard from obscuring content. + if (wasScrolledToBottom) { + // If we were scrolled to the bottom, don't do any fancy math. Just stay at the bottom. + [self scrollToBottomAnimated:NO]; + } else { + // If we were scrolled away from the bottom, shift the content in lockstep with the + // keyboard, up to the limits of the content bounds. + CGFloat insetChange = newInsets.bottom - oldInsets.bottom; + CGFloat oldYOffset = self.collectionView.contentOffset.y; + CGFloat newYOffset = Clamp(oldYOffset + insetChange, 0, self.safeContentHeight); + CGPoint newOffset = CGPointMake(0, newYOffset); + + // If the user is dismissing the keyboard via interactive scrolling, any additional conset offset feels + // redundant, so we only adjust content offset when *presenting* the keyboard. + if (insetChange > 0 && newYOffset > keyboardEndFrame.origin.y) { + [self.collectionView setContentOffset:newOffset animated:NO]; + } + } + }; + + if (self.isViewCompletelyAppeared) { + adjustInsets(); + } else { + // Even though we are scrolling without explicitly animating, the notification seems to occur within the context + // of a system animation, which is desirable when the view is visible, because the user sees the content rise + // in sync with the keyboard. However, when the view hasn't yet been presented, the animation conflicts and the + // result is that initial load causes the collection cells to visably "animate" to their final position once the + // view appears. + [UIView performWithoutAnimation:adjustInsets]; + } +} + - (void)didApproveAttachment:(SignalAttachment *)attachment { OWSAssert(attachment); @@ -3690,13 +3821,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { return [self.collectionView.collectionViewLayout collectionViewContentSize].height; } -- (void)scrollToBottomImmediately -{ - OWSAssert([NSThread isMainThread]); - - [self scrollToBottomAnimated:NO]; -} - - (void)scrollToBottomAnimated:(BOOL)animated { OWSAssert([NSThread isMainThread]); @@ -3706,9 +3830,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { } CGFloat contentHeight = self.safeContentHeight; - CGFloat dstY = MAX(0, contentHeight - self.collectionView.height); - [self.collectionView setContentOffset:CGPointMake(0, dstY) animated:animated]; + CGFloat dstY + = MAX(0, contentHeight + self.collectionView.contentInset.bottom - self.collectionView.bounds.size.height); + + [self.collectionView setContentOffset:CGPointMake(0, dstY) animated:NO]; [self didScrollToBottom]; } @@ -3718,22 +3844,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { { [self updateLastVisibleTimestamp]; [self autoLoadMoreIfNecessary]; - - if (self.isUserScrolling && [self isScrolledAwayFromBottom]) { - [self.inputToolbar endEditingTextMessage]; - } -} - -// See the comments on isScrolledToBottom. -- (BOOL)isScrolledAwayFromBottom -{ - CGFloat contentHeight = self.safeContentHeight; - // Note the usage of MAX() to handle the case where there isn't enough - // content to fill the collection view at its current size. - CGFloat contentOffsetYBottom = MAX(0.f, contentHeight - self.collectionView.bounds.size.height); - const CGFloat kThreshold = 250; - BOOL isScrolledAwayFromBottom = (self.collectionView.contentOffset.y < contentOffsetYBottom - kThreshold); - return isScrolledAwayFromBottom; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView @@ -4034,8 +4144,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)collectionViewWillChangeLayout { OWSAssert([NSThread isMainThread]); - - self.wasScrolledToBottomBeforeLayoutChange = [self isScrolledToBottom]; } - (void)collectionViewDidChangeLayout @@ -4043,15 +4151,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { OWSAssert([NSThread isMainThread]); [self updateLastVisibleTimestamp]; - - // JSQMessageView has glitchy behavior. When presenting/dismissing view - // controllers, the size of the input toolbar and/or collection view can - // repeatedly change, leaving scroll state in an invalid state. The - // simplest fix that covers most cases is to ensure that we remain - // "scrolled to bottom" across these changes. - if (self.wasScrolledToBottomBeforeLayoutChange) { - [self scrollToBottomImmediately]; - } } #pragma mark - View Items diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m index 76398a747..224048f80 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m @@ -68,6 +68,11 @@ NS_ASSUME_NONNULL_BEGIN [self clearState]; return; } + + if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) { + [self.collectionView layoutIfNeeded]; + } + if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) { OWSFail( @"%@ Collection view has invalid size: %@", self.logTag, NSStringFromCGRect(self.collectionView.bounds)); diff --git a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m b/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m index 23997a5b8..264daab7d 100644 --- a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m +++ b/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m @@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)auditAndCleanupAsync:(void (^_Nullable)())completion { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [OWSOrphanedDataCleaner auditAndCleanup:YES completion:completion]; +// [OWSOrphanedDataCleaner auditAndCleanup:YES completion:completion]; }); }