From 81268012e5c7d779877a7263eb8cac7df1fa20e6 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 2 Jan 2018 15:55:40 -0600 Subject: [PATCH 01/23] Better keyboard management. - fixes problems on iOS11.2 where emoji keyboard sometimes obscures text input. - better animation for interactive pan gesture when viewing message details - more intuitive swipe-to-dismiss keyboard in conversation view - converge on one mnethod for dismissing keyboard in conversation view - [ ] Pop keyboard, then hit attachment, dismisses keyboard, which is fine, but the content should immediately scroll down with the keyboard, instead it stays up, and scrolls down only once the attachment action sheet has been dismissed. // FREEBIE --- Signal/src/UIView+OWS.h | 5 +- Signal/src/UIView+OWS.m | 28 ++- .../ConversationInputToolbar.h | 2 + .../ConversationInputToolbar.m | 45 +++- .../ConversationViewController.m | 213 +++++++++++++----- .../ConversationView/ConversationViewLayout.m | 5 + .../src/Storage/OWSOrphanedDataCleaner.m | 2 +- 7 files changed, 228 insertions(+), 72 deletions(-) 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]; }); } From 918e3f7dfe77745f0ec4810ccd46666c50ab58ef Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 5 Jan 2018 09:10:13 -0600 Subject: [PATCH 02/23] Videos play in full-screen media view controller, use modern movie player. // FREEBIE --- .../Cells/ConversationViewCell.h | 6 +- .../ConversationView/Cells/OWSMessageCell.m | 6 +- .../ConversationViewController.m | 78 +- .../ViewControllers/FullImageViewController.m | 840 ++++++++++++------ 4 files changed, 592 insertions(+), 338 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 22b18ae08..db315d5f7 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -18,7 +18,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)didTapImageViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream imageView:(UIView *)imageView; -- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream; +- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem + attachmentStream:(TSAttachmentStream *)attachmentStream + imageView:(UIView *)imageView; - (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream; - (void)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem; - (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index da153cbc4..2aedc8d45 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSMessageCell.h" @@ -1172,7 +1172,9 @@ NS_ASSUME_NONNULL_BEGIN [self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream]; return; case OWSMessageCellType_Video: - [self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream]; + [self.delegate didTapVideoViewItem:self.viewItem + attachmentStream:self.attachmentStream + imageView:self.stillImageView]; return; case OWSMessageCellType_GenericAttachment: [AttachmentSharing showShareUIForAttachment:self.attachmentStream]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 0ada12975..07ff935cb 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -169,7 +169,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic) NSArray *viewItems; @property (nonatomic) NSMutableDictionary *viewItemCache; -@property (nonatomic, nullable) MPMoviePlayerController *videoPlayer; @property (nonatomic, nullable) AVAudioRecorder *audioRecorder; @property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer; @property (nonatomic, nullable) NSUUID *voiceMessageUUID; @@ -1999,6 +1998,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { OWSAssert(attachmentStream); OWSAssert(imageView); + [self dismissKeyBoard]; + UIWindow *window = [UIApplication sharedApplication].keyWindow; CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window]; FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream @@ -2007,43 +2008,22 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [vc presentFromViewController:self]; } -- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream +- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem + attachmentStream:(TSAttachmentStream *)attachmentStream + imageView:(UIImageView *)imageView { OWSAssert([NSThread isMainThread]); OWSAssert(viewItem); OWSAssert(attachmentStream); - NSFileManager *fileManager = [NSFileManager defaultManager]; - if (![fileManager fileExistsAtPath:[attachmentStream.mediaURL path]]) { - OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.mediaURL); - } - [self dismissKeyBoard]; - self.videoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:attachmentStream.mediaURL]; - [_videoPlayer prepareToPlay]; + UIWindow *window = [UIApplication sharedApplication].keyWindow; + CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(moviePlayerWillExitFullscreen:) - name:MPMoviePlayerWillExitFullscreenNotification - object:_videoPlayer]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(moviePlayerDidExitFullscreen:) - name:MPMoviePlayerDidExitFullscreenNotification - object:_videoPlayer]; - - _videoPlayer.controlStyle = MPMovieControlStyleDefault; - _videoPlayer.shouldAutoplay = YES; - [self.view addSubview:_videoPlayer.view]; - // We can't animate from the cell media frame; - // 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]; + FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream + fromRect:convertedRect + viewItem:viewItem]; + [vc presentFromViewController:self]; } - (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream @@ -2124,42 +2104,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self.navigationController pushViewController:view animated:YES]; } -#pragma mark - Video Playback - -// There's more than one way to exit the fullscreen video playback. -// There's a done button, a "toggle fullscreen" button and I think -// there's some gestures too. These fire slightly different notifications. -// We want to hide & clean up the video player immediately in all of -// these cases. -- (void)moviePlayerWillExitFullscreen:(id)sender -{ - DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - - [self clearVideoPlayer]; -} - -// See comment on moviePlayerWillExitFullscreen: -- (void)moviePlayerDidExitFullscreen:(id)sender -{ - DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - - [self clearVideoPlayer]; -} - -- (void)clearVideoPlayer -{ - [_videoPlayer stop]; - [_videoPlayer.view removeFromSuperview]; - self.videoPlayer = nil; -} - -- (void)setVideoPlayer:(MPMoviePlayerController *_Nullable)videoPlayer -{ - _videoPlayer = videoPlayer; - - [ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:videoPlayer != nil]; -} - #pragma mark - System Messages - (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction diff --git a/Signal/src/ViewControllers/FullImageViewController.m b/Signal/src/ViewControllers/FullImageViewController.m index 49a4c6bcb..50dc56e86 100644 --- a/Signal/src/ViewControllers/FullImageViewController.m +++ b/Signal/src/ViewControllers/FullImageViewController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "FullImageViewController.h" @@ -11,6 +11,8 @@ #import "UIColor+OWS.h" #import "UIUtil.h" #import "UIView+OWS.h" +#import +#import #import #import @@ -19,8 +21,6 @@ NS_ASSUME_NONNULL_BEGIN #define kMinZoomScale 1.0f #define kMaxZoomScale 8.0f -#define kBackgroundAlpha 0.6f - // In order to use UIMenuController, the view from which it is // presented must have certain custom behaviors. @interface AttachmentMenuView : UIView @@ -47,14 +47,12 @@ NS_ASSUME_NONNULL_BEGIN @interface FullImageViewController () -@property (nonatomic) UIView *backgroundView; @property (nonatomic) UIScrollView *scrollView; @property (nonatomic) UIImageView *imageView; + @property (nonatomic) UIButton *shareButton; -@property (nonatomic) UIView *contentView; @property (nonatomic) CGRect originRect; -@property (nonatomic) BOOL isPresenting; @property (nonatomic) NSData *fileData; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @@ -62,6 +60,15 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) ConversationViewItem *viewItem; @property (nonatomic) UIToolbar *footerBar; +@property (nonatomic) BOOL areToolbarsHidden; +@property (nonatomic, nullable) MPMoviePlayerController *mpVideoPlayer; +@property (nonatomic, nullable) AVPlayer *videoPlayer; + +@property (nonatomic, nullable) NSArray *imageViewConstraints; +@property (nonatomic, nullable) NSLayoutConstraint *imageViewBottomConstraint; +@property (nonatomic, nullable) NSLayoutConstraint *imageViewLeadingConstraint; +@property (nonatomic, nullable) NSLayoutConstraint *imageViewTopConstraint; +@property (nonatomic, nullable) NSLayoutConstraint *imageViewTrailingConstraint; @end @@ -71,7 +78,6 @@ NS_ASSUME_NONNULL_BEGIN fromRect:(CGRect)rect viewItem:(ConversationViewItem *_Nullable)viewItem { - self = [super initWithNibName:nil bundle:nil]; if (self) { @@ -85,7 +91,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect { - self = [super initWithNibName:nil bundle:nil]; if (self) { @@ -139,22 +144,42 @@ NS_ASSUME_NONNULL_BEGIN } } -- (void)loadView { - self.view = [AttachmentMenuView new]; - self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha]; - self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +- (BOOL)isVideo +{ + if (self.attachmentStream) { + return self.attachmentStream.isVideo; + } else if (self.attachment) { + return self.attachment.isVideo; + } else { + return NO; + } } -- (void)viewDidLoad { +- (void)loadView +{ + self.view = [AttachmentMenuView new]; + self.view.backgroundColor = [UIColor clearColor]; +} + +- (void)viewDidLoad +{ [super viewDidLoad]; - - [self initializeBackground]; - [self initializeContentViewAndFooterBar]; - [self initializeScrollView]; - [self initializeImageView]; + + [self createContents]; [self initializeGestureRecognizers]; - [self populateImageView:self.image]; + // Even though bars are opaque, we want content to be layed out behind them. + // The bars might obscure part of the content, but they can easily be hidden by tapping + // The alternative would be that content would shift when the navbars hide. + self.extendedLayoutIncludesOpaqueBars = YES; + + // TODO better title. + self.title = @"Attachment"; + + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop + target:self + action:@selector(didTapDismissButton:)]; } - (void)viewWillDisappear:(BOOL)animated { @@ -166,101 +191,281 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + [self updateMinZoomScale]; + [self centerImageViewConstraints]; +} + +- (void)updateMinZoomScale +{ + CGSize viewSize = self.scrollView.bounds.size; + UIImage *image = self.imageView.image; + OWSAssert(image); + + if (image.size.width == 0 || image.size.height == 0) { + OWSFail(@"%@ Invalid image dimensions. %@", self.logTag, NSStringFromCGSize(image.size)); + return; + } + + CGFloat scaleWidth = viewSize.width / image.size.width; + CGFloat scaleHeight = viewSize.height / image.size.height; + CGFloat minScale = MIN(scaleWidth, scaleHeight); + self.scrollView.minimumZoomScale = minScale; + self.scrollView.zoomScale = minScale; +} + #pragma mark - Initializers -- (void)initializeBackground { - self.imageView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha]; - - self.backgroundView = [UIView new]; - self.backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha]; - [self.view addSubview:self.backgroundView]; - [self.backgroundView autoPinEdgesToSuperviewEdges]; -} +- (void)createContents +{ + CGFloat kFooterHeight = 44; -- (void)initializeContentViewAndFooterBar { - self.contentView = [UIView new]; - [self.backgroundView addSubview:self.contentView]; - [self.contentView autoPinWidthToSuperview]; - [self.contentView autoPinToTopLayoutGuideOfViewController:self withInset:0]; - - self.footerBar = [UIToolbar new]; - _footerBar.barTintColor = [UIColor ows_signalBrandBlueColor]; - [self.footerBar setItems:@[ - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction - target:self - action:@selector(shareWasPressed:)], - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], - ] - animated:NO]; - [self.backgroundView addSubview:self.footerBar]; - [self.footerBar autoPinWidthToSuperview]; - [self.footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0]; - [self.footerBar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.contentView]; -} + UIScrollView *scrollView = [UIScrollView new]; + [self.view addSubview:scrollView]; + self.scrollView = scrollView; + scrollView.delegate = self; -- (void)shareWasPressed:(id)sender { - DDLogInfo(@"%@: sharing image.", self.logTag); + // TODO set max based on MIN. + scrollView.maximumZoomScale = kMaxZoomScale; + scrollView.showsVerticalScrollIndicator = NO; + scrollView.showsHorizontalScrollIndicator = NO; + scrollView.decelerationRate = UIScrollViewDecelerationRateFast; + self.automaticallyAdjustsScrollViewInsets = NO; - [AttachmentSharing showShareUIForURL:self.attachmentUrl]; -} + [scrollView autoPinToSuperviewEdges]; -- (void)initializeScrollView { - self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; - self.scrollView.delegate = self; - self.scrollView.zoomScale = 1.0f; - self.scrollView.maximumZoomScale = kMaxZoomScale; - self.scrollView.scrollEnabled = NO; - [self.contentView addSubview:self.scrollView]; -} - -- (void)initializeImageView { if (self.isAnimated) { if ([self.fileData ows_isValidImage]) { YYImage *animatedGif = [YYImage imageWithData:self.fileData]; - YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] init]; - imageView.image = animatedGif; - imageView.frame = self.originRect; - imageView.contentMode = UIViewContentModeScaleAspectFill; - imageView.clipsToBounds = YES; - self.imageView = imageView; + YYAnimatedImageView *animatedView = [[YYAnimatedImageView alloc] init]; + animatedView.image = animatedGif; + self.imageView = animatedView; } else { - self.imageView = [[UIImageView alloc] initWithFrame:self.originRect]; + self.imageView = [UIImageView new]; } + } else if (self.isVideo) { + [self setupVideoPlayer]; + + // Present the static video preview + UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image]; + self.imageView = imageView; + } else { // Present the static image using standard UIImageView - self.imageView = [[UIImageView alloc] initWithFrame:self.originRect]; - self.imageView.contentMode = UIViewContentModeScaleAspectFill; - self.imageView.userInteractionEnabled = YES; - self.imageView.clipsToBounds = YES; - self.imageView.layer.allowsEdgeAntialiasing = YES; - // Use trilinear filters for better scaling quality at - // some performance cost. - self.imageView.layer.minificationFilter = kCAFilterTrilinear; - self.imageView.layer.magnificationFilter = kCAFilterTrilinear; + UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image]; + + self.imageView = imageView; } - [self.scrollView addSubview:self.imageView]; + OWSAssert(self.imageView); + + [scrollView addSubview:self.imageView]; + self.imageView.contentMode = UIViewContentModeScaleAspectFit; + self.imageView.userInteractionEnabled = YES; + self.imageView.clipsToBounds = YES; + self.imageView.layer.allowsEdgeAntialiasing = YES; + self.imageView.translatesAutoresizingMaskIntoConstraints = NO; + + // Use trilinear filters for better scaling quality at + // some performance cost. + self.imageView.layer.minificationFilter = kCAFilterTrilinear; + self.imageView.layer.magnificationFilter = kCAFilterTrilinear; + + [self applyInitialImageViewConstraints]; + + if (self.isVideo) { + UIButton *playButton = [UIButton new]; + + [playButton addTarget:self action:@selector(playVideo) forControlEvents:UIControlEventTouchUpInside]; + + UIImage *playImage = [UIImage imageNamed:@"play_button"]; + [playButton setBackgroundImage:playImage forState:UIControlStateNormal]; + playButton.contentMode = UIViewContentModeScaleAspectFill; + + [self.view addSubview:playButton]; + + CGFloat playButtonWidth = ScaleFromIPhone5(70); + [playButton autoSetDimensionsToSize:CGSizeMake(playButtonWidth, playButtonWidth)]; + [playButton autoCenterInSuperview]; + } + + UIToolbar *footerBar = [UIToolbar new]; + _footerBar = footerBar; + footerBar.barTintColor = [UIColor ows_signalBrandBlueColor]; + [footerBar setItems:@[ + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction + target:self + action:@selector(didPressShare:)], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash + target:self + action:@selector(didPressDelete:)], + ] + animated:NO]; + [self.view addSubview:footerBar]; + + [footerBar autoPinWidthToSuperview]; + [footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0]; + [footerBar autoSetDimension:ALDimensionHeight toSize:kFooterHeight]; } -- (void)populateImageView:(UIImage *)image { - if (image && !self.isAnimated) { - self.imageView.image = image; +- (void)applyInitialImageViewConstraints +{ + if (self.imageViewConstraints.count > 0) { + [NSLayoutConstraint deactivateConstraints:self.imageViewConstraints]; + } + + CGRect convertedRect = + [self.imageView.superview convertRect:self.originRect fromView:[UIApplication sharedApplication].keyWindow]; + + NSMutableArray *imageViewConstraints = [NSMutableArray new]; + self.imageViewConstraints = imageViewConstraints; + + [imageViewConstraints addObjectsFromArray:[self.imageView autoSetDimensionsToSize:convertedRect.size]]; + [imageViewConstraints addObjectsFromArray:@[ + [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y], + [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x] + ]]; +} + +- (void)applyFinalImageViewConstraints +{ + if (self.imageViewConstraints.count > 0) { + [NSLayoutConstraint deactivateConstraints:self.imageViewConstraints]; + } + + NSMutableArray *imageViewConstraints = [NSMutableArray new]; + self.imageViewConstraints = imageViewConstraints; + + self.imageViewLeadingConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; + self.imageViewTopConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + self.imageViewTrailingConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; + self.imageViewBottomConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + + [imageViewConstraints addObjectsFromArray:@[ + self.imageViewTopConstraint, + self.imageViewTrailingConstraint, + self.imageViewBottomConstraint, + self.imageViewLeadingConstraint + ]]; +} + +- (void)setupVideoPlayer +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:[self.attachmentUrl path]]) { + OWSFail(@"%@ Missing video file: %@", self.logTag, self.attachmentStream.mediaURL); + } + + if (@available(iOS 9.0, *)) { + AVPlayer *player = [[AVPlayer alloc] initWithURL:self.attachmentUrl]; + self.videoPlayer = player; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playerItemDidPlayToCompletion:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:player.currentItem]; + } else { + MPMoviePlayerController *videoPlayer = + [[MPMoviePlayerController alloc] initWithContentURL:self.attachmentStream.mediaURL]; + self.mpVideoPlayer = videoPlayer; + + videoPlayer.controlStyle = MPMovieControlStyleNone; + [videoPlayer prepareToPlay]; + + // + // [[NSNotificationCenter defaultCenter] addObserver:self + // selector:@selector(moviePlayerWillExitFullscreen:) + // name:MPMoviePlayerWillExitFullscreenNotification + // object:videoPlayer]; + // [[NSNotificationCenter defaultCenter] addObserver:self + // selector:@selector(moviePlayerDidExitFullscreen:) + // name:MPMoviePlayerDidExitFullscreenNotification + // object:videoPlayer]; + // [[NSNotificationCenter defaultCenter] addObserver:self + // selector:@selector(moviePlayerWillEnterFullscreen:) + // name:MPMoviePlayerWillEnterFullscreenNotification + // object:videoPlayer]; + // [[NSNotificationCenter defaultCenter] addObserver:self + // selector:@selector(moviePlayerPlaybackStateDidChange:) + // name:MPMoviePlayerPlaybackStateDidChangeNotification + // object:videoPlayer]; + // + // [[NSNotificationCenter defaultCenter] addObserver:self + // selector:@selector(moviePlayerDidEnterFullscreen:) + // name:MPMoviePlayerDidEnterFullscreenNotification + // object:videoPlayer]; + // + // + // [[NSNotificationCenter defaultCenter] addObserver:self + // selector:@selector(moviePlayerDidFinishPlayback:) + // name:MPMoviePlayerPlaybackDidFinishNotification + // object:videoPlayer]; + // + // // Don't show any controls intially. We switch control style after the view is fullscreen to make them + // appear upon tapping. + //// videoPlayer.controlStyle = MPMovieControlStyleFullscreen; + // videoPlayer.shouldAutoplay = YES; + // + // // We can't animate from the cell media frame; + // // MPMoviePlayerController will animate a crop of its + // // contents rather than scaling them. + // videoPlayer.view.frame = self.view.bounds; + // + // self.imageView = videoPlayer.view; } } -- (void)initializeGestureRecognizers { - UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(imageDismissGesture:)]; - singleTap.delegate = self; - [self.view addGestureRecognizer:singleTap]; - - UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(imageDismissGesture:)]; +- (void)setAreToolbarsHidden:(BOOL)areToolbarsHidden +{ + if (_areToolbarsHidden == areToolbarsHidden) { + return; + } + + _areToolbarsHidden = areToolbarsHidden; + + if (!areToolbarsHidden) { + // Hiding the status bar affects the positioing of the navbar. We don't want to show that in the animation + // so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last. + [[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden withAnimation:UIStatusBarAnimationFade]; + } + [UIView animateWithDuration:0.1 + animations:^(void) { + self.view.backgroundColor = areToolbarsHidden ? UIColor.blackColor : UIColor.whiteColor; + self.navigationController.navigationBar.alpha = areToolbarsHidden ? 0 : 1; + self.footerBar.alpha = areToolbarsHidden ? 0 : 1; + } + completion:^(BOOL finished) { + // although navbar has 0 alpha at this point, if we don't also "hide" it, adjusting the status bar + // resets the alpha. + if (areToolbarsHidden) { + // [self.navigationController setNavigationBarHidden:areToolbarsHidden + // animated:NO]; + // Hiding the status bar affects the positioing of the navbar. We don't want to show that in the + // animation so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last. + [[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden + withAnimation:UIStatusBarAnimationNone]; + // position the navbar, but have it be transparent + self.navigationController.navigationBar.alpha = 0; + } + }]; +} + +- (void)initializeGestureRecognizers +{ + UITapGestureRecognizer *doubleTap = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTapImage:)]; doubleTap.numberOfTapsRequired = 2; - doubleTap.delegate = self; [self.view addGestureRecognizer:doubleTap]; - + + UITapGestureRecognizer *singleTap = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapImage:)]; + [singleTap requireGestureRecognizerToFail:doubleTap]; + + [self.view addGestureRecognizer:singleTap]; + // UISwipeGestureRecognizer supposedly supports multiple directions, // but in practice it works better if you use a separate GR for each // direction. @@ -270,8 +475,8 @@ NS_ASSUME_NONNULL_BEGIN @(UISwipeGestureRecognizerDirectionUp), @(UISwipeGestureRecognizerDirectionDown), ]) { - UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self - action:@selector(imageDismissGesture:)]; + UISwipeGestureRecognizer *swipe = + [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(didSwipeImage:)]; swipe.direction = (UISwipeGestureRecognizerDirection) direction.integerValue; swipe.delegate = self; [self.view addGestureRecognizer:swipe]; @@ -285,10 +490,39 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Gesture Recognizers -- (void)imageDismissGesture:(UIGestureRecognizer *)sender { - if (sender.state == UIGestureRecognizerStateRecognized) { + +- (void)didTapDismissButton:(id)sender +{ + [self dismiss]; +} + +- (void)didTapImage:(id)sender +{ + DDLogVerbose(@"%@ did tap image.", self.logTag); + self.areToolbarsHidden = !self.areToolbarsHidden; +} + +- (void)didDoubleTapImage:(id)sender +{ + DDLogVerbose(@"%@ did tap image.", self.logTag); + if (self.scrollView.zoomScale == self.scrollView.minimumZoomScale) { + [self.scrollView setZoomScale:self.scrollView.minimumZoomScale * 2 animated:YES]; + } else { + [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES]; + } +} + +- (void)didSwipeImage:(UIGestureRecognizer *)sender +{ + // Ignore if image is zoomed in at all. + // e.g. otherwise, for example, if the image is horizontally larger than the scroll + // view, but fits vertically, swiping left/right will scroll the image, but swiping up/down + // would dismiss the image. That would not be intuitive. + if (self.scrollView.zoomScale != self.scrollView.minimumZoomScale) { + return; + } + [self dismiss]; - } } - (void)longPressGesture:(UIGestureRecognizer *)sender { @@ -318,6 +552,31 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)didPressShare:(id)sender +{ + DDLogInfo(@"%@: sharing image.", self.logTag); + + [self.viewItem shareAction]; +} + +- (void)didPressDelete:(id)sender +{ + DDLogInfo(@"%@: sharing image.", self.logTag); + + UIAlertController *actionSheet = + [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", nil) + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + [self.viewItem deleteAction]; + [self dismiss]; + }]]; + + [actionSheet addAction:[OWSAlerts cancelAction]]; + + [self presentViewController:actionSheet animated:YES completion:nil]; +} + - (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender { if (action == self.viewItem.metadataActionSelector) { @@ -343,169 +602,96 @@ NS_ASSUME_NONNULL_BEGIN - (void)deleteAction:(nullable id)sender { - [self.viewItem deleteAction]; - - [self dismiss]; -} - -- (BOOL)canBecomeFirstResponder -{ - return YES; + [self didPressDelete:sender]; } #pragma mark - Presentation -- (void)presentFromViewController:(UIViewController *)viewController { - _isPresenting = YES; - self.view.userInteractionEnabled = NO; - [self.view addSubview:self.imageView]; - self.modalPresentationStyle = UIModalPresentationOverCurrentContext; - self.view.alpha = 0; +- (void)presentFromViewController:(UIViewController *)viewController +{ + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self]; - [viewController - presentViewController:self - animated:NO - completion:^{ - UIWindow *window = [UIApplication sharedApplication].keyWindow; - // During the presentation animation, we want to seamlessly animate the image - // from its location in the conversation view. To do so, we need a - // consistent coordinate system, so we pass the `originRect` in the - // coordinate system of the window. - self.imageView.frame = [self.view convertRect:self.originRect - fromView:window]; - - [UIView animateWithDuration:0.25f - delay:0 - options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut - animations:^() { - self.view.alpha = 1.0f; - // During the presentation animation, we want to seamlessly animate the image - // to its resting location in this view. We use `resizedFrameForImageView` - // to determine its size "at rest" in the content view, and then convert - // from the content view's coordinate system to the root view coordinate - // system because the image view is temporarily hosted by the root view during - // the presentation animation. - self.imageView.frame = [self resizedFrameForImageView:self.image.size]; - self.imageView.center = [self.contentView convertPoint:self.contentView.center - fromView:self.contentView]; - } - completion:^(BOOL completed) { - self.scrollView.frame = self.contentView.bounds; - [self.scrollView addSubview:self.imageView]; - [self updateLayouts]; - self.view.userInteractionEnabled = YES; - _isPresenting = NO; - }]; - [UIUtil modalCompletionBlock](); - }]; + // UIModalPresentationCustom retains the current view context behind our VC, allowing us to manually + // animate in our view, over the existing context, similar to a cross disolve, but allowing us to have + // more fine grained control + navController.modalPresentationStyle = UIModalPresentationCustom; + navController.navigationBar.barTintColor = UIColor.ows_materialBlueColor; + navController.navigationBar.translucent = NO; + navController.navigationBar.opaque = YES; + + self.view.userInteractionEnabled = NO; + + self.view.alpha = 0.0; + [viewController presentViewController:navController + animated:NO + completion:^{ + + // 1. Fade in the entire view. + [UIView animateWithDuration:0.1 + animations:^{ + self.view.alpha = 1.0; + }]; + + // Make sure imageView is layed out before we update it's frame in the next + // animation. + [self.imageView.superview layoutIfNeeded]; + + // 2. Animate imageView from it's initial position, which should match where it was + // in the presenting view to it's final position, front and center in this view. This + // animation intentionally overlaps the previous + [UIView animateWithDuration:0.2 + delay:0.08 + options:UIViewAnimationOptionCurveEaseOut + animations:^(void) { + [self applyFinalImageViewConstraints]; + [self.imageView.superview layoutIfNeeded]; + // We must lay out *before* we centerImageViewConstraints + // because it uses the imageView.frame to build the contstraints + // that will center the imageView, and then once again + // to ensure that the centered constraints are applied. + [self centerImageViewConstraints]; + [self.imageView.superview layoutIfNeeded]; + self.view.backgroundColor = UIColor.whiteColor; + } + completion:^(BOOL finished) { + self.view.userInteractionEnabled = YES; + + if (self.isVideo) { + [self playVideo]; + } + }]; + }]; } -- (void)dismiss { +- (void)dismiss +{ self.view.userInteractionEnabled = NO; - [UIView animateWithDuration:0.25f - delay:0 - options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveLinear - animations:^() { - self.backgroundView.backgroundColor = [UIColor clearColor]; - self.scrollView.alpha = 0; - self.view.alpha = 0; + [UIApplication sharedApplication].statusBarHidden = NO; + + OWSAssert(self.imageView.superview); + + [self.imageView.superview layoutIfNeeded]; + + // Move the image view pack to it's initial position, i.e. where + // it sits on the screen in the conversation view. + [self applyInitialImageViewConstraints]; + [UIView animateWithDuration:0.2 + delay:0.0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^(void) { + [self.imageView.superview layoutIfNeeded]; + + // In case user has hidden bars, which changes background to black. + self.view.backgroundColor = UIColor.whiteColor; + + // fade out content and toolbars + self.navigationController.view.alpha = 0.0; } - completion:^(BOOL completed) { - [self.presentingViewController dismissViewControllerAnimated:NO completion:nil]; + completion:^(BOOL finished) { + [self.presentingViewController dismissViewControllerAnimated:NO completion:nil]; }]; } -#pragma mark - Update Layout - -- (void)viewDidLayoutSubviews { - [self updateLayouts]; -} - -- (void)updateLayouts { - if (_isPresenting) { - return; - } - - self.scrollView.frame = self.contentView.bounds; - self.imageView.frame = [self resizedFrameForImageView:self.image.size]; - self.scrollView.contentSize = self.imageView.frame.size; - self.scrollView.contentInset = [self contentInsetForScrollView:self.scrollView.zoomScale]; -} - -#pragma mark - Resizing - -- (CGRect)resizedFrameForImageView:(CGSize)imageSize { - CGRect frame = self.contentView.bounds; - CGSize screenSize = - CGSizeMake(frame.size.width * self.scrollView.zoomScale, frame.size.height * self.scrollView.zoomScale); - CGSize targetSize = screenSize; - - if ([self isImagePortrait]) { - if ([self getAspectRatioForCGSize:screenSize] < [self getAspectRatioForCGSize:imageSize]) { - targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize]; - } else { - targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize]; - } - } else { - if ([self getAspectRatioForCGSize:screenSize] > [self getAspectRatioForCGSize:imageSize]) { - targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize]; - } else { - targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize]; - } - } - - frame.size = targetSize; - frame.origin = CGPointMake(0, 0); - return frame; -} - -- (UIEdgeInsets)contentInsetForScrollView:(CGFloat)targetZoomScale { - UIEdgeInsets inset = UIEdgeInsetsZero; - - CGSize boundsSize = self.scrollView.bounds.size; - CGSize contentSize = self.image.size; - CGSize minSize; - - if ([self isImagePortrait]) { - if ([self getAspectRatioForCGSize:boundsSize] < [self getAspectRatioForCGSize:contentSize]) { - minSize.height = boundsSize.height; - minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize]; - } else { - minSize.width = boundsSize.width; - minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize]; - } - } else { - if ([self getAspectRatioForCGSize:boundsSize] > [self getAspectRatioForCGSize:contentSize]) { - minSize.width = boundsSize.width; - minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize]; - } else { - minSize.height = boundsSize.height; - minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize]; - } - } - - CGSize finalSize = self.view.bounds.size; - - minSize.width *= targetZoomScale; - minSize.height *= targetZoomScale; - - if (minSize.height > finalSize.height && minSize.width > finalSize.width) { - inset = UIEdgeInsetsZero; - } else { - CGFloat dy = boundsSize.height - minSize.height; - CGFloat dx = boundsSize.width - minSize.width; - - dy = (dy > 0) ? dy : 0; - dx = (dx > 0) ? dx : 0; - - inset.top = dy / 2.0f; - inset.bottom = dy / 2.0f; - inset.left = dx / 2.0f; - inset.right = dx / 2.0f; - } - return inset; -} - #pragma mark - UIScrollViewDelegate - (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView @@ -513,30 +699,150 @@ NS_ASSUME_NONNULL_BEGIN return self.imageView; } -- (void)scrollViewDidZoom:(UIScrollView *)scrollView { - scrollView.contentInset = [self contentInsetForScrollView:scrollView.zoomScale]; +- (void)centerImageViewConstraints +{ + OWSAssert(self.scrollView); - if (self.scrollView.scrollEnabled == NO) { - self.scrollView.scrollEnabled = YES; + CGSize scrollViewSize = self.scrollView.bounds.size; + CGSize imageViewSize = self.imageView.frame.size; + + CGFloat yOffset = MAX(0, (scrollViewSize.height - imageViewSize.height) / 2); + self.imageViewTopConstraint.constant = yOffset; + self.imageViewBottomConstraint.constant = yOffset; + + CGFloat xOffset = MAX(0, (scrollViewSize.width - imageViewSize.width) / 2); + self.imageViewLeadingConstraint.constant = xOffset; + self.imageViewTrailingConstraint.constant = xOffset; +} + +- (void)scrollViewDidZoom:(UIScrollView *)scrollView +{ + [self centerImageViewConstraints]; + [self.view layoutIfNeeded]; +} + +#pragma mark - Video Playback + +- (void)playVideo +{ + OWSAssert(self.isVideo); + OWSAssert(self.videoPlayer); + + AVPlayerViewController *vc = [AVPlayerViewController new]; + AVPlayer *player = self.videoPlayer; + vc.player = player; + + vc.modalPresentationStyle = UIModalPresentationCustom; + vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + + // Rewind for repeated plays + [player seekToTime:kCMTimeZero]; + [self presentViewController:vc + animated:NO + completion:^(void) { + [player play]; + }]; +} + +- (void)playerItemDidPlayToCompletion:(NSNotification *)notification +{ + OWSAssert(self.isVideo); + OWSAssert(self.videoPlayer); + DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + + [self dismissViewControllerAnimated:NO completion:nil]; +} + +- (void)moviePlayerPlaybackStateDidChange:(NSNotification *)notification +{ + DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + OWSAssert(self.mpVideoPlayer); +} + +- (void)moviePlayerWillEnterFullscreen:(NSNotification *)notification +{ + DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + OWSAssert(self.videoPlayer); + self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone; +} + +- (void)moviePlayerDidEnterFullscreen:(NSNotification *)notification +{ + DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + OWSAssert(self.videoPlayer); + self.mpVideoPlayer.controlStyle = MPMovieControlStyleFullscreen; +} + +// There's more than one way to exit the fullscreen video playback. +// There's a done button, a "toggle fullscreen" button and I think +// there's some gestures too. These fire slightly different notifications. +// We want to hide & clean up the video player immediately in all of +// these cases. +- (void)moviePlayerWillExitFullscreen:(NSNotification *)notification +{ + DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + + // If we didn't just complete playback, user chose to exit fullscreen. + // In that case, we dismiss the view controller since the user is probably done. + // if (!self.didJustCompleteVideoPlayback) { + // [self dismiss]; + // } + + // self.didJustCompleteVideoPlayback = NO; + self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone; + + // [self clearVideoPlayer]; +} + +// See comment on moviePlayerWillExitFullscreen: +- (void)moviePlayerDidExitFullscreen:(NSNotification *)notification +{ + DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + self.mpVideoPlayer.controlStyle = MPMovieControlStyleEmbedded; + // [self clearVideoPlayer]; +} + +- (void)moviePlayerDidFinishPlayback:(NSNotification *)notification +{ + OWSAssert(self.videoPlayer); + + NSNumber *reason = notification.userInfo[MPMoviePlayerPlaybackDidFinishReasonUserInfoKey]; + DDLogDebug(@"%@ movie player finished with reason %@", self.logTag, reason); + OWSAssert(reason); + + switch (reason.integerValue) { + case MPMovieFinishReasonPlaybackEnded: { + DDLogDebug(@"%@ video played to completion.", self.logTag); + self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone; + [self.mpVideoPlayer setFullscreen:NO animated:YES]; + break; + } + case MPMovieFinishReasonPlaybackError: { + DDLogDebug(@"%@ error playing video.", self.logTag); + break; + } + case MPMovieFinishReasonUserExited: { + // FIXME: unable to fire this (only tried on iOS11.2 so far) + DDLogDebug(@"%@ user exited video playback", self.logTag); + [self dismiss]; + break; + } } } -- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale -{ - self.scrollView.scrollEnabled = (scale > 1); - self.scrollView.contentInset = [self contentInsetForScrollView:scale]; -} - -#pragma mark - Utility - -- (BOOL)isImagePortrait { - return ([self getAspectRatioForCGSize:self.image.size] > 1.0f); -} - -- (CGFloat)getAspectRatioForCGSize:(CGSize)size { - return size.height / size.width; -} +//- (void)clearVideoPlayer +//{ +// [self.videoPlayer stop]; +// [self.videoPlayer.view removeFromSuperview]; +// self.videoPlayer = nil; +//} +//- (void)setVideoPlayer:(MPMoviePlayerController *_Nullable)videoPlayer +//{ +// _mpVideoPlayer = mpVideoPlayer; +// +// [ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:videoPlayer != nil]; +//} #pragma mark - Saving images to Camera Roll From 86d61eee30ab5117dec326281214dbceedc59454 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Sun, 7 Jan 2018 12:49:34 -0500 Subject: [PATCH 03/23] Custom video player layer to avoid "double present/dismiss" // FREEBIE --- Signal.xcodeproj/project.pbxproj | 4 + .../Contents.json | 23 ++ .../VideoPlayer_Slider_Thumb_15x15_@1x.png | Bin 0 -> 3522 bytes .../VideoPlayer_Slider_Thumb_15x15_@2x.png | Bin 0 -> 3628 bytes .../VideoPlayer_Slider_Thumb_15x15_@3x.png | Bin 0 -> 3879 bytes .../ViewControllers/FullImageViewController.m | 233 ++++++++++++------ Signal/src/views/VideoPlayerView.swift | 200 +++++++++++++++ 7 files changed, 389 insertions(+), 71 deletions(-) create mode 100644 Signal/Images.xcassets/sliderProgressThumb.imageset/Contents.json create mode 100644 Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@1x.png create mode 100644 Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@2x.png create mode 100644 Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@3x.png create mode 100644 Signal/src/views/VideoPlayerView.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 7e8623061..416be1250 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -160,6 +160,7 @@ 452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; }; 452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; }; 452ECA4E1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; }; + 453034AB200289F50018945D /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453034AA200289F50018945D /* VideoPlayerView.swift */; }; 45360B8D1F9521F800FA666C /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; }; 45360B8E1F9521F800FA666C /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; }; 45360B901F9527DA00FA666C /* SearcherTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8F1F9527DA00FA666C /* SearcherTest.swift */; }; @@ -633,6 +634,7 @@ 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = ""; }; 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = ""; }; 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageFetcherJob.swift; path = Jobs/MessageFetcherJob.swift; sourceTree = ""; }; + 453034AA200289F50018945D /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; 45360B8C1F9521F800FA666C /* Searcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Searcher.swift; sourceTree = ""; }; 45360B8F1F9527DA00FA666C /* SearcherTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearcherTest.swift; sourceTree = ""; }; 45387B021E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWS102MoveLoggingPreferenceToUserDefaults.h; path = Migrations/OWS102MoveLoggingPreferenceToUserDefaults.h; sourceTree = ""; }; @@ -1507,6 +1509,7 @@ 76EB052B18170B33006006FC /* Views */ = { isa = PBXGroup; children = ( + 453034AA200289F50018945D /* VideoPlayerView.swift */, 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */, 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */, 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */, @@ -2327,6 +2330,7 @@ 45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */, 45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */, 34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */, + 453034AB200289F50018945D /* VideoPlayerView.swift in Sources */, 45F3AEB61DFDE7900080CE33 /* AvatarImageView.swift in Sources */, 7038632718F70C0700D4A43F /* CryptoTools.m in Sources */, 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */, diff --git a/Signal/Images.xcassets/sliderProgressThumb.imageset/Contents.json b/Signal/Images.xcassets/sliderProgressThumb.imageset/Contents.json new file mode 100644 index 000000000..6c8bfcfad --- /dev/null +++ b/Signal/Images.xcassets/sliderProgressThumb.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "VideoPlayer_Slider_Thumb_15x15_@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "VideoPlayer_Slider_Thumb_15x15_@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "VideoPlayer_Slider_Thumb_15x15_@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@1x.png b/Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..194cce88a451db1eb9464fb52b3dd788b8560346 GIT binary patch literal 3522 zcmV;z4L$OSP)6dx000c$X+uL$Nkc;* zP;zf(X>4Tx09b{kR|i;A$rhellF$-*l@dDA3@BYnqy~^)1W}10kU$7A1p%=kiz~Qx z5y7&eq9}_UfyIUe3u_0v3%V#M>*6XlEN>EuSa#q0zIXG@z5kta&YU`DZU9i_Fa-i0 z3INFDi-mzv^n@u1iB!^EAP2O8GO!2AOqNJMkBA6`EP$WIXM=C9SM30iKCjtH*ZP+J zuNNVOEn>mvaX4~QM665zkcj}0x>*9D7yx-QIBz;zERf;7Gf`mZ*n9-Qak|w zOe>EQ6A!T=#KHWTVc`(FL(E}`d=nwIg1C~yW=Qh{V}_mQiWxBwLw(pYVOErs-x>gX zVhY>Oe{dd8&BzLo+F%0lK~Z+J|FF$@9N(}(U3hysGdKcb3y5E`d4W-b>k#w=;t1)Q z@SWht=Y@q1`b8*Zi)8-7cS5_E6C=%;BLGAbu`niT&^BUBDmQ=uaR9_Q9AQw@pufbU z0-mhSU`&Ybg_5Y)K_5v0Y<{eCO=(>$U<&;M2V+HQ0&xfvut64>3GXbx2c3WlM1Tvj zWhf0WK_(njXn`#pfsl(2hY)gzK!%J%2)ah-!xB;%0iKJ!kOJig<)mw5 z!6AKlx8H3)(u``_b_lj^PO?C5G=j=plb7J|bw%dnl7JB>k12EA%8*0zv7rNXGQ4 z5i6(#!WmrFrR9D9lC^KO+kKI@@X>W4tCW;)>H_K_TqRs4+$UTIRQUXZ&_?Kn=nmm7 z{N4I)RI)~5kOoKANCmLJ$PF8E@?=`Rwexe%2G{v&1q2)6q{~*3QZwP;%6LR0`IA5_ zhWAvNwB1*^3;bGdOMa6MptdhPehyG;R2t5Hj&>VfpL9*BB|nwFLQf!=UX?$<7JQ~k z^T1vl_9_e%*wXgWdTHBeHMAGB`=2>zH)wy-+GtxK zUv>oc=UqoJr8rPX6l;nTg-$^z#_;Dz@qx&cVnPX}XhUic#ggJj88Z@(;acYnWtjWL z{vNbA0aAx6fiLSWOvowaU=I2FwU5*%y@>k7&WeSyTrgWE zFOuzczD!Q=Mg56nT?oz~Q)M~)j%~Oh?tn932MPyNoQ^x;K0|o0D-HK0S*CDrNcF<~ za93#``=T$f{;Tz)cLtm2PP8QY6D>c}iGIW&VgP(PNau;;h`|uMNx8)A9I^C}_stUI z3b|<V$mWBEpa_(J6<{qW2b;k*Pz`FqesCBx z!5(`CTmY9r8@L0{lP>TS^nq7k06`EELPpdPUBn16M{E#B#1-*E{E<*33Q0hcku-#l zNRWJF5mJJzLduaXNHww#X+TaO=a4JNEu<6aLHmsBoeh?eq3PQ5{T){XQb)N;d8RC{Y_3dKp01pyT&jFP`I2(4ioA-2imwV&Wu8if zN`uM`m3~!qRR`5@)lAi;s=HLrsdlT8)GXBe)l$?7)wZggRC}n7shg_%s=@~oMw(@h2{~>4lPW}LW`lr(^{^zU#nf~owk8C zU7M}FM7vh|s`hK0F*-gvY@K4AJvwbV?{ulU{<;~uD|HX+-qXYNto0)GX6tRzJFV9{ zMq`Y}7}l8LG5g2d)<^ZN^`rFX=x^0;)$cbjGzc^h7_2uqY0zt^W$0s=Zn(zqs9}$h zx{;R=*JzE=F{39`O)8zrqn1%mQTvSbjf0FO##@Xp8V{IQn#7tEnCvmRXR2W8YMN@g z#qc;!)ibBkw(7u74->u0YH zZ)0z!ccu3OA5)(cpDLfn^s)4GdJX;AB-$k5q(hTl`?~wi^*!!~`33qF`(5-`^N;l} z_rD)t5|9?KJD@MnDR55U@gPD_NYKil8w>*mi&4$!3w8;f8+-WB~S zW>U=Zm^-mnv69&4IOVvcxazpq@qY2E;yb2{n=*IGg#^6>ZbE$`DKRdwDzSg6|J1cp zyONxeijvx=Sx(EDc5b@fbl&tMGn8jAXBAtO5UEwo0rdKS(;W zy|OE^-^`AieIQ3Qho94$Yn{6!_iTD17)s;19ctC`H1e8;d;_Z~hVgk^AGt zCF7T@U-GdyrTBaat)#T%{ZiJ_^FKNKRQA)SWvR<9E_YtOX$5fwe?|LB@0B}OsjkXf z^>Fp%)pcvAYl_#rTsvd!xzY)xo7X9TB(4x9ri{Q?eKAmF#`8FL_`4{*e96 z2V4)-9yC9=;gI^F;yR=*x32Fn=Ws`TLj9G7;D+W#kH$m4+W)$v$+)TFh~|;yM-`41 z9{qGI?^yqF!SUV`X(t{w&uG4La>~i;ry@^XJRNfS?3sWwCtK(($Ig15Z939<1{oiEm1a=BE0+3j-Um5Eo5UiH3u;+o&JGuIi{FSLcVU1^VP zzj-6+M#s&Ro87l~x1Qh5zCCbf0sM$BzNd1p^uGT6%HPNSzNf>v<4C7pXX}IL2X`K_ z9zOj;^vC!%z4ZJRXqx)v(Ti3TO@8aJ*9+>@}^uFxF*bj{# zLq6X9l=4Tx09b{kR|i;A$rhellF$-*l@dDA3@BYnqy~^)1W}10kU$7A1p%=kiz~Qx z5y7&eq9}_UfyIUe3u_0v3%V#M>*6XlEN>EuSa#q0zIXG@z5kta&YU`DZU9i_Fa-i0 z3INFDi-mzv^n@u1iB!^EAP2O8GO!2AOqNJMkBA6`EP$WIXM=C9SM30iKCjtH*ZP+J zuNNVOEn>mvaX4~QM665zkcj}0x>*9D7yx-QIBz;zERf;7Gf`mZ*n9-Qak|w zOe>EQ6A!T=#KHWTVc`(FL(E}`d=nwIg1C~yW=Qh{V}_mQiWxBwLw(pYVOErs-x>gX zVhY>Oe{dd8&BzLo+F%0lK~Z+J|FF$@9N(}(U3hysGdKcb3y5E`d4W-b>k#w=;t1)Q z@SWht=Y@q1`b8*Zi)8-7cS5_E6C=%;BLGAbu`niT&^BUBDmQ=uaR9_Q9AQw@pufbU z0-mhSU`&Ybg_5Y)K_5v0Y<{eCO=(>$U<&;M2V+HQ0&xfvut64>3GXbx2c3WlM1Tvj zWhf0WK_(njXn`#pfsl(2hY)gzK!%J%2)ah-!xB;%0iKJ!kOJig<)mw5 z!6AKlx8H3)(u``_b_lj^PO?C5G=j=plb7J|bw%dnl7JB>k12EA%8*0zv7rNXGQ4 z5i6(#!WmrFrR9D9lC^KO+kKI@@X>W4tCW;)>H_K_TqRs4+$UTIRQUXZ&_?Kn=nmm7 z{N4I)RI)~5kOoKANCmLJ$PF8E@?=`Rwexe%2G{v&1q2)6q{~*3QZwP;%6LR0`IA5_ zhWAvNwB1*^3;bGdOMa6MptdhPehyG;R2t5Hj&>VfpL9*BB|nwFLQf!=UX?$<7JQ~k z^T1vl_9_e%*wXgWdTHBeHMAGB`=2>zH)wy-+GtxK zUv>oc=UqoJr8rPX6l;nTg-$^z#_;Dz@qx&cVnPX}XhUic#ggJj88Z@(;acYnWtjWL z{vNbA0aAx6fiLSWOvowaU=I2FwU5*%y@>k7&WeSyTrgWE zFOuzczD!Q=Mg56nT?oz~Q)M~)j%~Oh?tn932MPyNoQ^x;K0|o0D-HK0S*CDrNcF<~ za93#``=T$f{;Tz)cLtm2PP8QY6D>c}iGIW&VgP(PNau;;h`|uMNx8)A9I^C}_stUI z3b|<V$mWBEpa_(J6<{qW2b;k*Pz`FqesCBx z!5(`CTmY9r8@L0{lP>TS^nq7k06`EELPpdPUBn16M{E#B#1-*E{E<*33Q0hcku-#l zNRWJF5mJJzLduaXNHww#X+TaO=a4JNEu<6aLHmsBoeh?eq3PQ5{T){XQb)N;d8RC{Y_3dKp01pyT&jFP`I2(4ioA-2imwV&Wu8if zN`uM`m3~!qRR`5@)lAi;s=HLrsdlT8)GXBe)l$?7)wZggRC}n7shg_%s=@~oMw(@h2{~>4lPW}LW`lr(^{^zU#nf~owk8C zU7M}FM7vh|s`hK0F*-gvY@K4AJvwbV?{ulU{<;~uD|HX+-qXYNto0)GX6tRzJFV9{ zMq`Y}7}l8LG5g2d)<^ZN^`rFX=x^0;)$cbjGzc^h7_2uqY0zt^W$0s=Zn(zqs9}$h zx{;R=*JzE=F{39`O)8zrqn1%mQTvSbjf0FO##@Xp8V{IQn#7tEnCvmRXR2W8YMN@g z#qc;!)ibBkw(7u74->u0YH zZ)0z!ccu3OA5)(cpDLfn^s)4GdJX;AB-$k5q(hTl`?~wi^*!!~`33qF`(5-`^N;l} z_rD)t5|9?KJD@MnDR55U@gPD_NYKil8w>*mi&4$!3w8;f8+-WB~S zW>U=Zm^-mnv69&4IOVvcxazpq@qY2E;yb2{n=*IGg#^6>ZbE$`DKRdwDzSg6|J1cp zyONxeijvx=Sx(EDc5b@fbl&tMGn8jAXBAtO5UEwo0rdKS(;W zy|OE^-^`AieIQ3Qho94$Yn{6!_iTD17)s;19ctC`H1e8;d;_Z~hVgk^AGt zCF7T@U-GdyrTBaat)#T%{ZiJ_^FKNKRQA)SWvR<9E_YtOX$5fwe?|LB@0B}OsjkXf z^>Fp%)pcvAYl_#rTsvd!xzY)xo7X9TB(4x9ri{Q?eKAmF#`8FL_`4{*e96 z2V4)-9yC9=;gI^F;yR=*x32Fn=Ws`TLj9G7;D+W#kH$m4+W)$v$+)TFh~|;yM-`41 z9{qGI?^yqF!SUV`X(t{w&uG4La>~i;ry@^XJRNfS?3sWwCtK(($Ig15Z939<1{oiEm1a=BE0+3j-Um5Eo5UiH3u;+o&JGuIi{FSLcVU1^VP zzj-6+M#s&Ro87l~x1Qh5zCCbf0sM$BzNd1p^uGT6%HPNSzNf>v<4C7pXX}IL2X`K_ z9zOj;^vC!%z4ZJRXqx)v(Ti3TO@8aJ*9+>@}^uFxF*bj{# zLq6X9l=j-5ySyCfRdG$|wznii9x5*3s(R~5AvwyR5$PPgO00004Tx09b{kR|i;A$rhellF$-*l@dDA3@BYnqy~^)1W}10kU$7A1p%=kiz~Qx z5y7&eq9}_UfyIUe3u_0v3%V#M>*6XlEN>EuSa#q0zIXG@z5kta&YU`DZU9i_Fa-i0 z3INFDi-mzv^n@u1iB!^EAP2O8GO!2AOqNJMkBA6`EP$WIXM=C9SM30iKCjtH*ZP+J zuNNVOEn>mvaX4~QM665zkcj}0x>*9D7yx-QIBz;zERf;7Gf`mZ*n9-Qak|w zOe>EQ6A!T=#KHWTVc`(FL(E}`d=nwIg1C~yW=Qh{V}_mQiWxBwLw(pYVOErs-x>gX zVhY>Oe{dd8&BzLo+F%0lK~Z+J|FF$@9N(}(U3hysGdKcb3y5E`d4W-b>k#w=;t1)Q z@SWht=Y@q1`b8*Zi)8-7cS5_E6C=%;BLGAbu`niT&^BUBDmQ=uaR9_Q9AQw@pufbU z0-mhSU`&Ybg_5Y)K_5v0Y<{eCO=(>$U<&;M2V+HQ0&xfvut64>3GXbx2c3WlM1Tvj zWhf0WK_(njXn`#pfsl(2hY)gzK!%J%2)ah-!xB;%0iKJ!kOJig<)mw5 z!6AKlx8H3)(u``_b_lj^PO?C5G=j=plb7J|bw%dnl7JB>k12EA%8*0zv7rNXGQ4 z5i6(#!WmrFrR9D9lC^KO+kKI@@X>W4tCW;)>H_K_TqRs4+$UTIRQUXZ&_?Kn=nmm7 z{N4I)RI)~5kOoKANCmLJ$PF8E@?=`Rwexe%2G{v&1q2)6q{~*3QZwP;%6LR0`IA5_ zhWAvNwB1*^3;bGdOMa6MptdhPehyG;R2t5Hj&>VfpL9*BB|nwFLQf!=UX?$<7JQ~k z^T1vl_9_e%*wXgWdTHBeHMAGB`=2>zH)wy-+GtxK zUv>oc=UqoJr8rPX6l;nTg-$^z#_;Dz@qx&cVnPX}XhUic#ggJj88Z@(;acYnWtjWL z{vNbA0aAx6fiLSWOvowaU=I2FwU5*%y@>k7&WeSyTrgWE zFOuzczD!Q=Mg56nT?oz~Q)M~)j%~Oh?tn932MPyNoQ^x;K0|o0D-HK0S*CDrNcF<~ za93#``=T$f{;Tz)cLtm2PP8QY6D>c}iGIW&VgP(PNau;;h`|uMNx8)A9I^C}_stUI z3b|<V$mWBEpa_(J6<{qW2b;k*Pz`FqesCBx z!5(`CTmY9r8@L0{lP>TS^nq7k06`EELPpdPUBn16M{E#B#1-*E{E<*33Q0hcku-#l zNRWJF5mJJzLduaXNHww#X+TaO=a4JNEu<6aLHmsBoeh?eq3PQ5{T){XQb)N;d8RC{Y_3dKp01pyT&jFP`I2(4ioA-2imwV&Wu8if zN`uM`m3~!qRR`5@)lAi;s=HLrsdlT8)GXBe)l$?7)wZggRC}n7shg_%s=@~oMw(@h2{~>4lPW}LW`lr(^{^zU#nf~owk8C zU7M}FM7vh|s`hK0F*-gvY@K4AJvwbV?{ulU{<;~uD|HX+-qXYNto0)GX6tRzJFV9{ zMq`Y}7}l8LG5g2d)<^ZN^`rFX=x^0;)$cbjGzc^h7_2uqY0zt^W$0s=Zn(zqs9}$h zx{;R=*JzE=F{39`O)8zrqn1%mQTvSbjf0FO##@Xp8V{IQn#7tEnCvmRXR2W8YMN@g z#qc;!)ibBkw(7u74->u0YH zZ)0z!ccu3OA5)(cpDLfn^s)4GdJX;AB-$k5q(hTl`?~wi^*!!~`33qF`(5-`^N;l} z_rD)t5|9?KJD@MnDR55U@gPD_NYKil8w>*mi&4$!3w8;f8+-WB~S zW>U=Zm^-mnv69&4IOVvcxazpq@qY2E;yb2{n=*IGg#^6>ZbE$`DKRdwDzSg6|J1cp zyONxeijvx=Sx(EDc5b@fbl&tMGn8jAXBAtO5UEwo0rdKS(;W zy|OE^-^`AieIQ3Qho94$Yn{6!_iTD17)s;19ctC`H1e8;d;_Z~hVgk^AGt zCF7T@U-GdyrTBaat)#T%{ZiJ_^FKNKRQA)SWvR<9E_YtOX$5fwe?|LB@0B}OsjkXf z^>Fp%)pcvAYl_#rTsvd!xzY)xo7X9TB(4x9ri{Q?eKAmF#`8FL_`4{*e96 z2V4)-9yC9=;gI^F;yR=*x32Fn=Ws`TLj9G7;D+W#kH$m4+W)$v$+)TFh~|;yM-`41 z9{qGI?^yqF!SUV`X(t{w&uG4La>~i;ry@^XJRNfS?3sWwCtK(($Ig15Z939<1{oiEm1a=BE0+3j-Um5Eo5UiH3u;+o&JGuIi{FSLcVU1^VP zzj-6+M#s&Ro87l~x1Qh5zCCbf0sM$BzNd1p^uGT6%HPNSzNf>v<4C7pXX}IL2X`K_ z9zOj;^vC!%z4ZJRXqx)v(Ti3TO@8aJ*9+>@}^uFxF*bj{# zLq6X9l=5#uLF7?y;R8kwAtz8?)QDOmdI1njASQfH3-YH+DTV1g?S8xa+i5%9onZp-O(*Sg)9voMjClI2sTH?)4}TlMzDrU{038dBfm0lEdXN6n8z8K zy~W{;rg%SZb1VfJl)`D&j1n>yYFNNQyJjt$NRV}h!>pkmj?tQkGkQ`B$3wV&2f3NR z?shHY`n+CC-f?xbGm+FPvOQ#{AyIK32@yQ`-WM$rZhDp<@LPV6C~+L-M04OLY8S$0 zM;mV!T)&wJt5l}BfrpsA*`_QzMmQ(RU?M}O9j`wqHM^re!Y8K~TlCImL{l4JW0h(k_)1Idy){$#+3894-i|X>cQj6uRZwT z)K$SZj$FbRi>;P%_mpVKN6K7V6INiI2a2g~{C`_swfjCQbi3O7_-a> +@interface FullImageViewController () @property (nonatomic) UIScrollView *scrollView; -@property (nonatomic) UIImageView *imageView; +//@property (nonatomic) UIImageView *imageView; +@property (nonatomic) UIView *imageView; @property (nonatomic) UIButton *shareButton; @@ -63,6 +63,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL areToolbarsHidden; @property (nonatomic, nullable) MPMoviePlayerController *mpVideoPlayer; @property (nonatomic, nullable) AVPlayer *videoPlayer; +@property (nonatomic, nullable) UIButton *playVideoButton; +@property (nonatomic, nullable) PlayerProgressBar *videoProgressBar; +@property (nonatomic, nullable) UIBarButtonItem *videoPlayBarButton; +@property (nonatomic, nullable) UIBarButtonItem *videoPauseBarButton; @property (nonatomic, nullable) NSArray *imageViewConstraints; @property (nonatomic, nullable) NSLayoutConstraint *imageViewBottomConstraint; @@ -201,7 +205,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateMinZoomScale { CGSize viewSize = self.scrollView.bounds.size; - UIImage *image = self.imageView.image; + UIImage *image = self.image; OWSAssert(image); if (image.size.width == 0 || image.size.height == 0) { @@ -212,8 +216,11 @@ NS_ASSUME_NONNULL_BEGIN CGFloat scaleWidth = viewSize.width / image.size.width; CGFloat scaleHeight = viewSize.height / image.size.height; CGFloat minScale = MIN(scaleWidth, scaleHeight); - self.scrollView.minimumZoomScale = minScale; - self.scrollView.zoomScale = minScale; + + if (minScale != self.scrollView.minimumZoomScale) { + self.scrollView.minimumZoomScale = minScale; + self.scrollView.zoomScale = minScale; + } } #pragma mark - Initializers @@ -246,12 +253,7 @@ NS_ASSUME_NONNULL_BEGIN self.imageView = [UIImageView new]; } } else if (self.isVideo) { - [self setupVideoPlayer]; - - // Present the static video preview - UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image]; - self.imageView = imageView; - + self.imageView = [self buildVideoPlayerView]; } else { // Present the static image using standard UIImageView UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image]; @@ -276,34 +278,43 @@ NS_ASSUME_NONNULL_BEGIN [self applyInitialImageViewConstraints]; if (self.isVideo) { - UIButton *playButton = [UIButton new]; + PlayerProgressBar *videoProgressBar = [PlayerProgressBar new]; + videoProgressBar.delegate = self; + videoProgressBar.player = self.videoPlayer; - [playButton addTarget:self action:@selector(playVideo) forControlEvents:UIControlEventTouchUpInside]; + self.videoProgressBar = videoProgressBar; + [self.view addSubview:videoProgressBar]; + [videoProgressBar autoPinWidthToSuperview]; + [videoProgressBar autoPinToTopLayoutGuideOfViewController:self withInset:0]; + CGFloat kVideoProgressBarHeight = 44; + [videoProgressBar autoSetDimension:ALDimensionHeight toSize:kVideoProgressBarHeight]; + + UIButton *playVideoButton = [UIButton new]; + self.playVideoButton = playVideoButton; + + [playVideoButton addTarget:self action:@selector(playVideo) forControlEvents:UIControlEventTouchUpInside]; UIImage *playImage = [UIImage imageNamed:@"play_button"]; - [playButton setBackgroundImage:playImage forState:UIControlStateNormal]; - playButton.contentMode = UIViewContentModeScaleAspectFill; + [playVideoButton setBackgroundImage:playImage forState:UIControlStateNormal]; + playVideoButton.contentMode = UIViewContentModeScaleAspectFill; - [self.view addSubview:playButton]; + [self.view addSubview:playVideoButton]; - CGFloat playButtonWidth = ScaleFromIPhone5(70); - [playButton autoSetDimensionsToSize:CGSizeMake(playButtonWidth, playButtonWidth)]; - [playButton autoCenterInSuperview]; + CGFloat playVideoButtonWidth = ScaleFromIPhone5(70); + [playVideoButton autoSetDimensionsToSize:CGSizeMake(playVideoButtonWidth, playVideoButtonWidth)]; + [playVideoButton autoCenterInSuperview]; } UIToolbar *footerBar = [UIToolbar new]; _footerBar = footerBar; footerBar.barTintColor = [UIColor ows_signalBrandBlueColor]; - [footerBar setItems:@[ - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction - target:self - action:@selector(didPressShare:)], - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash - target:self - action:@selector(didPressDelete:)], - ] - animated:NO]; + self.videoPlayBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay + target:self + action:@selector(didPressPlayBarButton:)]; + self.videoPauseBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause + target:self + action:@selector(didPressPauseBarButton:)]; + [self updateFooterBarButtonItemsWithIsPlayingVideo:YES]; [self.view addSubview:footerBar]; [footerBar autoPinWidthToSuperview]; @@ -311,6 +322,36 @@ NS_ASSUME_NONNULL_BEGIN [footerBar autoSetDimension:ALDimensionHeight toSize:kFooterHeight]; } +- (void)updateFooterBarButtonItemsWithIsPlayingVideo:(BOOL)isPlayingVideo +{ + OWSAssert(self.footerBar); + + NSMutableArray *toolbarItems = [NSMutableArray new]; + + [toolbarItems addObjectsFromArray:@[ + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction + target:self + action:@selector(didPressShare:)], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], + ]]; + + if (self.isVideo) { + UIBarButtonItem *playerButton = isPlayingVideo ? self.videoPauseBarButton : self.videoPlayBarButton; + [toolbarItems addObjectsFromArray:@[ + playerButton, + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil], + ]]; + } + + [toolbarItems addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash + target:self + action:@selector(didPressDelete:)]]; + + [self.footerBar setItems:toolbarItems animated:NO]; +} + - (void)applyInitialImageViewConstraints { if (self.imageViewConstraints.count > 0) { @@ -352,7 +393,7 @@ NS_ASSUME_NONNULL_BEGIN ]]; } -- (void)setupVideoPlayer +- (UIView *)buildVideoPlayerView { NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:[self.attachmentUrl path]]) { @@ -361,12 +402,23 @@ NS_ASSUME_NONNULL_BEGIN if (@available(iOS 9.0, *)) { AVPlayer *player = [[AVPlayer alloc] initWithURL:self.attachmentUrl]; + [player seekToTime:kCMTimeZero]; self.videoPlayer = player; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidPlayToCompletion:) name:AVPlayerItemDidPlayToEndTimeNotification object:player.currentItem]; + + VideoPlayerView *playerView = [VideoPlayerView new]; + playerView.player = player; + + [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow + forConstraints:^{ + [playerView autoSetDimensionsToSize:self.image.size]; + }]; + + return playerView; } else { MPMoviePlayerController *videoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:self.attachmentStream.mediaURL]; @@ -375,6 +427,8 @@ NS_ASSUME_NONNULL_BEGIN videoPlayer.controlStyle = MPMovieControlStyleNone; [videoPlayer prepareToPlay]; + return [[UIImageView alloc] initWithImage:self.image]; + // // [[NSNotificationCenter defaultCenter] addObserver:self // selector:@selector(moviePlayerWillExitFullscreen:) @@ -426,31 +480,17 @@ NS_ASSUME_NONNULL_BEGIN _areToolbarsHidden = areToolbarsHidden; - if (!areToolbarsHidden) { - // Hiding the status bar affects the positioing of the navbar. We don't want to show that in the animation - // so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last. - [[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden withAnimation:UIStatusBarAnimationFade]; - } + // Hiding the status bar affects the positioing of the navbar. We don't want to show that in the animation + // so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last. + [[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden withAnimation:UIStatusBarAnimationNone]; + [self.navigationController setNavigationBarHidden:areToolbarsHidden animated:NO]; + self.videoProgressBar.hidden = areToolbarsHidden; + [UIView animateWithDuration:0.1 - animations:^(void) { - self.view.backgroundColor = areToolbarsHidden ? UIColor.blackColor : UIColor.whiteColor; - self.navigationController.navigationBar.alpha = areToolbarsHidden ? 0 : 1; - self.footerBar.alpha = areToolbarsHidden ? 0 : 1; - } - completion:^(BOOL finished) { - // although navbar has 0 alpha at this point, if we don't also "hide" it, adjusting the status bar - // resets the alpha. - if (areToolbarsHidden) { - // [self.navigationController setNavigationBarHidden:areToolbarsHidden - // animated:NO]; - // Hiding the status bar affects the positioing of the navbar. We don't want to show that in the - // animation so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last. - [[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden - withAnimation:UIStatusBarAnimationNone]; - // position the navbar, but have it be transparent - self.navigationController.navigationBar.alpha = 0; - } - }]; + animations:^(void) { + self.view.backgroundColor = areToolbarsHidden ? UIColor.blackColor : UIColor.whiteColor; + self.footerBar.alpha = areToolbarsHidden ? 0 : 1; + }]; } - (void)initializeGestureRecognizers @@ -490,7 +530,6 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Gesture Recognizers - - (void)didTapDismissButton:(id)sender { [self dismiss]; @@ -605,6 +644,20 @@ NS_ASSUME_NONNULL_BEGIN [self didPressDelete:sender]; } +- (void)didPressPlayBarButton:(id)sender +{ + OWSAssert(self.isVideo); + OWSAssert(self.videoPlayer); + [self playVideo]; +} + +- (void)didPressPauseBarButton:(id)sender +{ + OWSAssert(self.isVideo); + OWSAssert(self.videoPlayer); + [self pauseVideo]; +} + #pragma mark - Presentation - (void)presentFromViewController:(UIViewController *)viewController @@ -724,24 +777,31 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Video Playback - (void)playVideo +{ + OWSAssert(self.videoPlayer); + AVPlayer *player = self.videoPlayer; + + [self updateFooterBarButtonItemsWithIsPlayingVideo:YES]; + self.playVideoButton.hidden = YES; + self.areToolbarsHidden = YES; + + OWSAssert(player.currentItem); + AVPlayerItem *item = player.currentItem; + if (CMTIME_COMPARE_INLINE(item.currentTime, ==, item.duration)) { + // Rewind for repeated plays + [player seekToTime:kCMTimeZero]; + } + + [player play]; +} + +- (void)pauseVideo { OWSAssert(self.isVideo); OWSAssert(self.videoPlayer); - AVPlayerViewController *vc = [AVPlayerViewController new]; - AVPlayer *player = self.videoPlayer; - vc.player = player; - - vc.modalPresentationStyle = UIModalPresentationCustom; - vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - - // Rewind for repeated plays - [player seekToTime:kCMTimeZero]; - [self presentViewController:vc - animated:NO - completion:^(void) { - [player play]; - }]; + [self updateFooterBarButtonItemsWithIsPlayingVideo:NO]; + [self.videoPlayer pause]; } - (void)playerItemDidPlayToCompletion:(NSNotification *)notification @@ -750,9 +810,40 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.videoPlayer); DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - [self dismissViewControllerAnimated:NO completion:nil]; + // [self dismissViewControllerAnimated:NO completion:nil]; + self.areToolbarsHidden = NO; + self.playVideoButton.hidden = NO; + + [self updateFooterBarButtonItemsWithIsPlayingVideo:NO]; } +- (void)playerProgressBarDidStartScrubbing:(PlayerProgressBar *)playerProgressBar +{ + OWSAssert(self.videoPlayer); + [self.videoPlayer pause]; +} + +- (void)playerProgressBar:(PlayerProgressBar *)playerProgressBar scrubbedToTime:(CMTime)time +{ + OWSAssert(self.videoPlayer); + [self.videoPlayer seekToTime:time]; +} + +- (void)playerProgressBar:(PlayerProgressBar *)playerProgressBar + didFinishScrubbingAtTime:(CMTime)time + shouldResumePlayback:(BOOL)shouldResumePlayback +{ + OWSAssert(self.videoPlayer); + [self.videoPlayer seekToTime:time]; + + if (shouldResumePlayback) { + [self.videoPlayer play]; + } +} + + +// iOS8 TODO + - (void)moviePlayerPlaybackStateDidChange:(NSNotification *)notification { DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); diff --git a/Signal/src/views/VideoPlayerView.swift b/Signal/src/views/VideoPlayerView.swift new file mode 100644 index 000000000..107f746a0 --- /dev/null +++ b/Signal/src/views/VideoPlayerView.swift @@ -0,0 +1,200 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@available(iOS 9.0, *) +@objc +public class VideoPlayerView: UIView { + var player: AVPlayer? { + get { + return playerLayer.player + } + set { + playerLayer.player = newValue + } + } + + var playerLayer: AVPlayerLayer { + return layer as! AVPlayerLayer + } + + // Override UIView property + override public static var layerClass: AnyClass { + return AVPlayerLayer.self + } +} + +@available(iOS 9.0, *) +@objc +public protocol PlayerProgressBarDelegate { + func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) + func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) + func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) +} + +@available(iOS 9.0, *) +@objc +public class PlayerProgressBar: UIView { + public let TAG = "[PlayerProgressBar]" + + @objc + public weak var delegate: PlayerProgressBarDelegate? + + private lazy var formatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .positional + formatter.allowedUnits = [.minute, .second ] + formatter.zeroFormattingBehavior = [ .pad ] + + return formatter + }() + + // MARK: Subviews + private let positionLabel = UILabel() + private let remainingLabel = UILabel() + private let slider = UISlider() + private let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + weak private var progressObserver: AnyObject? + + private let kPreferredTimeScale: CMTimeScale = 100 + + public var player: AVPlayer? { + didSet { + guard let item = player?.currentItem else { + owsFail("No player item") + return + } + + slider.minimumValue = 0 + + let duration: CMTime = item.asset.duration + slider.maximumValue = Float(CMTimeGetSeconds(duration)) + + // OPTIMIZE We need a high frequency observer for smooth slider updates, + // but could use a much less frequent observer for label updates + progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] (_) in + self?.updateState() + }) as AnyObject + updateState() + } + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public init(frame: CGRect) { + super.init(frame: frame) + + // Background + backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) + if !UIAccessibilityIsReduceTransparencyEnabled() { + addSubview(blurEffectView) + blurEffectView.autoPinToSuperviewEdges() + } + + // Configure controls + + let kLabelFont = UIFont.monospacedDigitSystemFont(ofSize: 12, weight: UIFontWeightRegular) + positionLabel.font = kLabelFont + remainingLabel.font = kLabelFont + + // We use a smaller thumb for the progress slider. + slider.setThumbImage(#imageLiteral(resourceName: "sliderProgressThumb"), for: .normal) + slider.maximumTrackTintColor = UIColor.ows_black() + slider.minimumTrackTintColor = UIColor.ows_black() + + slider.addTarget(self, action: #selector(handleSliderTouchDown), for: .touchDown) + slider.addTarget(self, action: #selector(handleSliderTouchUp), for: .touchUpInside) + slider.addTarget(self, action: #selector(handleSliderTouchUp), for: .touchUpOutside) + slider.addTarget(self, action: #selector(handleSliderValueChanged), for: .valueChanged) + + // Layout Subviews + + addSubview(positionLabel) + addSubview(remainingLabel) + addSubview(slider) + + positionLabel.autoPinEdge(toSuperviewMargin: .leading) + positionLabel.autoVCenterInSuperview() + + let kSliderMargin: CGFloat = 8 + + slider.autoPinEdge(.leading, to: .trailing, of: positionLabel, withOffset: kSliderMargin) + slider.autoVCenterInSuperview() + + remainingLabel.autoPinEdge(.leading, to: .trailing, of: slider, withOffset: kSliderMargin) + remainingLabel.autoPinEdge(toSuperviewMargin: .trailing) + remainingLabel.autoVCenterInSuperview() + } + + // MARK: Gesture handling + + var wasPlayingWhenScrubbingStarted: Bool = false + + @objc + private func handleSliderTouchDown(_ slider: UISlider) { + guard let player = self.player else { + owsFail("player was nil") + return + } + + self.wasPlayingWhenScrubbingStarted = (player.rate != 0) && (player.error == nil) + + self.delegate?.playerProgressBarDidStartScrubbing(self) + } + + @objc + private func handleSliderTouchUp(_ slider: UISlider) { + let sliderTime = time(slider: slider) + self.delegate?.playerProgressBar(self, didFinishScrubbingAtTime: sliderTime, shouldResumePlayback:wasPlayingWhenScrubbingStarted) + } + + @objc + private func handleSliderValueChanged(_ slider: UISlider) { + let sliderTime = time(slider: slider) + self.delegate?.playerProgressBar(self, scrubbedToTime: sliderTime) + } + + // MARK: Render cycle + + private func updateState() { + guard let player = player else { + owsFail("\(TAG) player isn't set.") + return + } + + guard let item = player.currentItem else { + owsFail("\(TAG) player has no item.") + return + } + + let position = player.currentTime() + let positionSeconds: Float64 = CMTimeGetSeconds(position) + positionLabel.text = formatter.string(from: positionSeconds) + + let duration: CMTime = item.asset.duration + let remainingTime = duration - position + let remainingSeconds = CMTimeGetSeconds(remainingTime) + + guard let remainingString = formatter.string(from: remainingSeconds) else { + owsFail("unable to format time remaining") + remainingLabel.text = "0:00" + return + } + + // show remaining time as negative + remainingLabel.text = "-\(remainingString)" + + slider.setValue(Float(positionSeconds), animated: false) + } + + // MARK: Util + + private func time(slider: UISlider) -> CMTime { + let seconds: Double = Double(slider.value) + return CMTime(seconds: seconds, preferredTimescale: kPreferredTimeScale) + } +} From c7c433c59ce9e711ffb99dbce6ba8af476451c23 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Sun, 7 Jan 2018 17:26:28 -0500 Subject: [PATCH 04/23] iOS8 compatability for video player --- .../ViewControllers/FullImageViewController.m | 216 ++++-------------- 1 file changed, 50 insertions(+), 166 deletions(-) diff --git a/Signal/src/ViewControllers/FullImageViewController.m b/Signal/src/ViewControllers/FullImageViewController.m index 79bb9f0cb..d3ab7454c 100644 --- a/Signal/src/ViewControllers/FullImageViewController.m +++ b/Signal/src/ViewControllers/FullImageViewController.m @@ -12,6 +12,7 @@ #import "UIUtil.h" #import "UIView+OWS.h" #import +#import #import #import #import @@ -61,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIToolbar *footerBar; @property (nonatomic) BOOL areToolbarsHidden; -@property (nonatomic, nullable) MPMoviePlayerController *mpVideoPlayer; + @property (nonatomic, nullable) AVPlayer *videoPlayer; @property (nonatomic, nullable) UIButton *playVideoButton; @property (nonatomic, nullable) PlayerProgressBar *videoProgressBar; @@ -278,16 +279,18 @@ NS_ASSUME_NONNULL_BEGIN [self applyInitialImageViewConstraints]; if (self.isVideo) { - PlayerProgressBar *videoProgressBar = [PlayerProgressBar new]; - videoProgressBar.delegate = self; - videoProgressBar.player = self.videoPlayer; + if (@available(iOS 9, *)) { + PlayerProgressBar *videoProgressBar = [PlayerProgressBar new]; + videoProgressBar.delegate = self; + videoProgressBar.player = self.videoPlayer; - self.videoProgressBar = videoProgressBar; - [self.view addSubview:videoProgressBar]; - [videoProgressBar autoPinWidthToSuperview]; - [videoProgressBar autoPinToTopLayoutGuideOfViewController:self withInset:0]; - CGFloat kVideoProgressBarHeight = 44; - [videoProgressBar autoSetDimension:ALDimensionHeight toSize:kVideoProgressBarHeight]; + self.videoProgressBar = videoProgressBar; + [self.view addSubview:videoProgressBar]; + [videoProgressBar autoPinWidthToSuperview]; + [videoProgressBar autoPinToTopLayoutGuideOfViewController:self withInset:0]; + CGFloat kVideoProgressBarHeight = 44; + [videoProgressBar autoSetDimension:ALDimensionHeight toSize:kVideoProgressBarHeight]; + } UIButton *playVideoButton = [UIButton new]; self.playVideoButton = playVideoButton; @@ -336,13 +339,16 @@ NS_ASSUME_NONNULL_BEGIN ]]; if (self.isVideo) { - UIBarButtonItem *playerButton = isPlayingVideo ? self.videoPauseBarButton : self.videoPlayBarButton; - [toolbarItems addObjectsFromArray:@[ - playerButton, - [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace - target:nil - action:nil], - ]]; + // bar button video controls only work on iOS9+ + if (@available(iOS 9.0, *)) { + UIBarButtonItem *playerButton = isPlayingVideo ? self.videoPauseBarButton : self.videoPlayBarButton; + [toolbarItems addObjectsFromArray:@[ + playerButton, + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil], + ]]; + } } [toolbarItems addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash @@ -420,55 +426,7 @@ NS_ASSUME_NONNULL_BEGIN return playerView; } else { - MPMoviePlayerController *videoPlayer = - [[MPMoviePlayerController alloc] initWithContentURL:self.attachmentStream.mediaURL]; - self.mpVideoPlayer = videoPlayer; - - videoPlayer.controlStyle = MPMovieControlStyleNone; - [videoPlayer prepareToPlay]; - return [[UIImageView alloc] initWithImage:self.image]; - - // - // [[NSNotificationCenter defaultCenter] addObserver:self - // selector:@selector(moviePlayerWillExitFullscreen:) - // name:MPMoviePlayerWillExitFullscreenNotification - // object:videoPlayer]; - // [[NSNotificationCenter defaultCenter] addObserver:self - // selector:@selector(moviePlayerDidExitFullscreen:) - // name:MPMoviePlayerDidExitFullscreenNotification - // object:videoPlayer]; - // [[NSNotificationCenter defaultCenter] addObserver:self - // selector:@selector(moviePlayerWillEnterFullscreen:) - // name:MPMoviePlayerWillEnterFullscreenNotification - // object:videoPlayer]; - // [[NSNotificationCenter defaultCenter] addObserver:self - // selector:@selector(moviePlayerPlaybackStateDidChange:) - // name:MPMoviePlayerPlaybackStateDidChangeNotification - // object:videoPlayer]; - // - // [[NSNotificationCenter defaultCenter] addObserver:self - // selector:@selector(moviePlayerDidEnterFullscreen:) - // name:MPMoviePlayerDidEnterFullscreenNotification - // object:videoPlayer]; - // - // - // [[NSNotificationCenter defaultCenter] addObserver:self - // selector:@selector(moviePlayerDidFinishPlayback:) - // name:MPMoviePlayerPlaybackDidFinishNotification - // object:videoPlayer]; - // - // // Don't show any controls intially. We switch control style after the view is fullscreen to make them - // appear upon tapping. - //// videoPlayer.controlStyle = MPMovieControlStyleFullscreen; - // videoPlayer.shouldAutoplay = YES; - // - // // We can't animate from the cell media frame; - // // MPMoviePlayerController will animate a crop of its - // // contents rather than scaling them. - // videoPlayer.view.frame = self.view.bounds; - // - // self.imageView = videoPlayer.view; } } @@ -778,21 +736,26 @@ NS_ASSUME_NONNULL_BEGIN - (void)playVideo { - OWSAssert(self.videoPlayer); - AVPlayer *player = self.videoPlayer; + if (@available(iOS 9, *)) { + OWSAssert(self.videoPlayer); + AVPlayer *player = self.videoPlayer; - [self updateFooterBarButtonItemsWithIsPlayingVideo:YES]; - self.playVideoButton.hidden = YES; - self.areToolbarsHidden = YES; + [self updateFooterBarButtonItemsWithIsPlayingVideo:YES]; + self.playVideoButton.hidden = YES; + self.areToolbarsHidden = YES; - OWSAssert(player.currentItem); - AVPlayerItem *item = player.currentItem; - if (CMTIME_COMPARE_INLINE(item.currentTime, ==, item.duration)) { - // Rewind for repeated plays - [player seekToTime:kCMTimeZero]; + OWSAssert(player.currentItem); + AVPlayerItem *item = player.currentItem; + if (CMTIME_COMPARE_INLINE(item.currentTime, ==, item.duration)) { + // Rewind for repeated plays + [player seekToTime:kCMTimeZero]; + } + + [player play]; + } else { + [self legacyPlayVideo]; + return; } - - [player play]; } - (void)pauseVideo @@ -841,100 +804,21 @@ NS_ASSUME_NONNULL_BEGIN } } +#pragma mark iOS8 Video Playback -// iOS8 TODO - -- (void)moviePlayerPlaybackStateDidChange:(NSNotification *)notification +// AVPlayer was introduced in iOS9, so on iOS8 we fall back to MPMoviePlayer +// This causes an unforutnate "double present" since we present the full screen view and then the MPMovie view over top. +// And similarly a double dismiss. +- (void)legacyPlayVideo { - DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - OWSAssert(self.mpVideoPlayer); -} - -- (void)moviePlayerWillEnterFullscreen:(NSNotification *)notification -{ - DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - OWSAssert(self.videoPlayer); - self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone; -} - -- (void)moviePlayerDidEnterFullscreen:(NSNotification *)notification -{ - DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - OWSAssert(self.videoPlayer); - self.mpVideoPlayer.controlStyle = MPMovieControlStyleFullscreen; -} - -// There's more than one way to exit the fullscreen video playback. -// There's a done button, a "toggle fullscreen" button and I think -// there's some gestures too. These fire slightly different notifications. -// We want to hide & clean up the video player immediately in all of -// these cases. -- (void)moviePlayerWillExitFullscreen:(NSNotification *)notification -{ - DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - - // If we didn't just complete playback, user chose to exit fullscreen. - // In that case, we dismiss the view controller since the user is probably done. - // if (!self.didJustCompleteVideoPlayback) { - // [self dismiss]; - // } - - // self.didJustCompleteVideoPlayback = NO; - self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone; - - // [self clearVideoPlayer]; -} - -// See comment on moviePlayerWillExitFullscreen: -- (void)moviePlayerDidExitFullscreen:(NSNotification *)notification -{ - DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - self.mpVideoPlayer.controlStyle = MPMovieControlStyleEmbedded; - // [self clearVideoPlayer]; -} - -- (void)moviePlayerDidFinishPlayback:(NSNotification *)notification -{ - OWSAssert(self.videoPlayer); - - NSNumber *reason = notification.userInfo[MPMoviePlayerPlaybackDidFinishReasonUserInfoKey]; - DDLogDebug(@"%@ movie player finished with reason %@", self.logTag, reason); - OWSAssert(reason); - - switch (reason.integerValue) { - case MPMovieFinishReasonPlaybackEnded: { - DDLogDebug(@"%@ video played to completion.", self.logTag); - self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone; - [self.mpVideoPlayer setFullscreen:NO animated:YES]; - break; - } - case MPMovieFinishReasonPlaybackError: { - DDLogDebug(@"%@ error playing video.", self.logTag); - break; - } - case MPMovieFinishReasonUserExited: { - // FIXME: unable to fire this (only tried on iOS11.2 so far) - DDLogDebug(@"%@ user exited video playback", self.logTag); - [self dismiss]; - break; - } + if (@available(iOS 9.0, *)) { + OWSFail(@"legacy video is for iOS8 only"); } + MPMoviePlayerViewController *vc = [[MPMoviePlayerViewController alloc] initWithContentURL:self.attachmentUrl]; + + [self presentViewController:vc animated:YES completion:nil]; } -//- (void)clearVideoPlayer -//{ -// [self.videoPlayer stop]; -// [self.videoPlayer.view removeFromSuperview]; -// self.videoPlayer = nil; -//} - -//- (void)setVideoPlayer:(MPMoviePlayerController *_Nullable)videoPlayer -//{ -// _mpVideoPlayer = mpVideoPlayer; -// -// [ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:videoPlayer != nil]; -//} - #pragma mark - Saving images to Camera Roll - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { From 8454e512d8448e02175e2330b3b29921249b8a68 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Sun, 7 Jan 2018 17:35:52 -0500 Subject: [PATCH 05/23] Use FullSreen media VC for message details // FREEBIE --- .../ViewControllers/FullImageViewController.m | 165 +++++++++++++----- .../ViewControllers/MediaMessageView.swift | 75 ++++---- .../MessageDetailViewController.swift | 6 +- 3 files changed, 152 insertions(+), 94 deletions(-) diff --git a/Signal/src/ViewControllers/FullImageViewController.m b/Signal/src/ViewControllers/FullImageViewController.m index d3ab7454c..0ab8b8a42 100644 --- a/Signal/src/ViewControllers/FullImageViewController.m +++ b/Signal/src/ViewControllers/FullImageViewController.m @@ -4,6 +4,7 @@ #import "FullImageViewController.h" #import "AttachmentSharing.h" +#import "ConversationViewController.h" #import "ConversationViewItem.h" #import "Signal-Swift.h" #import "TSAttachmentStream.h" @@ -19,8 +20,6 @@ NS_ASSUME_NONNULL_BEGIN -#define kMaxZoomScale 8.0f - // In order to use UIMenuController, the view from which it is // presented must have certain custom behaviors. @interface AttachmentMenuView : UIView @@ -132,7 +131,11 @@ NS_ASSUME_NONNULL_BEGIN if (self.attachmentStream) { return self.attachmentStream.image; } else if (self.attachment) { - return self.attachment.image; + if (self.isVideo) { + return self.attachment.videoPreview; + } else { + return self.attachment.image; + } } else { return nil; } @@ -220,6 +223,7 @@ NS_ASSUME_NONNULL_BEGIN if (minScale != self.scrollView.minimumZoomScale) { self.scrollView.minimumZoomScale = minScale; + self.scrollView.maximumZoomScale = minScale * 8; self.scrollView.zoomScale = minScale; } } @@ -235,8 +239,6 @@ NS_ASSUME_NONNULL_BEGIN self.scrollView = scrollView; scrollView.delegate = self; - // TODO set max based on MIN. - scrollView.maximumZoomScale = kMaxZoomScale; scrollView.showsVerticalScrollIndicator = NO; scrollView.showsHorizontalScrollIndicator = NO; scrollView.decelerationRate = UIScrollViewDecelerationRateFast; @@ -247,7 +249,7 @@ NS_ASSUME_NONNULL_BEGIN if (self.isAnimated) { if ([self.fileData ows_isValidImage]) { YYImage *animatedGif = [YYImage imageWithData:self.fileData]; - YYAnimatedImageView *animatedView = [[YYAnimatedImageView alloc] init]; + YYAnimatedImageView *animatedView = [YYAnimatedImageView new]; animatedView.image = animatedGif; self.imageView = animatedView; } else { @@ -308,21 +310,27 @@ NS_ASSUME_NONNULL_BEGIN [playVideoButton autoCenterInSuperview]; } - UIToolbar *footerBar = [UIToolbar new]; - _footerBar = footerBar; - footerBar.barTintColor = [UIColor ows_signalBrandBlueColor]; - self.videoPlayBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay - target:self - action:@selector(didPressPlayBarButton:)]; - self.videoPauseBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause - target:self - action:@selector(didPressPauseBarButton:)]; - [self updateFooterBarButtonItemsWithIsPlayingVideo:YES]; - [self.view addSubview:footerBar]; - [footerBar autoPinWidthToSuperview]; - [footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0]; - [footerBar autoSetDimension:ALDimensionHeight toSize:kFooterHeight]; + // Don't show footer bar after tapping approval-view + if (self.viewItem) { + UIToolbar *footerBar = [UIToolbar new]; + _footerBar = footerBar; + footerBar.barTintColor = [UIColor ows_signalBrandBlueColor]; + self.videoPlayBarButton = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay + target:self + action:@selector(didPressPlayBarButton:)]; + self.videoPauseBarButton = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause + target:self + action:@selector(didPressPauseBarButton:)]; + [self updateFooterBarButtonItemsWithIsPlayingVideo:YES]; + [self.view addSubview:footerBar]; + + [footerBar autoPinWidthToSuperview]; + [footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0]; + [footerBar autoSetDimension:ALDimensionHeight toSize:kFooterHeight]; + } } - (void)updateFooterBarButtonItemsWithIsPlayingVideo:(BOOL)isPlayingVideo @@ -438,8 +446,8 @@ NS_ASSUME_NONNULL_BEGIN _areToolbarsHidden = areToolbarsHidden; - // Hiding the status bar affects the positioing of the navbar. We don't want to show that in the animation - // so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last. + // Hiding the status bar affects the positioing of the navbar. We don't want to show that in an animation, it's + // better to just have everythign "flit" in/out. [[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden withAnimation:UIStatusBarAnimationNone]; [self.navigationController setNavigationBarHidden:areToolbarsHidden animated:NO]; self.videoProgressBar.hidden = areToolbarsHidden; @@ -490,7 +498,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)didTapDismissButton:(id)sender { - [self dismiss]; + [self dismissSelfAnimated:YES completion:nil]; } - (void)didTapImage:(id)sender @@ -519,7 +527,7 @@ NS_ASSUME_NONNULL_BEGIN return; } - [self dismiss]; + [self dismissSelfAnimated:YES completion:nil]; } - (void)longPressGesture:(UIGestureRecognizer *)sender { @@ -551,23 +559,56 @@ NS_ASSUME_NONNULL_BEGIN - (void)didPressShare:(id)sender { - DDLogInfo(@"%@: sharing image.", self.logTag); + DDLogInfo(@"%@: didPressShare", self.logTag); + if (!self.viewItem) { + OWSFail(@"share should only be available when a viewItem is present"); + return; + } [self.viewItem shareAction]; } - (void)didPressDelete:(id)sender { - DDLogInfo(@"%@: sharing image.", self.logTag); + DDLogInfo(@"%@: didPressDelete", self.logTag); + if (!self.viewItem) { + OWSFail(@"delete should only be available when a viewItem is present"); + return; + } UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", nil) - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction *action) { - [self.viewItem deleteAction]; - [self dismiss]; - }]]; + + [actionSheet + addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", nil) + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + OWSAssert([self.presentingViewController + isKindOfClass:[UINavigationController class]]); + UINavigationController *navController + = (UINavigationController *)self.presentingViewController; + + if ([navController.topViewController + isKindOfClass:[ConversationViewController class]]) { + [self dismissSelfAnimated:YES + completion:^{ + [self.viewItem deleteAction]; + }]; + } else if ([navController.topViewController + isKindOfClass:[MessageDetailViewController class]]) { + [self dismissSelfAnimated:NO + completion:^{ + [self.viewItem deleteAction]; + }]; + [navController popViewControllerAnimated:YES]; + } else { + OWSFail(@"Unexpected presentation context."); + [self dismissSelfAnimated:YES + completion:^{ + [self.viewItem deleteAction]; + }]; + } + }]]; [actionSheet addAction:[OWSAlerts cancelAction]]; @@ -576,6 +617,10 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender { + if (self.viewItem == nil) { + return NO; + } + if (action == self.viewItem.metadataActionSelector) { return NO; } @@ -584,21 +629,41 @@ NS_ASSUME_NONNULL_BEGIN - (void)copyAction:(nullable id)sender { + if (!self.viewItem) { + OWSFail(@"copy should only be available when a viewItem is present"); + return; + } + [self.viewItem copyAction]; } - (void)shareAction:(nullable id)sender { - [self.viewItem shareAction]; + if (!self.viewItem) { + OWSFail(@"share should only be available when a viewItem is present"); + return; + } + + [self didPressShare:sender]; } - (void)saveAction:(nullable id)sender { + if (!self.viewItem) { + OWSFail(@"save should only be available when a viewItem is present"); + return; + } + [self.viewItem saveAction]; } - (void)deleteAction:(nullable id)sender { + if (!self.viewItem) { + OWSFail(@"delete should only be available when a viewItem is present"); + return; + } + [self didPressDelete:sender]; } @@ -674,7 +739,7 @@ NS_ASSUME_NONNULL_BEGIN }]; } -- (void)dismiss +- (void)dismissSelfAnimated:(BOOL)isAnimated completion:(void (^_Nullable)(void))completion { self.view.userInteractionEnabled = NO; [UIApplication sharedApplication].statusBarHidden = NO; @@ -686,21 +751,26 @@ NS_ASSUME_NONNULL_BEGIN // Move the image view pack to it's initial position, i.e. where // it sits on the screen in the conversation view. [self applyInitialImageViewConstraints]; - [UIView animateWithDuration:0.2 - delay:0.0 - options:UIViewAnimationOptionCurveEaseInOut - animations:^(void) { - [self.imageView.superview layoutIfNeeded]; - // In case user has hidden bars, which changes background to black. - self.view.backgroundColor = UIColor.whiteColor; + if (isAnimated) { + [UIView animateWithDuration:0.2 + delay:0.0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^(void) { + [self.imageView.superview layoutIfNeeded]; - // fade out content and toolbars - self.navigationController.view.alpha = 0.0; - } - completion:^(BOOL finished) { - [self.presentingViewController dismissViewControllerAnimated:NO completion:nil]; - }]; + // In case user has hidden bars, which changes background to black. + self.view.backgroundColor = UIColor.whiteColor; + + // fade out content and toolbars + self.navigationController.view.alpha = 0.0; + } + completion:^(BOOL finished) { + [self.presentingViewController dismissViewControllerAnimated:NO completion:completion]; + }]; + } else { + [self.presentingViewController dismissViewControllerAnimated:NO completion:completion]; + } } #pragma mark - UIScrollViewDelegate @@ -773,7 +843,6 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.videoPlayer); DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); - // [self dismissViewControllerAnimated:NO completion:nil]; self.areToolbarsHidden = NO; self.playVideoButton.hidden = NO; diff --git a/Signal/src/ViewControllers/MediaMessageView.swift b/Signal/src/ViewControllers/MediaMessageView.swift index 5f4fdd20b..438ba42e5 100644 --- a/Signal/src/ViewControllers/MediaMessageView.swift +++ b/Signal/src/ViewControllers/MediaMessageView.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @@ -19,6 +19,11 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { let mode: MediaMessageViewMode + // If this is for a persisted interaction, viewItem and attachmentStream will not be nil + // If this is for an attachmet draft, before sending, viewItem and attachmentStream will be nil + var viewItem: ConversationViewItem? + var attachmentStream: TSAttachmentStream? + let attachment: SignalAttachment var videoPlayer: MPMoviePlayerController? @@ -45,10 +50,16 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { fatalError("\(#function) is unimplemented.") } - required init(attachment: SignalAttachment, mode: MediaMessageViewMode) { + convenience init(attachment: SignalAttachment, mode: MediaMessageViewMode) { + self.init(attachment: attachment, mode: mode, viewItem: nil, attachmentStream: nil) + } + + required init(attachment: SignalAttachment, mode: MediaMessageViewMode, viewItem: ConversationViewItem?, attachmentStream: TSAttachmentStream?) { assert(!attachment.hasError) - self.mode = mode self.attachment = attachment + self.mode = mode + self.viewItem = viewItem + self.attachmentStream = attachmentStream super.init(frame: CGRect.zero) createViews() @@ -401,13 +412,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { guard let fromView = sender.view else { return } - guard let fromViewController = fromViewController() else { - return - } - let window = UIApplication.shared.keyWindow - let convertedRect = fromView.convert(fromView.bounds, to:window) - let viewController = FullImageViewController(attachment:attachment, from:convertedRect) - viewController.present(from:fromViewController) + showFullImageViewController(fromView: fromView) } private func fromViewController() -> UIViewController? { @@ -429,44 +434,28 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { guard sender.state == .recognized else { return } - guard let dataUrl = attachment.dataUrl else { + guard let fromView = sender.view else { return } - guard let videoPlayer = MPMoviePlayerController(contentURL: dataUrl) else { + showFullImageViewController(fromView: fromView) + } + + func showFullImageViewController(fromView: UIView) { + guard let fromViewController = fromViewController() else { return } - videoPlayer.prepareToPlay() + let window = UIApplication.shared.keyWindow + let convertedRect = fromView.convert(fromView.bounds, to:window) - NotificationCenter.default.addObserver(forName: .MPMoviePlayerWillExitFullscreen, object: nil, queue: nil) { [weak self] _ in - self?.moviePlayerWillExitFullscreen() - } - NotificationCenter.default.addObserver(forName: .MPMoviePlayerDidExitFullscreen, object: nil, queue: nil) { [weak self] _ in - self?.moviePlayerDidExitFullscreen() - } + let viewController: FullImageViewController = { + if let viewItem = self.viewItem, let attachmentStream = self.attachmentStream { + return FullImageViewController(attachmentStream: attachmentStream, from: convertedRect, viewItem: viewItem) + } else { + // e.g. when MediaMessageView does not belong to a persisted interaction, e.g. approval view. + return FullImageViewController(attachment: attachment, from:convertedRect) + } + }() - videoPlayer.controlStyle = .default - videoPlayer.shouldAutoplay = true - - self.addSubview(videoPlayer.view) - videoPlayer.view.frame = self.bounds - self.videoPlayer = videoPlayer - videoPlayer.view.autoPinToSuperviewEdges() - ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(true) - videoPlayer.setFullscreen(true, animated:false) - } - - private func moviePlayerWillExitFullscreen() { - clearVideoPlayer() - } - - private func moviePlayerDidExitFullscreen() { - clearVideoPlayer() - } - - private func clearVideoPlayer() { - videoPlayer?.stop() - videoPlayer?.view.removeFromSuperview() - videoPlayer = nil - ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(false) + viewController.present(from:fromViewController) } } diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index d9e0ec686..b6eb06037 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @@ -406,7 +406,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate { } guard let attachment = TSAttachment.fetch(uniqueId: attachmentId) else { - owsFail("Missing attachment") + Logger.warn("\(TAG) Missing attachment. Was it deleted?") return rows } self.attachment = attachment @@ -433,7 +433,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate { let contentType = attachment.contentType if let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) { let attachment = SignalAttachment(dataSource: dataSource, dataUTI: dataUTI) - let mediaMessageView = MediaMessageView(attachment: attachment, mode: .small) + let mediaMessageView = MediaMessageView(attachment: attachment, mode: .small, viewItem: viewItem, attachmentStream: attachmentStream) mediaMessageView.backgroundColor = UIColor.white self.mediaMessageView = mediaMessageView rows.append(mediaMessageView) From 412fe2735eeaaa4f7b7427f1f325f9156f4bd829 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Sun, 7 Jan 2018 23:33:25 -0500 Subject: [PATCH 06/23] Rename FullImageViewController -> MediaDetailViewController // FREEBIE --- Signal.xcodeproj/project.pbxproj | 12 +-- Signal/src/Signal-Bridging-Header.h | 4 +- .../ConversationViewController.m | 6 +- ...ntroller.h => MediaDetailViewController.h} | 4 +- ...ntroller.m => MediaDetailViewController.m} | 91 +++++++++---------- .../ViewControllers/MediaMessageView.swift | 12 +-- 6 files changed, 64 insertions(+), 65 deletions(-) rename Signal/src/ViewControllers/{FullImageViewController.h => MediaDetailViewController.h} (83%) rename Signal/src/ViewControllers/{FullImageViewController.m => MediaDetailViewController.m} (92%) diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 416be1250..ec40b7684 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -44,7 +44,7 @@ 34B3F8791E8DF1700035BE1A /* CountryCodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8411E8DF1700035BE1A /* CountryCodeViewController.m */; }; 34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8441E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift */; }; 34B3F87C1E8DF1700035BE1A /* FingerprintViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8461E8DF1700035BE1A /* FingerprintViewController.m */; }; - 34B3F87D1E8DF1700035BE1A /* FullImageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8481E8DF1700035BE1A /* FullImageViewController.m */; }; + 34B3F87D1E8DF1700035BE1A /* MediaDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8481E8DF1700035BE1A /* MediaDetailViewController.m */; }; 34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */; }; 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */; }; 34B3F8811E8DF1700035BE1A /* LockInteractionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F84E1E8DF1700035BE1A /* LockInteractionController.m */; }; @@ -467,8 +467,8 @@ 34B3F8441E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradesPageViewController.swift; sourceTree = ""; }; 34B3F8451E8DF1700035BE1A /* FingerprintViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FingerprintViewController.h; sourceTree = ""; }; 34B3F8461E8DF1700035BE1A /* FingerprintViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FingerprintViewController.m; sourceTree = ""; }; - 34B3F8471E8DF1700035BE1A /* FullImageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FullImageViewController.h; sourceTree = ""; }; - 34B3F8481E8DF1700035BE1A /* FullImageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FullImageViewController.m; sourceTree = ""; }; + 34B3F8471E8DF1700035BE1A /* MediaDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaDetailViewController.h; sourceTree = ""; }; + 34B3F8481E8DF1700035BE1A /* MediaDetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaDetailViewController.m; sourceTree = ""; }; 34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InboxTableViewCell.h; sourceTree = ""; }; 34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InboxTableViewCell.m; sourceTree = ""; }; 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InviteFlow.swift; sourceTree = ""; }; @@ -1029,8 +1029,8 @@ 34B3F8461E8DF1700035BE1A /* FingerprintViewController.m */, 34E8BF361EE9E2FD00F5F4CA /* FingerprintViewScanController.h */, 34E8BF371EE9E2FD00F5F4CA /* FingerprintViewScanController.m */, - 34B3F8471E8DF1700035BE1A /* FullImageViewController.h */, - 34B3F8481E8DF1700035BE1A /* FullImageViewController.m */, + 34B3F8471E8DF1700035BE1A /* MediaDetailViewController.h */, + 34B3F8481E8DF1700035BE1A /* MediaDetailViewController.m */, 34BECE2C1F7ABCE000D7438D /* GifPicker */, 34B3F86F1E8DF1700035BE1A /* HomeViewController.h */, 34B3F8701E8DF1700035BE1A /* HomeViewController.m */, @@ -2389,7 +2389,7 @@ 45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */, 34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */, 45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */, - 34B3F87D1E8DF1700035BE1A /* FullImageViewController.m in Sources */, + 34B3F87D1E8DF1700035BE1A /* MediaDetailViewController.m in Sources */, 45666F7B1D9C0533008FE134 /* OWSDatabaseMigration.m in Sources */, 34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */, B90418E6183E9DD40038554A /* DateUtil.m in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 9b955a4b6..e6808cc49 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "AppSettingsViewController.h" @@ -10,7 +10,7 @@ #import "DebugUIPage.h" #import "Environment.h" #import "FingerprintViewController.h" -#import "FullImageViewController.h" +#import "MediaDetailViewController.h" #import "HomeViewController.h" #import "NSString+OWS.h" #import "NotificationsManager.h" diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 07ff935cb..11de97caf 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -19,7 +19,7 @@ #import "DebugUITableViewController.h" #import "Environment.h" #import "FingerprintViewController.h" -#import "FullImageViewController.h" +#import "MediaDetailViewController.h" #import "NSAttributedString+OWS.h" #import "NSString+OWS.h" #import "NewGroupViewController.h" @@ -2002,7 +2002,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { UIWindow *window = [UIApplication sharedApplication].keyWindow; CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window]; - FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream + MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream fromRect:convertedRect viewItem:viewItem]; [vc presentFromViewController:self]; @@ -2020,7 +2020,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { UIWindow *window = [UIApplication sharedApplication].keyWindow; CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window]; - FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream + MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream fromRect:convertedRect viewItem:viewItem]; [vc presentFromViewController:self]; diff --git a/Signal/src/ViewControllers/FullImageViewController.h b/Signal/src/ViewControllers/MediaDetailViewController.h similarity index 83% rename from Signal/src/ViewControllers/FullImageViewController.h rename to Signal/src/ViewControllers/MediaDetailViewController.h index 6ade09366..e93fa7e96 100644 --- a/Signal/src/ViewControllers/FullImageViewController.h +++ b/Signal/src/ViewControllers/MediaDetailViewController.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSViewController.h" @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN @class SignalAttachment; @class TSAttachmentStream; -@interface FullImageViewController : OWSViewController +@interface MediaDetailViewController : OWSViewController // If viewItem is non-null, long press will show a menu controller. - (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream diff --git a/Signal/src/ViewControllers/FullImageViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m similarity index 92% rename from Signal/src/ViewControllers/FullImageViewController.m rename to Signal/src/ViewControllers/MediaDetailViewController.m index 0ab8b8a42..99b7c70dc 100644 --- a/Signal/src/ViewControllers/FullImageViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -2,7 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import "FullImageViewController.h" +#import "MediaDetailViewController.h" #import "AttachmentSharing.h" #import "ConversationViewController.h" #import "ConversationViewItem.h" @@ -44,11 +44,10 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - -@interface FullImageViewController () +@interface MediaDetailViewController () @property (nonatomic) UIScrollView *scrollView; -//@property (nonatomic) UIImageView *imageView; -@property (nonatomic) UIView *imageView; +@property (nonatomic) UIView *mediaView; @property (nonatomic) UIButton *shareButton; @@ -69,14 +68,14 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) UIBarButtonItem *videoPauseBarButton; @property (nonatomic, nullable) NSArray *imageViewConstraints; -@property (nonatomic, nullable) NSLayoutConstraint *imageViewBottomConstraint; -@property (nonatomic, nullable) NSLayoutConstraint *imageViewLeadingConstraint; -@property (nonatomic, nullable) NSLayoutConstraint *imageViewTopConstraint; -@property (nonatomic, nullable) NSLayoutConstraint *imageViewTrailingConstraint; +@property (nonatomic, nullable) NSLayoutConstraint *mediaViewBottomConstraint; +@property (nonatomic, nullable) NSLayoutConstraint *mediaViewLeadingConstraint; +@property (nonatomic, nullable) NSLayoutConstraint *mediaViewTopConstraint; +@property (nonatomic, nullable) NSLayoutConstraint *mediaViewTrailingConstraint; @end -@implementation FullImageViewController +@implementation MediaDetailViewController - (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream fromRect:(CGRect)rect @@ -251,32 +250,32 @@ NS_ASSUME_NONNULL_BEGIN YYImage *animatedGif = [YYImage imageWithData:self.fileData]; YYAnimatedImageView *animatedView = [YYAnimatedImageView new]; animatedView.image = animatedGif; - self.imageView = animatedView; + self.mediaView = animatedView; } else { - self.imageView = [UIImageView new]; + self.mediaView = [UIImageView new]; } } else if (self.isVideo) { - self.imageView = [self buildVideoPlayerView]; + self.mediaView = [self buildVideoPlayerView]; } else { // Present the static image using standard UIImageView UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image]; - self.imageView = imageView; + self.mediaView = imageView; } - OWSAssert(self.imageView); + OWSAssert(self.mediaView); - [scrollView addSubview:self.imageView]; - self.imageView.contentMode = UIViewContentModeScaleAspectFit; - self.imageView.userInteractionEnabled = YES; - self.imageView.clipsToBounds = YES; - self.imageView.layer.allowsEdgeAntialiasing = YES; - self.imageView.translatesAutoresizingMaskIntoConstraints = NO; + [scrollView addSubview:self.mediaView]; + self.mediaView.contentMode = UIViewContentModeScaleAspectFit; + self.mediaView.userInteractionEnabled = YES; + self.mediaView.clipsToBounds = YES; + self.mediaView.layer.allowsEdgeAntialiasing = YES; + self.mediaView.translatesAutoresizingMaskIntoConstraints = NO; // Use trilinear filters for better scaling quality at // some performance cost. - self.imageView.layer.minificationFilter = kCAFilterTrilinear; - self.imageView.layer.magnificationFilter = kCAFilterTrilinear; + self.mediaView.layer.minificationFilter = kCAFilterTrilinear; + self.mediaView.layer.magnificationFilter = kCAFilterTrilinear; [self applyInitialImageViewConstraints]; @@ -373,15 +372,15 @@ NS_ASSUME_NONNULL_BEGIN } CGRect convertedRect = - [self.imageView.superview convertRect:self.originRect fromView:[UIApplication sharedApplication].keyWindow]; + [self.mediaView.superview convertRect:self.originRect fromView:[UIApplication sharedApplication].keyWindow]; NSMutableArray *imageViewConstraints = [NSMutableArray new]; self.imageViewConstraints = imageViewConstraints; - [imageViewConstraints addObjectsFromArray:[self.imageView autoSetDimensionsToSize:convertedRect.size]]; + [imageViewConstraints addObjectsFromArray:[self.mediaView autoSetDimensionsToSize:convertedRect.size]]; [imageViewConstraints addObjectsFromArray:@[ - [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y], - [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x] + [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y], + [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x] ]]; } @@ -394,16 +393,16 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray *imageViewConstraints = [NSMutableArray new]; self.imageViewConstraints = imageViewConstraints; - self.imageViewLeadingConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; - self.imageViewTopConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - self.imageViewTrailingConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; - self.imageViewBottomConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + self.mediaViewLeadingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; + self.mediaViewTopConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + self.mediaViewTrailingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; + self.mediaViewBottomConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [imageViewConstraints addObjectsFromArray:@[ - self.imageViewTopConstraint, - self.imageViewTrailingConstraint, - self.imageViewBottomConstraint, - self.imageViewLeadingConstraint + self.mediaViewTopConstraint, + self.mediaViewTrailingConstraint, + self.mediaViewBottomConstraint, + self.mediaViewLeadingConstraint ]]; } @@ -710,7 +709,7 @@ NS_ASSUME_NONNULL_BEGIN // Make sure imageView is layed out before we update it's frame in the next // animation. - [self.imageView.superview layoutIfNeeded]; + [self.mediaView.superview layoutIfNeeded]; // 2. Animate imageView from it's initial position, which should match where it was // in the presenting view to it's final position, front and center in this view. This @@ -720,13 +719,13 @@ NS_ASSUME_NONNULL_BEGIN options:UIViewAnimationOptionCurveEaseOut animations:^(void) { [self applyFinalImageViewConstraints]; - [self.imageView.superview layoutIfNeeded]; + [self.mediaView.superview layoutIfNeeded]; // We must lay out *before* we centerImageViewConstraints // because it uses the imageView.frame to build the contstraints // that will center the imageView, and then once again // to ensure that the centered constraints are applied. [self centerImageViewConstraints]; - [self.imageView.superview layoutIfNeeded]; + [self.mediaView.superview layoutIfNeeded]; self.view.backgroundColor = UIColor.whiteColor; } completion:^(BOOL finished) { @@ -744,9 +743,9 @@ NS_ASSUME_NONNULL_BEGIN self.view.userInteractionEnabled = NO; [UIApplication sharedApplication].statusBarHidden = NO; - OWSAssert(self.imageView.superview); + OWSAssert(self.mediaView.superview); - [self.imageView.superview layoutIfNeeded]; + [self.mediaView.superview layoutIfNeeded]; // Move the image view pack to it's initial position, i.e. where // it sits on the screen in the conversation view. @@ -757,7 +756,7 @@ NS_ASSUME_NONNULL_BEGIN delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^(void) { - [self.imageView.superview layoutIfNeeded]; + [self.mediaView.superview layoutIfNeeded]; // In case user has hidden bars, which changes background to black. self.view.backgroundColor = UIColor.whiteColor; @@ -777,7 +776,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { - return self.imageView; + return self.mediaView; } - (void)centerImageViewConstraints @@ -785,15 +784,15 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.scrollView); CGSize scrollViewSize = self.scrollView.bounds.size; - CGSize imageViewSize = self.imageView.frame.size; + CGSize imageViewSize = self.mediaView.frame.size; CGFloat yOffset = MAX(0, (scrollViewSize.height - imageViewSize.height) / 2); - self.imageViewTopConstraint.constant = yOffset; - self.imageViewBottomConstraint.constant = yOffset; + self.mediaViewTopConstraint.constant = yOffset; + self.mediaViewBottomConstraint.constant = yOffset; CGFloat xOffset = MAX(0, (scrollViewSize.width - imageViewSize.width) / 2); - self.imageViewLeadingConstraint.constant = xOffset; - self.imageViewTrailingConstraint.constant = xOffset; + self.mediaViewLeadingConstraint.constant = xOffset; + self.mediaViewTrailingConstraint.constant = xOffset; } - (void)scrollViewDidZoom:(UIScrollView *)scrollView diff --git a/Signal/src/ViewControllers/MediaMessageView.swift b/Signal/src/ViewControllers/MediaMessageView.swift index 438ba42e5..c909f0f70 100644 --- a/Signal/src/ViewControllers/MediaMessageView.swift +++ b/Signal/src/ViewControllers/MediaMessageView.swift @@ -412,7 +412,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { guard let fromView = sender.view else { return } - showFullImageViewController(fromView: fromView) + showMediaDetailViewController(fromView: fromView) } private func fromViewController() -> UIViewController? { @@ -437,22 +437,22 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { guard let fromView = sender.view else { return } - showFullImageViewController(fromView: fromView) + showMediaDetailViewController(fromView: fromView) } - func showFullImageViewController(fromView: UIView) { + func showMediaDetailViewController(fromView: UIView) { guard let fromViewController = fromViewController() else { return } let window = UIApplication.shared.keyWindow let convertedRect = fromView.convert(fromView.bounds, to:window) - let viewController: FullImageViewController = { + let viewController: MediaDetailViewController = { if let viewItem = self.viewItem, let attachmentStream = self.attachmentStream { - return FullImageViewController(attachmentStream: attachmentStream, from: convertedRect, viewItem: viewItem) + return MediaDetailViewController(attachmentStream: attachmentStream, from: convertedRect, viewItem: viewItem) } else { // e.g. when MediaMessageView does not belong to a persisted interaction, e.g. approval view. - return FullImageViewController(attachment: attachment, from:convertedRect) + return MediaDetailViewController(attachment: attachment, from:convertedRect) } }() From 8d2934d86ed81d8aa231fe66b28aa8ffb1613862 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 8 Jan 2018 15:11:35 -0500 Subject: [PATCH 07/23] CR: remove unnecessary code, comments // FREEBIE --- .../ConversationView/ConversationInputToolbar.m | 9 --------- .../ConversationView/ConversationViewController.m | 5 ----- .../ConversationView/ConversationViewLayout.m | 6 +----- SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m | 4 ++-- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 461c288c5..c5ddb7be1 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -76,15 +76,6 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; return newSize; } -- (void)setToolbarHeight:(CGFloat)toolbarHeight -{ - if (toolbarHeight == _toolbarHeight) { - return; - } - - _toolbarHeight = toolbarHeight; -} - - (void)createContents { self.layoutMargins = UIEdgeInsetsZero; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 11de97caf..da25a14f0 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3667,11 +3667,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { } 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; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m index 224048f80..f0dbdfb27 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "ConversationViewLayout.h" @@ -69,10 +69,6 @@ NS_ASSUME_NONNULL_BEGIN 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 264daab7d..4fa9b90d9 100644 --- a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m +++ b/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSOrphanedDataCleaner.h" @@ -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]; }); } From c91658119d04a1e0e82bc44953b703a7aff1bf06 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 8 Jan 2018 15:50:18 -0500 Subject: [PATCH 08/23] CR: double tap zoom centers on tap location // FREEBIE --- .../MediaDetailViewController.m | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index 99b7c70dc..4b7b094a6 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -506,12 +506,27 @@ NS_ASSUME_NONNULL_BEGIN self.areToolbarsHidden = !self.areToolbarsHidden; } -- (void)didDoubleTapImage:(id)sender +- (void)didDoubleTapImage:(UITapGestureRecognizer *)gesture { - DDLogVerbose(@"%@ did tap image.", self.logTag); + DDLogVerbose(@"%@ did double tap image.", self.logTag); if (self.scrollView.zoomScale == self.scrollView.minimumZoomScale) { - [self.scrollView setZoomScale:self.scrollView.minimumZoomScale * 2 animated:YES]; + CGFloat kDoubleTapZoomScale = 2; + + CGFloat zoomWidth = self.scrollView.width / kDoubleTapZoomScale; + CGFloat zoomHeight = self.scrollView.height / kDoubleTapZoomScale; + + // center zoom rect around tapLocation + CGPoint tapLocation = [gesture locationInView:self.scrollView]; + CGFloat zoomX = MAX(0, tapLocation.x - zoomWidth / 2); + CGFloat zoomY = MAX(0, tapLocation.y - zoomHeight / 2); + + CGRect zoomRect = CGRectMake(zoomX, zoomY, zoomWidth, zoomHeight); + + CGRect translatedRect = [self.mediaView convertRect:zoomRect fromView:self.scrollView]; + + [self.scrollView zoomToRect:translatedRect animated:YES]; } else { + // If already zoomed in at all, zoom out all the way. [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES]; } } From 1ec409ad2b0b0d828a64b81af820cd8d99277df3 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 8 Jan 2018 16:29:42 -0500 Subject: [PATCH 09/23] CR: re-enable default keyboard toggle // FREEBIE --- .../ConversationView/ConversationViewController.m | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index da25a14f0..a7d1a071f 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1401,13 +1401,8 @@ 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 dismissKeyBoard]; + [self popKeyBoard]; } #pragma mark - Dynamic Text From e11ac51e31dc5ac791ad7f2faedb39aa8812bd5b Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 8 Jan 2018 17:10:50 -0500 Subject: [PATCH 10/23] Bump version to 2.19.4. --- Carthage | 2 +- Signal/Signal-Info.plist | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Carthage b/Carthage index a172c59aa..0120ebf4e 160000 --- a/Carthage +++ b/Carthage @@ -1 +1 @@ -Subproject commit a172c59aa282c4e0cb43c8134abc5b237d3ca885 +Subproject commit 0120ebf4e1a9d4b55481af894e6ca32aa8bcf2b7 diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index b3d2ee0ec..439ac9580 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -38,7 +38,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.19.3 + 2.19.4 CFBundleSignature ???? CFBundleURLTypes @@ -55,7 +55,7 @@ CFBundleVersion - 2.19.3.3 + 2.19.4.0 ITSAppUsesNonExemptEncryption LOGS_EMAIL From 74019b2ae49118bd9ab93c5d5d13783fbb9c8562 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 8 Jan 2018 17:48:26 -0500 Subject: [PATCH 11/23] Fix keyboard animation glitch after sending // FREEBIE --- .../ConversationInputToolbar.h | 5 ++-- .../ConversationInputToolbar.m | 25 +++++++++++++++---- .../ConversationViewController.m | 15 ++--------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index 802d31d02..3f7ee27b8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -48,8 +48,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)messageText; - (void)setMessageText:(NSString *_Nullable)value; - (void)clearTextMessage; - -- (nullable NSString *)textInputPrimaryLanguage; +- (void)toggleDefaultKeyboard; - (void)updateFontSizes; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index c5ddb7be1..baa9d95c8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -199,6 +199,26 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; [self.inputTextView.undoManager removeAllActions]; } +- (void)toggleDefaultKeyboard +{ + // Primary language is nil for the emoji keyboard. + if (!self.inputTextView.textInputMode.primaryLanguage) { + // Stay on emoji keyboard after sending + return; + } + + // Otherwise, we want to toggle back to default keyboard if the user had the numeric keyboard present. + + // Momentarily switch to a non-default keyboard, else reloadInputViews + // will not affect the displayed keyboard. In practice this isn't perceptable to the user. + // The alternative would be to dismiss-and-pop the keyboard, but that can cause a more pronounced animation. + self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation; + [self.inputTextView reloadInputViews]; + + self.inputTextView.keyboardType = UIKeyboardTypeDefault; + [self.inputTextView reloadInputViews]; +} + - (void)setShouldShowVoiceMemoButton:(BOOL)shouldShowVoiceMemoButton { if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) { @@ -827,11 +847,6 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; [self.attachmentView viewWillDisappear:animated]; } -- (nullable NSString *)textInputPrimaryLanguage -{ - return self.inputTextView.textInputMode.primaryLanguage; -} - @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index a7d1a071f..4e9aa5d53 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1394,17 +1394,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { #pragma mark - JSQMessagesViewController method overrides -- (void)toggleDefaultKeyboard -{ - // Primary language is nil for the emoji keyboard & we want to stay on it after sending - if (!self.inputToolbar.textInputPrimaryLanguage) { - return; - } - - [self dismissKeyBoard]; - [self popKeyBoard]; -} - #pragma mark - Dynamic Text /** @@ -3902,10 +3891,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self messageWasSent:message]; if (updateKeyboardState) { - [self toggleDefaultKeyboard]; + [self.inputToolbar toggleDefaultKeyboard]; } - [self clearDraft]; [self.inputToolbar clearTextMessage]; + [self clearDraft]; if (didAddToProfileWhitelist) { [self ensureDynamicInteractions]; } From 7be8f008379525c9f445b11078f31b652c1a4c76 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 9 Jan 2018 12:32:10 -0500 Subject: [PATCH 12/23] Bump build to 2.19.4.1. // FREEBIE --- Signal/Signal-Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 439ac9580..aed185921 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -55,7 +55,7 @@ CFBundleVersion - 2.19.4.0 + 2.19.4.1 ITSAppUsesNonExemptEncryption LOGS_EMAIL From 74e03aad01cf65a3561719265f1c123761811238 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 11 Jan 2018 12:26:15 -0500 Subject: [PATCH 13/23] Fix intermittent content offset problem Using the CollectionView's frame to determine if we're at the bottom doesn't make sense unless the collection view is correctly layed out. // FREEBIE --- .../ConversationView/ConversationInputToolbar.h | 2 -- .../ConversationView/ConversationViewController.m | 11 ++++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index 3f7ee27b8..5b7df0499 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -26,8 +26,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)didApproveAttachment:(SignalAttachment *)attachment; -- (void)toolbarHeightDidChange:(CGFloat)newHeight; - @end #pragma mark - diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 4e9aa5d53..0a5103725 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3045,7 +3045,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { // content to fill the collection view at its current size. CGFloat contentOffsetYBottom = MAX(0.f, contentHeight + self.collectionView.contentInset.bottom - self.collectionView.bounds.size.height); - BOOL isScrolledToBottom = (self.collectionView.contentOffset.y > contentOffsetYBottom - kIsAtBottomTolerancePts); + + CGFloat distanceFromBottom = contentOffsetYBottom - self.collectionView.contentOffset.y; + BOOL isScrolledToBottom = distanceFromBottom <= kIsAtBottomTolerancePts; return isScrolledToBottom; } @@ -3693,7 +3695,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { 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. + // redundant, so we only adjust content offset when *presenting* the keyboard (i.e. when insetChange > 0). if (insetChange > 0 && newYOffset > keyboardEndFrame.origin.y) { [self.collectionView setContentOffset:newOffset animated:NO]; } @@ -3747,10 +3749,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)scrollToBottomAnimated:(BOOL)animated { OWSAssert([NSThread isMainThread]); - if (self.isUserScrolling) { return; } + + // Ensure the view is fully layed out before we try to scroll to the bottom, since + // we use the collectionView bounds to determine where the "bottom" is. + [self.view layoutIfNeeded]; CGFloat contentHeight = self.safeContentHeight; From e140ffc4231644e0719548e34305e415a1a910b1 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 10 Jan 2018 17:44:39 -0500 Subject: [PATCH 14/23] Fullscreen presentation touchups "zoom" presentation now accounts for zoomScale. Fix background flicker as status bar hides Round corners during presentation/dismiss. Smooths transition a bit. // FREEBIE --- .../MediaDetailViewController.m | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index 4b7b094a6..6ef8f8c71 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -44,6 +44,9 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - +// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. +const CGFloat MediaDetailViewControllerMediaCornerRadius = 17; + @interface MediaDetailViewController () @property (nonatomic) UIScrollView *scrollView; @@ -198,13 +201,6 @@ NS_ASSUME_NONNULL_BEGIN } } -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - [self updateMinZoomScale]; - [self centerImageViewConstraints]; -} - - (void)updateMinZoomScale { CGSize viewSize = self.scrollView.bounds.size; @@ -277,7 +273,7 @@ NS_ASSUME_NONNULL_BEGIN self.mediaView.layer.minificationFilter = kCAFilterTrilinear; self.mediaView.layer.magnificationFilter = kCAFilterTrilinear; - [self applyInitialImageViewConstraints]; + [self applyInitialMediaViewConstraints]; if (self.isVideo) { if (@available(iOS 9, *)) { @@ -365,7 +361,7 @@ NS_ASSUME_NONNULL_BEGIN [self.footerBar setItems:toolbarItems animated:NO]; } -- (void)applyInitialImageViewConstraints +- (void)applyInitialMediaViewConstraints { if (self.imageViewConstraints.count > 0) { [NSLayoutConstraint deactivateConstraints:self.imageViewConstraints]; @@ -384,7 +380,7 @@ NS_ASSUME_NONNULL_BEGIN ]]; } -- (void)applyFinalImageViewConstraints +- (void)applyFinalMediaViewConstraints { if (self.imageViewConstraints.count > 0) { [NSLayoutConstraint deactivateConstraints:self.imageViewConstraints]; @@ -450,10 +446,13 @@ NS_ASSUME_NONNULL_BEGIN [[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden withAnimation:UIStatusBarAnimationNone]; [self.navigationController setNavigationBarHidden:areToolbarsHidden animated:NO]; self.videoProgressBar.hidden = areToolbarsHidden; + + // We don't animate the background color change because the old color shows through momentarily + // behind where the status bar "used to be". + self.view.backgroundColor = areToolbarsHidden ? UIColor.blackColor : UIColor.whiteColor; [UIView animateWithDuration:0.1 animations:^(void) { - self.view.backgroundColor = areToolbarsHidden ? UIColor.blackColor : UIColor.whiteColor; self.footerBar.alpha = areToolbarsHidden ? 0 : 1; }]; } @@ -712,6 +711,14 @@ NS_ASSUME_NONNULL_BEGIN self.view.userInteractionEnabled = NO; self.view.alpha = 0.0; + + // Our zoomScale must == 1 in order for our initialMediaViewConstraints to align with the + // "zoomed" view's pre-presentation position. + OWSAssert(self.scrollView.zoomScale == 1.0); + [self.mediaView.superview layoutIfNeeded]; + + self.mediaView.layer.cornerRadius = MediaDetailViewControllerMediaCornerRadius; + [viewController presentViewController:navController animated:NO completion:^{ @@ -725,19 +732,21 @@ NS_ASSUME_NONNULL_BEGIN // Make sure imageView is layed out before we update it's frame in the next // animation. [self.mediaView.superview layoutIfNeeded]; + [self applyFinalMediaViewConstraints]; // 2. Animate imageView from it's initial position, which should match where it was // in the presenting view to it's final position, front and center in this view. This - // animation intentionally overlaps the previous - [UIView animateWithDuration:0.2 - delay:0.08 + // animation duration intentionally overlaps the previous + [UIView animateWithDuration:2.2 + delay:2.08 options:UIViewAnimationOptionCurveEaseOut animations:^(void) { - [self applyFinalImageViewConstraints]; + self.mediaView.layer.cornerRadius = 0; + [self updateMinZoomScale]; [self.mediaView.superview layoutIfNeeded]; - // We must lay out *before* we centerImageViewConstraints - // because it uses the imageView.frame to build the contstraints - // that will center the imageView, and then once again + // We must lay out once *before* we centerImageViewConstraints + // because it uses the imageView.frame to build the constraints + // that will center the imageView, and then once again *after* // to ensure that the centered constraints are applied. [self centerImageViewConstraints]; [self.mediaView.superview layoutIfNeeded]; @@ -764,7 +773,7 @@ NS_ASSUME_NONNULL_BEGIN // Move the image view pack to it's initial position, i.e. where // it sits on the screen in the conversation view. - [self applyInitialImageViewConstraints]; + [self applyInitialMediaViewConstraints]; if (isAnimated) { [UIView animateWithDuration:0.2 @@ -772,7 +781,12 @@ NS_ASSUME_NONNULL_BEGIN options:UIViewAnimationOptionCurveEaseInOut animations:^(void) { [self.mediaView.superview layoutIfNeeded]; - + + // Our zoomScale must == 1 in order for our initialMediaViewConstraints to align with the + // "zoomed" view's pre-presentation position. + self.scrollView.zoomScale = 1.0; + self.mediaView.layer.cornerRadius = MediaDetailViewControllerMediaCornerRadius; + // In case user has hidden bars, which changes background to black. self.view.backgroundColor = UIColor.whiteColor; From 8851413b357a538e836149222c75cf4360a87cad Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 11 Jan 2018 12:32:12 -0500 Subject: [PATCH 15/23] CR: cleanup, remove debug animation time, move constant // FREEBIE --- .../ConversationView/Cells/OWSMessageCell.h | 4 +++- .../ConversationView/Cells/OWSMessageCell.m | 3 +++ .../MediaDetailViewController.m | 18 ++++++++---------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h index f907e6ae1..3116bce7e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h @@ -1,11 +1,13 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "ConversationViewCell.h" NS_ASSUME_NONNULL_BEGIN +extern const CGFloat OWSMessageCellCornerRadius; + @interface OWSMessageCell : ConversationViewCell + (NSString *)cellReuseIdentifier; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 2aedc8d45..9a59ce6e0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -17,6 +17,9 @@ NS_ASSUME_NONNULL_BEGIN +// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. +const CGFloat OWSMessageCellCornerRadius = 17; + @interface BubbleMaskingView : UIView @property (nonatomic) BOOL isOutgoing; diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index 6ef8f8c71..305a3e12e 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -6,6 +6,7 @@ #import "AttachmentSharing.h" #import "ConversationViewController.h" #import "ConversationViewItem.h" +#import "OWSMessageCell.h" #import "Signal-Swift.h" #import "TSAttachmentStream.h" #import "TSInteraction.h" @@ -44,9 +45,6 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - -// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. -const CGFloat MediaDetailViewControllerMediaCornerRadius = 17; - @interface MediaDetailViewController () @property (nonatomic) UIScrollView *scrollView; @@ -716,9 +714,9 @@ const CGFloat MediaDetailViewControllerMediaCornerRadius = 17; // "zoomed" view's pre-presentation position. OWSAssert(self.scrollView.zoomScale == 1.0); [self.mediaView.superview layoutIfNeeded]; - - self.mediaView.layer.cornerRadius = MediaDetailViewControllerMediaCornerRadius; - + + self.mediaView.layer.cornerRadius = OWSMessageCellCornerRadius; + [viewController presentViewController:navController animated:NO completion:^{ @@ -737,8 +735,8 @@ const CGFloat MediaDetailViewControllerMediaCornerRadius = 17; // 2. Animate imageView from it's initial position, which should match where it was // in the presenting view to it's final position, front and center in this view. This // animation duration intentionally overlaps the previous - [UIView animateWithDuration:2.2 - delay:2.08 + [UIView animateWithDuration:0.2 + delay:0.08 options:UIViewAnimationOptionCurveEaseOut animations:^(void) { self.mediaView.layer.cornerRadius = 0; @@ -785,8 +783,8 @@ const CGFloat MediaDetailViewControllerMediaCornerRadius = 17; // Our zoomScale must == 1 in order for our initialMediaViewConstraints to align with the // "zoomed" view's pre-presentation position. self.scrollView.zoomScale = 1.0; - self.mediaView.layer.cornerRadius = MediaDetailViewControllerMediaCornerRadius; - + self.mediaView.layer.cornerRadius = OWSMessageCellCornerRadius; + // In case user has hidden bars, which changes background to black. self.view.backgroundColor = UIColor.whiteColor; From 7c2bfdfb1f98eab4ab21b183cf73d31ff92e9504 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 11 Jan 2018 15:46:38 -0500 Subject: [PATCH 16/23] rename: imageView -> mediaView // FREEBIE --- .../MediaDetailViewController.m | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index 305a3e12e..75d43c913 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -68,7 +68,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) UIBarButtonItem *videoPlayBarButton; @property (nonatomic, nullable) UIBarButtonItem *videoPauseBarButton; -@property (nonatomic, nullable) NSArray *imageViewConstraints; +@property (nonatomic, nullable) NSArray *mediaViewConstraints; @property (nonatomic, nullable) NSLayoutConstraint *mediaViewBottomConstraint; @property (nonatomic, nullable) NSLayoutConstraint *mediaViewLeadingConstraint; @property (nonatomic, nullable) NSLayoutConstraint *mediaViewTopConstraint; @@ -361,18 +361,18 @@ NS_ASSUME_NONNULL_BEGIN - (void)applyInitialMediaViewConstraints { - if (self.imageViewConstraints.count > 0) { - [NSLayoutConstraint deactivateConstraints:self.imageViewConstraints]; + if (self.mediaViewConstraints.count > 0) { + [NSLayoutConstraint deactivateConstraints:self.mediaViewConstraints]; } CGRect convertedRect = [self.mediaView.superview convertRect:self.originRect fromView:[UIApplication sharedApplication].keyWindow]; + + NSMutableArray *mediaViewConstraints = [NSMutableArray new]; + self.mediaViewConstraints = mediaViewConstraints; - NSMutableArray *imageViewConstraints = [NSMutableArray new]; - self.imageViewConstraints = imageViewConstraints; - - [imageViewConstraints addObjectsFromArray:[self.mediaView autoSetDimensionsToSize:convertedRect.size]]; - [imageViewConstraints addObjectsFromArray:@[ + [mediaViewConstraints addObjectsFromArray:[self.mediaView autoSetDimensionsToSize:convertedRect.size]]; + [mediaViewConstraints addObjectsFromArray:@[ [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y], [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x] ]]; @@ -380,19 +380,19 @@ NS_ASSUME_NONNULL_BEGIN - (void)applyFinalMediaViewConstraints { - if (self.imageViewConstraints.count > 0) { - [NSLayoutConstraint deactivateConstraints:self.imageViewConstraints]; + if (self.mediaViewConstraints.count > 0) { + [NSLayoutConstraint deactivateConstraints:self.mediaViewConstraints]; } - NSMutableArray *imageViewConstraints = [NSMutableArray new]; - self.imageViewConstraints = imageViewConstraints; + NSMutableArray *mediaViewConstraints = [NSMutableArray new]; + self.mediaViewConstraints = mediaViewConstraints; self.mediaViewLeadingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; self.mediaViewTopConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop]; self.mediaViewTrailingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; self.mediaViewBottomConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - [imageViewConstraints addObjectsFromArray:@[ + [mediaViewConstraints addObjectsFromArray:@[ self.mediaViewTopConstraint, self.mediaViewTrailingConstraint, self.mediaViewBottomConstraint, @@ -742,11 +742,12 @@ NS_ASSUME_NONNULL_BEGIN self.mediaView.layer.cornerRadius = 0; [self updateMinZoomScale]; [self.mediaView.superview layoutIfNeeded]; - // We must lay out once *before* we centerImageViewConstraints + + // We must lay out once *before* we centerMediaViewConstraints // because it uses the imageView.frame to build the constraints // that will center the imageView, and then once again *after* // to ensure that the centered constraints are applied. - [self centerImageViewConstraints]; + [self centerMediaViewConstraints]; [self.mediaView.superview layoutIfNeeded]; self.view.backgroundColor = UIColor.whiteColor; } @@ -806,7 +807,7 @@ NS_ASSUME_NONNULL_BEGIN return self.mediaView; } -- (void)centerImageViewConstraints +- (void)centerMediaViewConstraints { OWSAssert(self.scrollView); @@ -824,7 +825,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)scrollViewDidZoom:(UIScrollView *)scrollView { - [self centerImageViewConstraints]; + [self centerMediaViewConstraints]; [self.view layoutIfNeeded]; } From 3582ab42db90c33f4d571373d32f11bd59175c98 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 11 Jan 2018 18:29:06 -0500 Subject: [PATCH 17/23] Fix media detail presentation - video view now scales during presentation - no "swooping" when presenting large res images // FREEBIE --- .../MediaDetailViewController.m | 130 +++++++++++------- 1 file changed, 82 insertions(+), 48 deletions(-) diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index 75d43c913..06afb19bf 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -49,6 +49,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIScrollView *scrollView; @property (nonatomic) UIView *mediaView; +@property (nonatomic) UIView *presentationView; @property (nonatomic) UIButton *shareButton; @@ -68,7 +69,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) UIBarButtonItem *videoPlayBarButton; @property (nonatomic, nullable) UIBarButtonItem *videoPauseBarButton; -@property (nonatomic, nullable) NSArray *mediaViewConstraints; +@property (nonatomic, nullable) NSArray *presentationViewConstraints; @property (nonatomic, nullable) NSLayoutConstraint *mediaViewBottomConstraint; @property (nonatomic, nullable) NSLayoutConstraint *mediaViewLeadingConstraint; @property (nonatomic, nullable) NSLayoutConstraint *mediaViewTopConstraint; @@ -199,6 +200,14 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + [self updateMinZoomScale]; + [self centerMediaViewConstraints]; +} + - (void)updateMinZoomScale { CGSize viewSize = self.scrollView.bounds.size; @@ -260,6 +269,11 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.mediaView); [scrollView addSubview:self.mediaView]; + self.mediaViewLeadingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; + self.mediaViewTopConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + self.mediaViewTrailingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; + self.mediaViewBottomConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + self.mediaView.contentMode = UIViewContentModeScaleAspectFit; self.mediaView.userInteractionEnabled = YES; self.mediaView.clipsToBounds = YES; @@ -271,6 +285,19 @@ NS_ASSUME_NONNULL_BEGIN self.mediaView.layer.minificationFilter = kCAFilterTrilinear; self.mediaView.layer.magnificationFilter = kCAFilterTrilinear; + // The presentationView is only used during present/dismiss animations. + // It's a static image of the media content. + UIImageView *presentationView = [[UIImageView alloc] initWithImage:self.image]; + self.presentationView = presentationView; + + [self.view addSubview:presentationView]; + presentationView.hidden = YES; + presentationView.clipsToBounds = YES; + presentationView.layer.allowsEdgeAntialiasing = YES; + presentationView.layer.minificationFilter = kCAFilterTrilinear; + presentationView.layer.magnificationFilter = kCAFilterTrilinear; + presentationView.contentMode = UIViewContentModeScaleAspectFit; + [self applyInitialMediaViewConstraints]; if (self.isVideo) { @@ -361,42 +388,38 @@ NS_ASSUME_NONNULL_BEGIN - (void)applyInitialMediaViewConstraints { - if (self.mediaViewConstraints.count > 0) { - [NSLayoutConstraint deactivateConstraints:self.mediaViewConstraints]; + if (self.presentationViewConstraints.count > 0) { + [NSLayoutConstraint deactivateConstraints:self.presentationViewConstraints]; } - CGRect convertedRect = - [self.mediaView.superview convertRect:self.originRect fromView:[UIApplication sharedApplication].keyWindow]; - - NSMutableArray *mediaViewConstraints = [NSMutableArray new]; - self.mediaViewConstraints = mediaViewConstraints; + CGRect convertedRect = [self.presentationView.superview convertRect:self.originRect + fromView:[UIApplication sharedApplication].keyWindow]; - [mediaViewConstraints addObjectsFromArray:[self.mediaView autoSetDimensionsToSize:convertedRect.size]]; - [mediaViewConstraints addObjectsFromArray:@[ - [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y], - [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x] + NSMutableArray *presentationViewConstraints = [NSMutableArray new]; + self.presentationViewConstraints = presentationViewConstraints; + + [presentationViewConstraints + addObjectsFromArray:[self.presentationView autoSetDimensionsToSize:convertedRect.size]]; + [presentationViewConstraints addObjectsFromArray:@[ + [self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y], + [self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x] ]]; } - (void)applyFinalMediaViewConstraints { - if (self.mediaViewConstraints.count > 0) { - [NSLayoutConstraint deactivateConstraints:self.mediaViewConstraints]; + if (self.presentationViewConstraints.count > 0) { + [NSLayoutConstraint deactivateConstraints:self.presentationViewConstraints]; } - NSMutableArray *mediaViewConstraints = [NSMutableArray new]; - self.mediaViewConstraints = mediaViewConstraints; + NSMutableArray *presentationViewConstraints = [NSMutableArray new]; + self.presentationViewConstraints = presentationViewConstraints; - self.mediaViewLeadingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; - self.mediaViewTopConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - self.mediaViewTrailingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; - self.mediaViewBottomConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - - [mediaViewConstraints addObjectsFromArray:@[ - self.mediaViewTopConstraint, - self.mediaViewTrailingConstraint, - self.mediaViewBottomConstraint, - self.mediaViewLeadingConstraint + [presentationViewConstraints addObjectsFromArray:@[ + [self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeLeading], + [self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeTop], + [self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeTrailing], + [self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeBottom] ]]; } @@ -708,14 +731,22 @@ NS_ASSUME_NONNULL_BEGIN self.view.userInteractionEnabled = NO; + // We want to animate the tapped media from it's position in the previous VC + // to it's resting place in the center of this view controller. + // + // Rather than animating the actual media view in place, we animate the presentationView, which is a static + // image of the media content. Animating the actual media view is problematic for a couple reasons: + // 1. The media view ultimately lives in a zoomable scrollView. Getting both original positioning and the final positioning + // correct, involves manipulating the zoomScale and position simultaneously, which results in non-linear movement, + // especially noticeable on high resolution images. + // 2. For Video views, the AVPlayerLayer content does not scale with the presentation animation. So you instead get a full scale + // video, wherein only the cropping is animated. + // Using a simple image view allows us to address both these problems relatively easily. self.view.alpha = 0.0; - - // Our zoomScale must == 1 in order for our initialMediaViewConstraints to align with the - // "zoomed" view's pre-presentation position. - OWSAssert(self.scrollView.zoomScale == 1.0); - [self.mediaView.superview layoutIfNeeded]; - self.mediaView.layer.cornerRadius = OWSMessageCellCornerRadius; + self.mediaView.hidden = YES; + self.presentationView.hidden = NO; + self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius; [viewController presentViewController:navController animated:NO @@ -727,9 +758,7 @@ NS_ASSUME_NONNULL_BEGIN self.view.alpha = 1.0; }]; - // Make sure imageView is layed out before we update it's frame in the next - // animation. - [self.mediaView.superview layoutIfNeeded]; + [self.presentationView.superview layoutIfNeeded]; [self applyFinalMediaViewConstraints]; // 2. Animate imageView from it's initial position, which should match where it was @@ -739,10 +768,9 @@ NS_ASSUME_NONNULL_BEGIN delay:0.08 options:UIViewAnimationOptionCurveEaseOut animations:^(void) { - self.mediaView.layer.cornerRadius = 0; - [self updateMinZoomScale]; - [self.mediaView.superview layoutIfNeeded]; - + self.presentationView.layer.cornerRadius = 0; + [self.presentationView.superview layoutIfNeeded]; + // We must lay out once *before* we centerMediaViewConstraints // because it uses the imageView.frame to build the constraints // that will center the imageView, and then once again *after* @@ -752,6 +780,11 @@ NS_ASSUME_NONNULL_BEGIN self.view.backgroundColor = UIColor.whiteColor; } completion:^(BOOL finished) { + // At this point our presentation view should be overlayed perfectly + // with our media view. Swapping them out should be imperceptible. + self.mediaView.hidden = NO; + self.presentationView.hidden = YES; + self.view.userInteractionEnabled = YES; if (self.isVideo) { @@ -763,14 +796,19 @@ NS_ASSUME_NONNULL_BEGIN - (void)dismissSelfAnimated:(BOOL)isAnimated completion:(void (^_Nullable)(void))completion { + self.view.userInteractionEnabled = NO; [UIApplication sharedApplication].statusBarHidden = NO; - OWSAssert(self.mediaView.superview); + // Swapping mediaView for presentationView will be perceptible if we're not zoomed out all the way. + if (self.scrollView.zoomScale != self.scrollView.minimumZoomScale) { + [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES]; + } - [self.mediaView.superview layoutIfNeeded]; + self.mediaView.hidden = YES; + self.presentationView.hidden = NO; - // Move the image view pack to it's initial position, i.e. where + // Move the presentationView back to it's initial position, i.e. where // it sits on the screen in the conversation view. [self applyInitialMediaViewConstraints]; @@ -779,12 +817,8 @@ NS_ASSUME_NONNULL_BEGIN delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^(void) { - [self.mediaView.superview layoutIfNeeded]; - - // Our zoomScale must == 1 in order for our initialMediaViewConstraints to align with the - // "zoomed" view's pre-presentation position. - self.scrollView.zoomScale = 1.0; - self.mediaView.layer.cornerRadius = OWSMessageCellCornerRadius; + [self.presentationView.superview layoutIfNeeded]; + self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius; // In case user has hidden bars, which changes background to black. self.view.backgroundColor = UIColor.whiteColor; From 1ef824029e9af92dde22bcc9b50c31df0b2dcffc Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 11 Jan 2018 23:58:11 -0500 Subject: [PATCH 18/23] Fix distorted images // FREEBIE --- Signal/src/ViewControllers/MediaDetailViewController.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index 06afb19bf..468b7c2db 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -780,6 +780,12 @@ NS_ASSUME_NONNULL_BEGIN self.view.backgroundColor = UIColor.whiteColor; } completion:^(BOOL finished) { + // HACK: Setting the frame to itself *seems* like it should be a no-op, but + // it ensures the content is drawn at the right frame. In particular I was reproducibly + // some images squished (they were EXIF rotated, maybe relateed). + // similar to this report: https://stackoverflow.com/questions/27961884/swift-uiimageview-stretched-aspect + self.mediaView.frame = self.mediaView.frame; + // At this point our presentation view should be overlayed perfectly // with our media view. Swapping them out should be imperceptible. self.mediaView.hidden = NO; From 63c23b77d003c2b96eec361d989315dae46bc839 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 12 Jan 2018 00:17:06 -0500 Subject: [PATCH 19/23] Cleanup presentation view, feels less blurry // FREEBIE --- .../ConversationViewController.m | 4 +-- .../MediaDetailViewController.h | 2 +- .../MediaDetailViewController.m | 33 ++++++++++++++----- .../ViewControllers/MediaMessageView.swift | 2 +- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 0a5103725..ea817812b 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1989,7 +1989,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream fromRect:convertedRect viewItem:viewItem]; - [vc presentFromViewController:self]; + [vc presentFromViewController:self replacingView:imageView]; } - (void)didTapVideoViewItem:(ConversationViewItem *)viewItem @@ -2007,7 +2007,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream fromRect:convertedRect viewItem:viewItem]; - [vc presentFromViewController:self]; + [vc presentFromViewController:self replacingView:imageView]; } - (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream diff --git a/Signal/src/ViewControllers/MediaDetailViewController.h b/Signal/src/ViewControllers/MediaDetailViewController.h index e93fa7e96..d9dd6808a 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.h +++ b/Signal/src/ViewControllers/MediaDetailViewController.h @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect; -- (void)presentFromViewController:(UIViewController *)viewController; +- (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)view; @end diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index 468b7c2db..80cb4610c 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -50,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIScrollView *scrollView; @property (nonatomic) UIView *mediaView; @property (nonatomic) UIView *presentationView; - +@property (nonatomic) UIView *replacingView; @property (nonatomic) UIButton *shareButton; @property (nonatomic) CGRect originRect; @@ -717,8 +717,9 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Presentation -- (void)presentFromViewController:(UIViewController *)viewController +- (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)view; { + self.replacingView = view; UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self]; // UIModalPresentationCustom retains the current view context behind our VC, allowing us to manually @@ -755,6 +756,7 @@ NS_ASSUME_NONNULL_BEGIN // 1. Fade in the entire view. [UIView animateWithDuration:0.1 animations:^{ + self.replacingView.alpha = 0.0; self.view.alpha = 1.0; }]; @@ -819,9 +821,9 @@ NS_ASSUME_NONNULL_BEGIN [self applyInitialMediaViewConstraints]; if (isAnimated) { - [UIView animateWithDuration:0.2 + [UIView animateWithDuration:0.18 delay:0.0 - options:UIViewAnimationOptionCurveEaseInOut + options:UIViewAnimationOptionCurveEaseOut animations:^(void) { [self.presentationView.superview layoutIfNeeded]; self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius; @@ -829,13 +831,26 @@ NS_ASSUME_NONNULL_BEGIN // In case user has hidden bars, which changes background to black. self.view.backgroundColor = UIColor.whiteColor; - // fade out content and toolbars - self.navigationController.view.alpha = 0.0; } - completion:^(BOOL finished) { - [self.presentingViewController dismissViewControllerAnimated:NO completion:completion]; - }]; + completion:nil]; + + [UIView animateWithDuration:0.1 + delay:0.15 + options:UIViewAnimationOptionCurveEaseInOut + animations:^(void) { + + OWSAssert(self.replacingView); + self.replacingView.alpha = 1.0; + + // fade out content and toolbars + self.navigationController.view.alpha = 0.0; + } + completion:^(BOOL finished) { + [self.presentingViewController dismissViewControllerAnimated:NO completion:completion]; + }]; + } else { + self.replacingView.alpha = 1.0; [self.presentingViewController dismissViewControllerAnimated:NO completion:completion]; } } diff --git a/Signal/src/ViewControllers/MediaMessageView.swift b/Signal/src/ViewControllers/MediaMessageView.swift index c909f0f70..319dc3fc2 100644 --- a/Signal/src/ViewControllers/MediaMessageView.swift +++ b/Signal/src/ViewControllers/MediaMessageView.swift @@ -456,6 +456,6 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { } }() - viewController.present(from:fromViewController) + viewController.present(from: fromViewController, replacing: fromView) } } From 764b8153566f74a36db091598e1a10896160a0b7 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 12 Jan 2018 00:22:52 -0500 Subject: [PATCH 20/23] bump build // FREEBIE --- Signal/Signal-Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index aed185921..2beada336 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -55,7 +55,7 @@ CFBundleVersion - 2.19.4.1 + 2.19.4.2 ITSAppUsesNonExemptEncryption LOGS_EMAIL From a4cadfecfad2e9bcb082bc6ed3d5656530ecb84c Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 12 Jan 2018 00:55:17 -0500 Subject: [PATCH 21/23] bump build // FREEBIE --- Signal/Signal-Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 2beada336..0630e5736 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -55,7 +55,7 @@ CFBundleVersion - 2.19.4.2 + 2.19.4.3 ITSAppUsesNonExemptEncryption LOGS_EMAIL From 7eb6b1cdd6875da909d735b599eea508851f731c Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 12 Jan 2018 15:51:24 -0500 Subject: [PATCH 22/23] Revert submodule update from "Bump version to 2.19.4." Partial revert of commit e11ac51e31dc5ac791ad7f2faedb39aa8812bd5b to remove unintentional submodule update --- Carthage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Carthage b/Carthage index 0120ebf4e..a172c59aa 160000 --- a/Carthage +++ b/Carthage @@ -1 +1 @@ -Subproject commit 0120ebf4e1a9d4b55481af894e6ca32aa8bcf2b7 +Subproject commit a172c59aa282c4e0cb43c8134abc5b237d3ca885 From 56112e79beba101ecdb928fd37137ce288100d94 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 12 Jan 2018 15:53:15 -0500 Subject: [PATCH 23/23] bump build // FREEBIE --- Signal/Signal-Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 0630e5736..6abe2e18a 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -55,7 +55,7 @@ CFBundleVersion - 2.19.4.3 + 2.19.4.4 ITSAppUsesNonExemptEncryption LOGS_EMAIL