diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 1db27bc0fb..3c480ff54e 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -322,8 +322,6 @@ A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4F17A06537000A904E /* AddressBookUI.framework */; }; A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4D17A0652C000A904E /* AddressBook.framework */; }; A5509ECA1A69AB8B00ABA4BC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5509EC91A69AB8B00ABA4BC /* Main.storyboard */; }; - AD41D7B51A6F6F0600241130 /* play_button.png in Resources */ = {isa = PBXBuildFile; fileRef = AD41D7B31A6F6F0600241130 /* play_button.png */; }; - AD41D7B61A6F6F0600241130 /* play_button@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AD41D7B41A6F6F0600241130 /* play_button@2x.png */; }; AD83FF3F1A73426500B5C81A /* audio_pause_button_blue.png in Resources */ = {isa = PBXBuildFile; fileRef = AD83FF381A73426500B5C81A /* audio_pause_button_blue.png */; }; AD83FF401A73426500B5C81A /* audio_pause_button_blue@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AD83FF391A73426500B5C81A /* audio_pause_button_blue@2x.png */; }; AD83FF411A73426500B5C81A /* audio_play_button_blue@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AD83FF3A1A73426500B5C81A /* audio_play_button_blue@2x.png */; }; @@ -867,8 +865,6 @@ A1FDCBEE16DAA6C300868894 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; A5509EC91A69AB8B00ABA4BC /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Storyboard/Main.storyboard; sourceTree = ""; }; AD2AB1207E8888E4262D781B /* Pods-SignalTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.debug.xcconfig"; sourceTree = ""; }; - AD41D7B31A6F6F0600241130 /* play_button.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = play_button.png; sourceTree = ""; }; - AD41D7B41A6F6F0600241130 /* play_button@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play_button@2x.png"; sourceTree = ""; }; AD83FF381A73426500B5C81A /* audio_pause_button_blue.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = audio_pause_button_blue.png; sourceTree = ""; }; AD83FF391A73426500B5C81A /* audio_pause_button_blue@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "audio_pause_button_blue@2x.png"; sourceTree = ""; }; AD83FF3A1A73426500B5C81A /* audio_play_button_blue@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "audio_play_button_blue@2x.png"; sourceTree = ""; }; @@ -1796,8 +1792,6 @@ B10C9B5C1A7049EC00ECA2BF /* pause_icon@2x.png */, B10C9B5D1A7049EC00ECA2BF /* play_icon.png */, B10C9B5E1A7049EC00ECA2BF /* play_icon@2x.png */, - AD41D7B31A6F6F0600241130 /* play_button.png */, - AD41D7B41A6F6F0600241130 /* play_button@2x.png */, B633C5041A1D190B0059AC12 /* call@2x.png */, B633C50B1A1D190B0059AC12 /* contact_default_feed.png */, B633C51B1A1D190B0059AC12 /* endcall@2x.png */, @@ -2330,7 +2324,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - AD41D7B61A6F6F0600241130 /* play_button@2x.png in Resources */, AD83FF3F1A73426500B5C81A /* audio_pause_button_blue.png in Resources */, 34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */, A5509ECA1A69AB8B00ABA4BC /* Main.storyboard in Resources */, @@ -2340,7 +2333,6 @@ B633C5CE1A1D190B0059AC12 /* quit@2x.png in Resources */, AD83FF441A73426500B5C81A /* audio_pause_button.png in Resources */, B6F509971AA53F760068F56A /* Localizable.strings in Resources */, - AD41D7B51A6F6F0600241130 /* play_button.png in Resources */, B633C59D1A1D190B0059AC12 /* endcall@2x.png in Resources */, FC5CDF391A3393DD00B47253 /* error_white@2x.png in Resources */, B633C5D21A1D190B0059AC12 /* savephoto@2x.png in Resources */, diff --git a/Signal/Images.xcassets/play_button.imageset/Contents.json b/Signal/Images.xcassets/play_button.imageset/Contents.json new file mode 100644 index 0000000000..a9902e0a30 --- /dev/null +++ b/Signal/Images.xcassets/play_button.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "play_button@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images/play_button@2x.png b/Signal/Images.xcassets/play_button.imageset/play_button@2x.png similarity index 100% rename from Signal/Images/play_button@2x.png rename to Signal/Images.xcassets/play_button.imageset/play_button@2x.png diff --git a/Signal/Images.xcassets/play_button_large.imageset/Contents.json b/Signal/Images.xcassets/play_button_large.imageset/Contents.json new file mode 100644 index 0000000000..e6d2330753 --- /dev/null +++ b/Signal/Images.xcassets/play_button_large.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "play_button_large.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "play_button_large@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/play_button_large.imageset/play_button_large.png b/Signal/Images.xcassets/play_button_large.imageset/play_button_large.png new file mode 100644 index 0000000000..7f5f552aba Binary files /dev/null and b/Signal/Images.xcassets/play_button_large.imageset/play_button_large.png differ diff --git a/Signal/Images.xcassets/play_button_large.imageset/play_button_large@2x.png b/Signal/Images.xcassets/play_button_large.imageset/play_button_large@2x.png new file mode 100644 index 0000000000..b4e394f27d Binary files /dev/null and b/Signal/Images.xcassets/play_button_large.imageset/play_button_large@2x.png differ diff --git a/Signal/Images/play_button.png b/Signal/Images/play_button.png deleted file mode 100644 index f18452d20b..0000000000 Binary files a/Signal/Images/play_button.png and /dev/null differ diff --git a/SignalMessaging/attachments/AttachmentApprovalViewController.swift b/SignalMessaging/attachments/AttachmentApprovalViewController.swift index 43321c7370..b564f1b503 100644 --- a/SignalMessaging/attachments/AttachmentApprovalViewController.swift +++ b/SignalMessaging/attachments/AttachmentApprovalViewController.swift @@ -7,8 +7,8 @@ import MediaPlayer @objc public protocol AttachmentApprovalViewControllerDelegate: class { - func didApproveAttachment() - func didCancelAttachment() + func didApproveAttachment(attachment: SignalAttachment) + func didCancelAttachment(attachment: SignalAttachment) } @objc @@ -21,7 +21,9 @@ public class AttachmentApprovalViewController: OWSViewController { let attachment: SignalAttachment - let mediaMessageView: MediaMessageView + private(set) var bottomToolbar: UIToolbar! + private(set) var mediaMessageView: MediaMessageView! + private(set) var scrollView: UIScrollView! // MARK: Initializers @@ -30,11 +32,12 @@ public class AttachmentApprovalViewController: OWSViewController { fatalError("unimplemented") } + @objc required public init(attachment: SignalAttachment, delegate: AttachmentApprovalViewControllerDelegate) { assert(!attachment.hasError) self.attachment = attachment self.delegate = delegate - self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .large) + super.init(nibName: nil, bundle: nil) } @@ -42,14 +45,17 @@ public class AttachmentApprovalViewController: OWSViewController { override public func viewDidLoad() { super.viewDidLoad() - - view.backgroundColor = UIColor.white - - createViews() - self.navigationItem.title = dialogTitle() } + override public func viewWillLayoutSubviews() { + Logger.debug("\(logTag) in \(#function)") + super.viewWillLayoutSubviews() + + // e.g. if flipping to/from landscape + updateMinZoomScaleForSize(view.bounds.size) + } + private func dialogTitle() -> String { guard let filename = mediaMessageView.formattedFileName() else { return NSLocalizedString("ATTACHMENT_APPROVAL_DIALOG_TITLE", @@ -59,12 +65,19 @@ public class AttachmentApprovalViewController: OWSViewController { } override public func viewWillAppear(_ animated: Bool) { + Logger.debug("\(logTag) in \(#function)") super.viewWillAppear(animated) mediaMessageView.viewWillAppear(animated) } + override public func viewDidAppear(_ animated: Bool) { + Logger.debug("\(logTag) in \(#function)") + super.viewDidAppear(animated) + } + override public func viewWillDisappear(_ animated: Bool) { + Logger.debug("\(logTag) in \(#function)") super.viewWillDisappear(animated) mediaMessageView.viewWillDisappear(animated) @@ -72,100 +85,140 @@ public class AttachmentApprovalViewController: OWSViewController { // MARK: - Create Views - private func createViews() { - let previewTopMargin: CGFloat = 30 - let previewHMargin: CGFloat = 20 + public override func loadView() { - self.view.addSubview(mediaMessageView) - mediaMessageView.autoPinWidthToSuperview(withMargin:previewHMargin) - mediaMessageView.autoPin(toTopLayoutGuideOf: self, withInset:previewTopMargin) + self.view = UIView() - createButtonRow(mediaMessageView:mediaMessageView) - } + self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) - private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView { - assert(subviews.count > 0) + // Scroll View - used to zoom/pan on images and video + scrollView = UIScrollView() + view.addSubview(scrollView) - let stackView = UIView() + scrollView.delegate = self + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false - var lastView: UIView? - for subview in subviews { + // Panning should stop pretty soon after the user stops scrolling + scrollView.decelerationRate = UIScrollViewDecelerationRateFast - stackView.addSubview(subview) - subview.autoHCenterInSuperview() + // We want scroll view content up and behind the system status bar content + // but we want other content (e.g. bar buttons) to respect the top layout guide. + self.automaticallyAdjustsScrollViewInsets = false - if lastView == nil { - subview.autoPinEdge(toSuperviewEdge:.top) - } else { - subview.autoPinEdge(.top, to:.bottom, of:lastView!, withOffset:10) - } + scrollView.autoPinEdgesToSuperviewEdges() - lastView = subview + let backgroundColor = UIColor.black + self.view.backgroundColor = backgroundColor + + // Create full screen container view so the scrollView + // can compute an appropriate content size in which to center + // our media view. + let containerView = UIView.container() + scrollView.addSubview(containerView) + containerView.autoPinEdgesToSuperviewEdges() + containerView.autoMatch(.height, to: .height, of: self.view) + containerView.autoMatch(.width, to: .width, of: self.view) + + containerView.addSubview(mediaMessageView) + mediaMessageView.autoCenterInSuperview() + mediaMessageView.setCompressionResistanceHigh() + + // Add top and bottom gradients to ensure toolbar controls are legible + // when placed over media with a clashing color + let topGradient = GradientView(from: backgroundColor, to: UIColor.clear) + self.view.addSubview(topGradient) + topGradient.autoPinWidthToSuperview() + topGradient.autoPinEdge(toSuperviewEdge: .top) + topGradient.autoSetDimension(.height, toSize: ScaleFromIPhone5(60)) + + let bottomGradient = GradientView(from: UIColor.clear, to: backgroundColor) + self.view.addSubview(bottomGradient) + bottomGradient.autoPinWidthToSuperview() + bottomGradient.autoPinEdge(toSuperviewEdge: .bottom) + bottomGradient.autoSetDimension(.height, toSize: ScaleFromIPhone5(100)) + + // Hide the play button embedded in the MediaView and replace it with our own. + // This allows us to zoom in on the media view without zooming in on the button + if attachment.isVideo { + self.mediaMessageView.videoPlayButton?.isHidden = true + let playButton = UIButton() + playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "accessability label for button to start media playback") + playButton.setBackgroundImage(#imageLiteral(resourceName: "play_button"), for: .normal) + playButton.contentMode = .scaleAspectFit + + let playButtonWidth = ScaleFromIPhone5(70) + playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth)) + self.view.addSubview(playButton) + + playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) + playButton.autoCenterInSuperview() } - lastView?.autoPinEdge(toSuperviewEdge:.bottom) + // Top Toolbar + let topToolbar = makeClearToolbar() - return stackView + self.view.addSubview(topToolbar) + topToolbar.autoPinWidthToSuperview() + topToolbar.autoPin(toTopLayoutGuideOf: self, withInset: 0) + topToolbar.setContentHuggingVerticalHigh() + topToolbar.setCompressionResistanceVerticalHigh() + + let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(cancelPressed)) + cancelButton.tintColor = UIColor.white + topToolbar.items = [cancelButton] + + // Bottom Toolbar + self.bottomToolbar = makeClearToolbar() + // Making a toolbar transparent requires setting an empty uiimage + bottomToolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) + bottomToolbar.backgroundColor = UIColor.clear + + let sendTitle = NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON", comment: "Label for 'send' button in the 'attachment approval' dialog.") + let sendButton = UIBarButtonItem(title: sendTitle, + style: .plain, + target: self, + action: #selector(sendPressed)) + sendButton.tintColor = UIColor.white + + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + bottomToolbar.items = [flexibleSpace, sendButton] + + self.view.addSubview(bottomToolbar) + bottomToolbar.autoPin(toBottomLayoutGuideOf: self, withInset: 0) + bottomToolbar.autoPinWidthToSuperview() + bottomToolbar.setCompressionResistanceVerticalHigh() + bottomToolbar.setContentHuggingVerticalHigh() } - private func createButtonRow(mediaMessageView: UIView) { - let buttonTopMargin = ScaleFromIPhone5To7Plus(30, 40) - let buttonBottomMargin = ScaleFromIPhone5To7Plus(25, 40) - let buttonHSpacing = ScaleFromIPhone5To7Plus(20, 30) + private func makeClearToolbar() -> UIToolbar { + let toolbar = UIToolbar() - let buttonRow = UIView() - self.view.addSubview(buttonRow) - buttonRow.autoPinWidthToSuperview() - buttonRow.autoPinEdge(toSuperviewEdge:.bottom, withInset:buttonBottomMargin) - buttonRow.autoPinEdge(.top, to:.bottom, of:mediaMessageView, withOffset:buttonTopMargin) + toolbar.backgroundColor = UIColor.clear - // We use this invisible subview to ensure that the buttons are centered - // horizontally. - let buttonSpacer = UIView() - buttonRow.addSubview(buttonSpacer) - // Vertical positioning of this view doesn't matter. - buttonSpacer.autoPinEdge(toSuperviewEdge:.top) - buttonSpacer.autoSetDimension(.width, toSize:buttonHSpacing) - buttonSpacer.autoHCenterInSuperview() + // Making a toolbar transparent requires setting an empty uiimage + toolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) - let cancelButton = createButton(title: CommonStrings.cancelButton, - color : UIColor.ows_destructiveRed(), - action: #selector(cancelPressed)) - buttonRow.addSubview(cancelButton) - cancelButton.autoPinEdge(toSuperviewEdge:.top) - cancelButton.autoPinEdge(toSuperviewEdge:.bottom) - cancelButton.autoPinEdge(.right, to:.left, of:buttonSpacer) + // hide 1px top-border + toolbar.clipsToBounds = true - let sendButton = createButton(title: NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON", - comment: "Label for 'send' button in the 'attachment approval' dialog."), - color : UIColor(rgbHex:0x2ecc71), - action: #selector(sendPressed)) - buttonRow.addSubview(sendButton) - sendButton.autoPinEdge(toSuperviewEdge:.top) - sendButton.autoPinEdge(toSuperviewEdge:.bottom) - sendButton.autoPinEdge(.left, to:.right, of:buttonSpacer) - } - - private func createButton(title: String, color: UIColor, action: Selector) -> UIView { - let buttonWidth = ScaleFromIPhone5To7Plus(110, 140) - let buttonHeight = ScaleFromIPhone5To7Plus(35, 45) - - return OWSFlatButton.button(title:title, - titleColor:UIColor.white, - backgroundColor:color, - width:buttonWidth, - height:buttonHeight, - target:target, - selector:action) + return toolbar } // MARK: - Event Handlers + @objc + public func playButtonTapped() { + mediaMessageView.playVideo() + } + func cancelPressed(sender: UIButton) { - self.delegate?.didCancelAttachment() + self.delegate?.didCancelAttachment(attachment: attachment) } func sendPressed(sender: UIButton) { + // disable controls after send was tapped. + self.bottomToolbar.isUserInteractionEnabled = false // FIXME // this is just a temporary hack to provide some UI @@ -175,6 +228,90 @@ public class AttachmentApprovalViewController: OWSViewController { activityIndicatorView.autoCenterInSuperview() activityIndicatorView.startAnimating() - self.delegate?.didApproveAttachment() + self.delegate?.didApproveAttachment(attachment: attachment) + } +} + +extension AttachmentApprovalViewController: UIScrollViewDelegate { + + public func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return mediaMessageView + } + + fileprivate func updateMinZoomScaleForSize(_ size: CGSize) { + Logger.debug("\(logTag) in \(#function)") + + // Ensure bounds have been computed + mediaMessageView.layoutIfNeeded() + guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else { + Logger.warn("\(logTag) bad bounds in \(#function)") + return + } + + let widthScale = size.width / mediaMessageView.bounds.width + let heightScale = size.height / mediaMessageView.bounds.height + let minScale = min(widthScale, heightScale) + scrollView.maximumZoomScale = minScale * 5.0 + scrollView.minimumZoomScale = minScale + scrollView.zoomScale = minScale + } + + // Keep the media view centered within the scroll view as you zoom + public func scrollViewDidZoom(_ scrollView: UIScrollView) { + // The scroll view has zoomed, so you need to re-center the contents + let scrollViewSize = self.scrollViewVisibleSize + + // First assume that mediaMessageView center coincides with the contents center + // This is correct when the mediaMessageView is bigger than scrollView due to zoom + var contentCenter = CGPoint(x: (scrollView.contentSize.width / 2), y: (scrollView.contentSize.height / 2)) + + let scrollViewCenter = self.scrollViewCenter + + // if mediaMessageView is smaller than the scrollView visible size - fix the content center accordingly + if self.scrollView.contentSize.width < scrollViewSize.width { + contentCenter.x = scrollViewCenter.x + } + + if self.scrollView.contentSize.height < scrollViewSize.height { + contentCenter.y = scrollViewCenter.y + } + + self.mediaMessageView.center = contentCenter + } + + // return the scroll view center + private var scrollViewCenter: CGPoint { + let size = scrollViewVisibleSize + return CGPoint(x: (size.width / 2), y: (size.height / 2)) + } + + // Return scrollview size without the area overlapping with tab and nav bar. + private var scrollViewVisibleSize: CGSize { + let contentInset = scrollView.contentInset + let scrollViewSize = scrollView.bounds.standardized.size + let width = scrollViewSize.width - (contentInset.left + contentInset.right) + let height = scrollViewSize.height - (contentInset.top + contentInset.bottom) + return CGSize(width: width, height: height) + } +} + +private class GradientView: UIView { + + let gradientLayer = CAGradientLayer() + + required init(from fromColor: UIColor, to toColor: UIColor) { + gradientLayer.colors = [fromColor.cgColor, toColor.cgColor] + super.init(frame: CGRect.zero) + + self.layer.addSublayer(gradientLayer) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer.frame = self.bounds } } diff --git a/SignalMessaging/attachments/MediaMessageView.swift b/SignalMessaging/attachments/MediaMessageView.swift index d8e60e33b3..7bee41fccf 100644 --- a/SignalMessaging/attachments/MediaMessageView.swift +++ b/SignalMessaging/attachments/MediaMessageView.swift @@ -11,6 +11,7 @@ import SignalServiceKit public enum MediaMessageViewMode: UInt { case large case small + case attachmentApproval } @objc @@ -35,6 +36,9 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { @objc public var audioPlayButton: UIButton? + @objc + public var videoPlayButton: UIImageView? + @objc public var playbackState = AudioPlaybackState.stopped { didSet { @@ -129,7 +133,7 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { private func stackSpacing() -> CGFloat { switch mode { - case .large: + case .large, .attachmentApproval: return CGFloat(10) case .small: return CGFloat(5) @@ -250,13 +254,19 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { addSubviewWithScaleAspectFitLayout(view:imageView, aspectRatio:aspectRatio) contentView = imageView - let videoPlayIcon = UIImage(named:"play_button") - let videoPlayButton = UIImageView(image:videoPlayIcon) - imageView.addSubview(videoPlayButton) - videoPlayButton.autoCenterInSuperview() + // attachment approval provides it's own play button to keep it + // at the proper zoom scale. + if mode != .attachmentApproval { + let videoPlayIcon = UIImage(named:"play_button")! + let videoPlayButton = UIImageView(image: videoPlayIcon) + self.videoPlayButton = videoPlayButton + videoPlayButton.contentMode = .scaleAspectFit + self.addSubview(videoPlayButton) + videoPlayButton.autoCenterInSuperview() - imageView.isUserInteractionEnabled = true - imageView.addGestureRecognizer(UITapGestureRecognizer(target:self, action:#selector(videoTapped))) + imageView.isUserInteractionEnabled = true + imageView.addGestureRecognizer(UITapGestureRecognizer(target:self, action:#selector(videoTapped))) + } } private func createGenericPreview() { @@ -282,7 +292,7 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { private func createHeroViewSize() -> CGFloat { switch mode { - case .large: + case .large, .attachmentApproval: return ScaleFromIPhone5To7Plus(175, 225) case .small: return ScaleFromIPhone5To7Plus(80, 80) @@ -310,7 +320,7 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { private func labelFont() -> UIFont { switch mode { - case .large: + case .large, .attachmentApproval: return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) case .small: return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) @@ -416,6 +426,10 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { @objc func imageTapped(sender: UIGestureRecognizer) { + // Approval view handles it's own zooming gesture + guard mode != .attachmentApproval else { + return + } guard sender.state == .recognized else { return } @@ -430,16 +444,25 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { let convertedRect = fromView.convert(fromView.bounds, to:window) let viewController = FullImageViewController(attachment:attachment, from:convertedRect) viewController.present(from:fromViewController) - Logger.error("\(TAG) FIXME. image tapped.") } // MARK: - Video Playback @objc func videoTapped(sender: UIGestureRecognizer) { + // Approval view handles it's own play gesture + guard mode != .attachmentApproval else { + return + } guard sender.state == .recognized else { return } + + playVideo() + } + + @objc + public func playVideo() { guard let dataUrl = attachment.dataUrl else { return } diff --git a/SignalMessaging/attachments/SharingThreadPickerViewController.m b/SignalMessaging/attachments/SharingThreadPickerViewController.m index d14781b62e..4cb006518e 100644 --- a/SignalMessaging/attachments/SharingThreadPickerViewController.m +++ b/SignalMessaging/attachments/SharingThreadPickerViewController.m @@ -150,7 +150,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - AttachmentApprovalViewControllerDelegate -- (void)didApproveAttachment +- (void)didApproveAttachmentWithAttachment:(SignalAttachment *)attachment { [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; [ThreadUtil sendMessageWithAttachment:self.attachment inThread:self.thread messageSender:self.messageSender]; @@ -163,7 +163,7 @@ NS_ASSUME_NONNULL_BEGIN }); } -- (void)didCancelAttachment +- (void)didCancelAttachmentWithAttachment:(SignalAttachment *)attachment { [self cancelShareExperience]; } diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index bed1ade89d..ffbfbaa1a2 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -124,7 +124,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) - (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio { // Clamp to ensure view has reasonable aspect ratio. - CGFloat clampedRatio = Clamp(ratio, 0.5, 95.0); + CGFloat clampedRatio = Clamp(ratio, 0.05, 95.0); if (clampedRatio != ratio) { OWSFail(@"Invalid aspect ratio: %f for view: %@", ratio, self); } diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index 453cbc407c..55a3a8ddb2 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -15,6 +15,8 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE private var hasInitialRootViewController = false private var isReadyForAppExtensions = false + var loadViewController: SAELoadViewController! + override open func loadView() { super.loadView() @@ -84,7 +86,7 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE // upgrade process may depend on Environment. VersionMigrations.performUpdateCheck() - let loadViewController = SAELoadViewController(delegate:self) + self.loadViewController = SAELoadViewController(delegate:self) self.pushViewController(loadViewController, animated: false) self.isNavigationBarHidden = true @@ -342,15 +344,21 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE // MARK: ShareViewDelegate, SAEFailedViewDelegate public func shareViewWasCompleted() { - self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + self.dismiss(animated: true) { + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } } public func shareViewWasCancelled() { - self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + self.dismiss(animated: true) { + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } } public func shareViewFailed(error: Error) { - self.extensionContext!.cancelRequest(withError: error) + self.dismiss(animated: true) { + self.extensionContext!.cancelRequest(withError: error) + } } // MARK: Helpers @@ -358,6 +366,11 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE private func presentConversationPicker() { // pause any animation revealing the "loading" screen self.view.layer.removeAllAnimations() + + // Once we've presented the conversation picker, we hide the loading VC + // so that it's not revealed when we eventually dismiss the share extension. + loadViewController.view.isHidden = true + self.buildAttachment().then { attachment -> Void in let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: self) let navigationController = UINavigationController(rootViewController: conversationPicker) @@ -378,6 +391,8 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE enum ShareViewControllerError: Error { case assertionError(description: String) + case unsupportedMedia + } private func buildAttachment() -> Promise { @@ -394,13 +409,22 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE } Logger.info("\(self.logTag) attachment: \(itemProvider)") - // TODO support other utiTypes - let utiType = kUTTypeImage as String + // Order matters if we want to take advantage of share conversion in loadItem, + // Though currently we just use "data" for most things and rely on our SignalAttachment + // class to convert types for us. + let utiTypes: [String] = [kUTTypeImage as String, + kUTTypeURL as String, + kUTTypeData as String] - guard itemProvider.hasItemConformingToTypeIdentifier(utiType) else { - let error = ShareViewControllerError.assertionError(description: "only supporting images for now") + let matchingUtiType = utiTypes.first { (utiType: String) -> Bool in + itemProvider.hasItemConformingToTypeIdentifier(utiType) + } + + guard let utiType = matchingUtiType else { + let error = ShareViewControllerError.unsupportedMedia return Promise(error: error) } + Logger.debug("\(logTag) matched utiType: \(utiType)") let (promise, fulfill, reject) = Promise.pending()