From 81268012e5c7d779877a7263eb8cac7df1fa20e6 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 2 Jan 2018 15:55:40 -0600 Subject: [PATCH 1/9] 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 d8287e78b8..1c1e89aea3 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 cc09436498..0b5724d7e4 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 c1a0798c77..802d31d02a 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 0753c686d4..461c288c5e 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 99606fc8eb..0ada12975c 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 76398a7476..224048f808 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 23997a5b85..264daab7d4 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 2/9] 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 22b18ae080..db315d5f7d 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 da153cbc48..2aedc8d451 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 0ada12975c..07ff935cb3 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 49a4c6bcb4..50dc56e861 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 3/9] 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 7e86230613..416be1250b 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 0000000000..6c8bfcfad4 --- /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 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 +@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 0000000000..107f746a00 --- /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 4/9] 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 79bb9f0cbb..d3ab7454ce 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 5/9] 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 d3ab7454ce..0ab8b8a42e 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 5f4fdd20b5..438ba42e5f 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 d9e0ec6866..b6eb060379 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 6/9] 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 416be1250b..ec40b76848 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 9b955a4b6e..e6808cc49c 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 07ff935cb3..11de97cafb 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 6ade09366f..e93fa7e96f 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 0ab8b8a42e..99b7c70dcc 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 438ba42e5f..c909f0f70c 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 7/9] 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 461c288c5e..c5ddb7be1a 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 11de97cafb..da25a14f06 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 224048f808..f0dbdfb273 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 264daab7d4..4fa9b90d93 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 8/9] 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 99b7c70dcc..4b7b094a6e 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 9/9] 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 da25a14f06..a7d1a071f4 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