From d3e7c99a63734e246b452dbcae1690c62d7d921a Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 8 Dec 2017 14:13:52 -0500 Subject: [PATCH] Attachment approval: cancel/confirm to top/bottom toolbars - avoid double-submit by disabling after hitting send. - Animate SAE dismissal as modal Requires hiding the load view - attachment is centered in screen - add gradient to ensure controls are visible - Fix clamp constraints - I think this was a typo Otherwise we couldn't show an image which was 1" wide by 2" tall - set max zoom, hide scroll indicators - use media view - slower panning feels right - white send button - can share GIF, static, and sort of video... - Play button for attachment approval - move to image assets so we can use it in SAE - slightly larger button for full-screen approval view - don't launch redundant fullscreen UI for images when in approval view - fix scrollOffset in AttachmentApproval - consolidate view initialization logic in loadView and fix white background - CR: more legible arithmetic // FREEBIE --- Signal.xcodeproj/project.pbxproj | 8 - .../play_button.imageset/Contents.json | 21 ++ .../play_button.imageset}/play_button@2x.png | Bin .../play_button_large.imageset/Contents.json | 22 ++ .../play_button_large.png | Bin 0 -> 4436 bytes .../play_button_large@2x.png | Bin 0 -> 8487 bytes Signal/Images/play_button.png | Bin 2369 -> 0 bytes .../AttachmentApprovalViewController.swift | 295 +++++++++++++----- .../attachments/MediaMessageView.swift | 43 ++- .../SharingThreadPickerViewController.m | 4 +- SignalMessaging/categories/UIView+OWS.m | 2 +- .../ShareViewController.swift | 40 ++- 12 files changed, 327 insertions(+), 108 deletions(-) create mode 100644 Signal/Images.xcassets/play_button.imageset/Contents.json rename Signal/{Images => Images.xcassets/play_button.imageset}/play_button@2x.png (100%) create mode 100644 Signal/Images.xcassets/play_button_large.imageset/Contents.json create mode 100644 Signal/Images.xcassets/play_button_large.imageset/play_button_large.png create mode 100644 Signal/Images.xcassets/play_button_large.imageset/play_button_large@2x.png delete mode 100644 Signal/Images/play_button.png 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 0000000000000000000000000000000000000000..7f5f552aba5e9ec1eb7dd91475f5f622a4bf4001 GIT binary patch literal 4436 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 Ax&QzG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b4e394f27d40d487a3d80960c1537e52d4451197 GIT binary patch literal 8487 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 literal 0 HcmV?d00001 diff --git a/Signal/Images/play_button.png b/Signal/Images/play_button.png deleted file mode 100644 index f18452d20b91c74849ac3483a4c9d8e360bc03fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2369 zcmaJ@X;@QN8V);3B2W}jk%@^YTUH<&K_mo}C4hj6m4aC=5Hz_VxtNe)FtR9U5iKbD zDg+co1P7_IJyk#!fhrV5Dk3PcQv|FFwWv+3*!eNedG5XEobUPG_j|wP$4w9P--cMe zb~y|NLwIjz2B~+V<~7t;f34ioE~~dSkY^|q3`Rmywitl9^FR)O^cJwAfFOX)i;wLF z=rEYhMt(>r6w30WaX|r=t?|Li1R^yW2BW*lL~QPU077zrD8A4YHG91ch2-;GQM;X4 zc$UZmh~{sXi-BOde+XB;pG)PT+_oU;GMbt|06=V{OmILbp~+lPA9-o&v&M`=AwRl6 z`(06gi3()}B0WGcfON(>Vz_t$0ZAfZ2~N%=62SpU#1n`(y!s(w2u?I-5{*bkep)DX zG%+ud7R2=W6ia<`MMXo9h=#*SrBbZa5etf=a0Duqs^K6KF=`KtBwh%yWf-9Zy~Mx- zBwR6H1o1&3Qp3pRfN_v3O0D#-6a=Erv_i?JGN}uOld(lO0v4}HX~~hr`v0y1!Dnv? z6a@UO_kR*gLgGaLE(nl-abm8zagk_EC=ty=46q?k90Gy|mZ}&S4MLzK8WbTtf=Ngl z7Msf#Y7Dj?5iAzXTPT6pLN4IVbVaE-uzWs`=1K8za(472Qz;~M-94S$DehE?yC(tf zPNq6i$sS8sCdiEw077U9%ljKk`65;`3<8llG7}K<4+1ZunK%DVo31ycBZYbGGq{JiHXjZ5%zO6RZtp(i9G9L#*QXm7 zmKBVr&p$<{`{<9gTg;Ij4S8%g7g<#+>*k36@~ddR25;6&_#yaK^485Kbq+{gw6i0X zmWwBAXNK)6%vGhh*Yyi)%~RJ^TQtMNcGyqxON$u%ob^Tb6|;(I#lJ&S_XSCH5+7G1 zIua0cqo(pzV&ge%>Rqg_pL*ZqcrrGIH z#8-JaiU;Ur)NMb@3#@B@w~hF^`usPWbq^gnOuJAq!3eT^3zt7(Kc;Ok)_SvlnQdkl znr3;T@71fgi^-;}v@+MT(Iy`9Bh`QtYg5+P_;|^VUAxYZ9UZgIH%LY8vpW6yM%`tL zgN2Vs1+4snf`*WtJBwmKaNvc!CCUHcsdKJ$Q-_M|o?4|5Cx8Lh*U8%Gtx~Bd;d}PP z%MTv>>3o(&A#v5@f-S?Ul%MvRU9NA$;nfV=u>u12_V)G$#Kpy}AAj(GTd2p@>(zhG z*g)34IvWURASYI8)N8ZwsfU%EvD5Ce6-uR# zudnaZ(b3T{butA*DS5Xf%9?`>U{ut$b}Jo5?eNG5s5=mIHf&^eEgGYm+|c<%oSv7L z_rAZs|Flx6w09@3KJG}jnZlhwmA7iOjpG&)wv=7FCadGrAnJu$U$*&%5&xzpo zqdI%hD!te?T2+oRQW9Q8?P?>?9e?;g%&BoopbR)v!ziNKjQ9pX;&WnMmsVVKW zdrwV#4CHom)_xU-_`LvCQj#`qSH-bo$1vC8i*+hfhEoTf|1ik@ zRoeh(h`;eoL4N+*3}%{TOQ}Wwy*A*m>x2#?@|JyfOxqe4NGPlYWina!glMs-xVW{; zB`mr5aP!%a(ZQC8F@-EMY|}Z@B&*90X0Fqg1;VA#TMY8E zu&p13zeGMvPTrg|flbT2o_NQElXL<#cRg0plA~9w->{ug2QR4-c$@h;+;ELZxG*~?}98MKLl z%C7oJu_^|0r*9sABlqUflR11|3MCyM%rQQdmS)t>+=8!Tta>?F9ahCDdPgy`oM`Wo z$So)40uz5Zn1a41x>?LKFdklr9trqQ*;;tzal&S^qg?-{tcVWnuEP3rHa1 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()