From 1a4062dd89a97dd4c5cdb8e06a9bd9406abc91a4 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 28 Mar 2019 06:37:23 -0600 Subject: [PATCH] Nav buttons: batch, camera/library switch, done --- .../Contents.json | 23 ++ .../create-album-outline-32@1x.png | Bin 0 -> 477 bytes .../create-album-outline-32@2x.png | Bin 0 -> 813 bytes .../create-album-outline-32@3x.png | Bin 0 -> 1208 bytes .../Photos/ImagePickerController.swift | 115 +----- .../SendMediaNavigationController.swift | 335 ++++++++++++++++-- SignalMessaging/categories/UIFont+OWS.h | 1 + SignalMessaging/categories/UIFont+OWS.m | 6 + SignalMessaging/categories/UIView+OWS.h | 1 + SignalMessaging/categories/UIView+OWS.m | 9 +- 10 files changed, 360 insertions(+), 130 deletions(-) create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png create mode 100644 Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json new file mode 100644 index 000000000..ae32da7a5 --- /dev/null +++ b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "create-album-outline-32@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "create-album-outline-32@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "create-album-outline-32@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..d23c007d13f7c74ecb487527c2e2ffa568ca4936 GIT binary patch literal 477 zcmV<30V4j1P)ff2ev!v^UF zjF3*Co;W#YED;<+6MB7jCUN}r{3OeQm>F~8Vb;~}fdIq=X#ubSaFFH^r3J79Pqy1jxp66hFrd_W1)v^j zie7V^n1G0+vS;Rsh-C54NJd}6Jgfjby5>j}ke^!L!_Hql=?0pkPz8)R`Uo6-!JBXd z5!rbgf4dY1#>noWQB!pI`;kzbRh(Cyc6tJ1(Geh`a#jM&d@!vOU@>$F$JVRxB%+fB zX0FWyDK5l;+BwT!sCA1oHUzgSd>gC)fM-X6cK~mC&T}^1DKnTrDr+QrlO=hAy9`8y z`7$AaZ^>kcXl;5 zT?XUja#s*=Ga{3g2M|%Ak!z{W*%xHw9_hQx@^V6lb!-kd+}<=Ro&*2<&nbW(iTSP3 Tbda`p00000NkvXXu0mjfzh=!U literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9ccabb7c69fce53efb9b3b2cb0d4a6d58bbf66a5 GIT binary patch literal 813 zcmV+|1JeA7P)h5!$HpY63(cTNF-&KQ-utRcXi9CHn| zTZlozkdpxa>6=fUhvjb$hn*3lbIR^g5|lzXwiSktMtR4MmpCyduMSEf96X2yH%9qr zbbNQKLu?}?XW6sa9ueJ>|L)a)XVOOQeCgHU$=QvF@zud%g`coFxo~H)%r(BU+PH_+ zK`9al@Z-J+=RMn#SBEJtL%q0uC!&L@u`r0ptzaL*ir0`u*S-xF=e)pHT(49qV+sM! z;yOIJEDt$xKZmTjb_C2l+h2kt+p0ehX#^W74lQ r?axuAsSJ$(rKt>!fC6k-4gl~E>TFL#pFtBG00000NkvXXu0mjfl8j{` literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png b/Signal/Images.xcassets/media_send_batch_mode_disabled.imageset/create-album-outline-32@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b13aa24b3e1399da20c58955939905ac51468983 GIT binary patch literal 1208 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9FJ<{->y95KI&fq^B< z)5S5QV$R#S7YlD2@U%6?-Ji@miT4%nth@=luY3ghC*(}Z7W zfB6YIOjOczT=wzq!`4Z;LH3+NTUZnY8y6lI-~DaZ42DyfsAlcY&m9%a4DPQ#&7HgE z{OW*aU4}OuFU+ltMD>L93>kK?G_W(=*u6gc2;V;6=}WJxr!5u#zilJ;ee>(vq*uOu z#l&!6a=^o|T;14ascBm!+dK3bK1`P7`24K>YfwSn=i4WC-4VRVt*yJX=)C=}b5pzH?AqKr`Wq23F^e1`QmI4|)XdLG>&*!A2tOd;~XU!Gr1_irz?T2Ef){YTGDl4@l6hn zIpR^P4aoL*CE9#;4=Y(Ftye+pfUp@MM zOM2F8HgOG8mK$8pJ#LGHHcxBqQs`}IE?Jji`hQZ)#!39sub8-O`REuuom;-bZKY;| z$iuuy&)FV<8?PuzPX4Fq?@}FhLoLXCvX#o=cU>zSG}OEfvIHChqh-q&{{H=AUgmcD z%k9};{vVs7oZu;)T|D9Hy|fqsr98ojR%*Ju;_YV{(c|BR^@IOj3E_!GOq)A zg+;pV+@3-QmaA637me3_=~=P;v!~0Ajguu7CO>0%nfamG#pcPfc#lk{4^_`dSq@f z3NFuWxf1()!X}+5nns$-&9pVv^n^@Qe|e{kCBo;NMf;R3Tcp>Rw&q{odqGVzW9r0^ znF$t?)?Ycf|AFGA&zh<$e=dJNk&9bfEj8-Bn(1Ahr*q?exUQdPSw6`;>-R!Sv-*;? zcP>`j3weKE^sMq?{}QjI_k>Pq-CmWN>YvUkykqgghqsz!)#~dV7ILiJ=eqdRYd-av z%uX)%l#8e7|CtlY(44$|sYWsrCdy!9_|MQZLF) func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) + var isInBatchSelectMode: Bool { get } func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool } @@ -76,8 +77,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat navigationItem.titleView = titleView self.titleView = titleView - updateSelectButton() - collectionView.backgroundColor = .ows_gray95 let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection)) @@ -94,10 +93,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat @objc func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) { - guard isInBatchSelectMode else { - return - } - guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return @@ -108,6 +103,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } + guard delegate.isInBatchSelectMode else { + return + } + switch selectionPanGesture.state { case .possible: break @@ -138,11 +137,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } func tryToToggleBatchSelect(at indexPath: IndexPath) { - guard isInBatchSelectMode else { - owsFailDebug("isInBatchSelectMode was unexpectedly false") - return - } - guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return @@ -153,6 +147,11 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } + guard delegate.isInBatchSelectMode else { + owsFailDebug("isInBatchSelectMode was unexpectedly false") + return + } + let asset = photoCollectionContents.asset(at: indexPath.item) switch selectionPanGestureMode { case .select: @@ -168,8 +167,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat delegate.imagePicker(self, didDeselectAsset: asset) collectionView.deselectItem(at: indexPath, animated: true) } - - updateDoneButton() } override func viewWillLayoutSubviews() { @@ -181,12 +178,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let navBar = self.navigationController?.navigationBar as? OWSNavigationBar { - navBar.overrideTheme(type: .alwaysDark) - } else { - owsFailDebug("Invalid nav bar.") - } - // Determine the size of the thumbnails to request let scale = UIScreen.main.scale let cellSize = collectionViewFlowLayout.itemSize @@ -217,10 +208,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat super.viewDidAppear(animated) hasEverAppeared = true - // done button may have been disable from the last time we hit "Done" - // make sure to re-enable it if appropriate upon returning to the view - hasPressedDoneSinceAppeared = false - updateDoneButton() // Since we're presenting *over* the ConversationVC, we need to `becomeFirstResponder`. // @@ -332,78 +319,18 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - Batch Selection - lazy var doneButton: UIBarButtonItem = { - return UIBarButtonItem(barButtonSystemItem: .done, - target: self, - action: #selector(didPressDone)) - }() - - lazy var selectButton: UIBarButtonItem = { - return UIBarButtonItem(title: NSLocalizedString("BUTTON_SELECT", comment: "Button text to enable batch selection mode"), - style: .plain, - target: self, - action: #selector(didTapSelect)) - }() - - var isInBatchSelectMode = false { - didSet { - collectionView!.allowsMultipleSelection = isInBatchSelectMode - updateSelectButton() - updateDoneButton() - } - } - - @objc - func didPressDone(_ sender: Any) { - Logger.debug("") - + func batchSelectModeDidChange() { guard let delegate = delegate else { - owsFailDebug("delegate was unexpectedly nil") return } - hasPressedDoneSinceAppeared = true - updateDoneButton() - - delegate.imagePickerDidCompleteSelection(self) - } - - var hasPressedDoneSinceAppeared: Bool = false - func updateDoneButton() { - guard let collectionView = self.collectionView else { + guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return } - guard !hasPressedDoneSinceAppeared else { - doneButton.isEnabled = false - return - } - - if let count = collectionView.indexPathsForSelectedItems?.count, count > 0 { - doneButton.isEnabled = true - } else { - doneButton.isEnabled = false - } - } - - func updateSelectButton() { - guard !isShowingCollectionPickerController else { - navigationItem.rightBarButtonItem = nil - return - } - - let button = isInBatchSelectMode ? doneButton : selectButton - button.tintColor = .ows_gray05 - navigationItem.rightBarButtonItem = button - } - - @objc - func didTapSelect(_ sender: Any) { - isInBatchSelectMode = true - - // disabled until at least one item is selected - self.doneButton.isEnabled = false + collectionView.allowsMultipleSelection = delegate.isInBatchSelectMode + collectionView.reloadData() } func clearCollectionViewSelection() { @@ -477,9 +404,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) { collectionPickerView.superview?.layoutIfNeeded() - - self.updateSelectButton() - self.titleView.rotateIcon(.up) }.retainUntilComplete() } @@ -494,9 +418,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) { collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height) - - self.updateSelectButton() - self.titleView.rotateIcon(.down) }.done { _ in collectionPickerController.view.removeFromSuperview() @@ -550,9 +471,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let attachmentPromise: Promise = photoCollectionContents.outgoingAttachment(for: asset) delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise) - if isInBatchSelectMode { - updateDoneButton() - } else { + if !delegate.isInBatchSelectMode { // Don't show "selected" badge unless we're in batch mode collectionView.deselectItem(at: indexPath, animated: false) delegate.imagePickerDidCompleteSelection(self) @@ -568,10 +487,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let asset = photoCollectionContents.asset(at: indexPath.item) delegate.imagePicker(self, didDeselectAsset: asset) - - if isInBatchSelectMode { - updateDoneButton() - } } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 11eac7a05..e2e7a41b7 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -19,6 +19,32 @@ class SendMediaNavigationController: OWSNavigationController { override var prefersStatusBarHidden: Bool { return true } + override func viewDidLoad() { + super.viewDidLoad() + + self.delegate = self + + view.addSubview(batchModeButton) + batchModeButton.setCompressionResistanceHigh() + batchModeButton.autoPinEdge(toSuperviewMargin: .bottom) + batchModeButton.autoPinEdge(toSuperviewMargin: .trailing) + + view.addSubview(doneButton) + doneButton.setCompressionResistanceHigh() + doneButton.autoPinEdge(toSuperviewMargin: .bottom) + doneButton.autoPinEdge(toSuperviewMargin: .trailing) + + view.addSubview(cameraModeButton) + cameraModeButton.setCompressionResistanceHigh() + cameraModeButton.autoPinEdge(toSuperviewMargin: .bottom) + cameraModeButton.autoPinEdge(toSuperviewMargin: .leading) + + view.addSubview(mediaLibraryModeButton) + mediaLibraryModeButton.setCompressionResistanceHigh() + mediaLibraryModeButton.autoPinEdge(toSuperviewMargin: .bottom) + mediaLibraryModeButton.autoPinEdge(toSuperviewMargin: .leading) + } + // MARK: - @objc @@ -27,13 +53,8 @@ class SendMediaNavigationController: OWSNavigationController { @objc public class func showingCameraFirst() -> SendMediaNavigationController { let navController = SendMediaNavigationController() - - if let owsNavBar = navController.navigationBar as? OWSNavigationBar { - owsNavBar.overrideTheme(type: .clear) - } else { - owsFailDebug("unexpected navbar: \(navController.navigationBar)") - } navController.setViewControllers([navController.captureViewController], animated: false) + navController.updateButtons() return navController } @@ -41,18 +62,126 @@ class SendMediaNavigationController: OWSNavigationController { @objc public class func showingMediaLibraryFirst() -> SendMediaNavigationController { let navController = SendMediaNavigationController() - - if let owsNavBar = navController.navigationBar as? OWSNavigationBar { - owsNavBar.overrideTheme(type: .clear) - } else { - owsFailDebug("unexpected navbar: \(navController.navigationBar)") - } navController.setViewControllers([navController.mediaLibraryViewController], animated: false) + navController.updateButtons() return navController } - // MARK: + var isInBatchSelectMode = false { + didSet { + if oldValue != isInBatchSelectMode { + updateButtons() + mediaLibraryViewController.batchSelectModeDidChange() + } + } + } + + func updateButtons() { + guard let topViewController = viewControllers.last else { + return + } + + switch topViewController { + case is AttachmentApprovalViewController: + batchModeButton.isHidden = true + doneButton.isHidden = true + cameraModeButton.isHidden = true + mediaLibraryModeButton.isHidden = true + case is ImagePickerGridController: + batchModeButton.isHidden = isInBatchSelectMode + doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0) + cameraModeButton.isHidden = false + mediaLibraryModeButton.isHidden = true + case is PhotoCaptureViewController: + batchModeButton.isHidden = isInBatchSelectMode + doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0) + cameraModeButton.isHidden = true + mediaLibraryModeButton.isHidden = false + default: + owsFailDebug("unexpected topViewController: \(topViewController)") + } + + doneButton.updateCount() + } + + func fadeTo(viewControllers: [UIViewController]) { + let transition: CATransition = CATransition() + transition.duration = 0.1 + transition.type = kCATransitionFade + view.layer.add(transition, forKey: nil) + setViewControllers(viewControllers, animated: false) + } + + // MARK: - Events + + private func didTapBatchModeButton() { + // There's no way to _disable_ batch mode. + isInBatchSelectMode = true + } + + private func didTapCameraModeButton() { + fadeTo(viewControllers: [captureViewController]) + updateButtons() + } + + private func didTapMediaLibraryModeButton() { + fadeTo(viewControllers: [mediaLibraryViewController]) + updateButtons() + } + + // MARK: Views + + private lazy var doneButton: DoneButton = { + let button = DoneButton() + button.delegate = self + + return button + }() + + private lazy var batchModeButton: UIButton = { + let button = OWSButton(imageName: "media_send_batch_mode_disabled", + tintColor: .ows_gray60, + block: { [weak self] in self?.didTapBatchModeButton() }) + + let width: CGFloat = 44 + button.autoSetDimensions(to: CGSize(width: width, height: width)) + button.layer.cornerRadius = width / 2 + button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + button.backgroundColor = .ows_white + + return button + }() + + private lazy var cameraModeButton: UIButton = { + let button = OWSButton(imageName: "settings-avatar-camera-2", + tintColor: .ows_gray60, + block: { [weak self] in self?.didTapCameraModeButton() }) + + let width: CGFloat = 44 + button.autoSetDimensions(to: CGSize(width: width, height: width)) + button.layer.cornerRadius = width / 2 + button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + button.backgroundColor = .ows_white + + return button + }() + + private lazy var mediaLibraryModeButton: UIButton = { + let button = OWSButton(imageName: "actionsheet_camera_roll_black", + tintColor: .ows_gray60, + block: { [weak self] in self?.didTapMediaLibraryModeButton() }) + + let width: CGFloat = 44 + button.autoSetDimensions(to: CGSize(width: width, height: width)) + button.layer.cornerRadius = width / 2 + button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + button.backgroundColor = .ows_white + + return button + }() + + // MARK: State private var attachmentDraftCollection: AttachmentDraftCollection = .empty @@ -83,22 +212,60 @@ class SendMediaNavigationController: OWSNavigationController { approvalViewController.approvalDelegate = self pushViewController(approvalViewController, animated: true) + updateButtons() + } +} + +extension SendMediaNavigationController: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + if let navbarTheme = preferredNavbarTheme(viewController: viewController) { + if let owsNavBar = navigationBar as? OWSNavigationBar { + owsNavBar.overrideTheme(type: navbarTheme) + } else { + owsFailDebug("unexpected navigationBar: \(navigationBar)") + } + } + } + + // In case back navigation was canceled, we re-apply whatever is showing. + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + if let navbarTheme = preferredNavbarTheme(viewController: viewController) { + if let owsNavBar = navigationBar as? OWSNavigationBar { + owsNavBar.overrideTheme(type: navbarTheme) + } else { + owsFailDebug("unexpected navigationBar: \(navigationBar)") + } + } + } + + // MARK: - Helpers + + private func preferredNavbarTheme(viewController: UIViewController) -> OWSNavigationBar.NavigationBarThemeOverride? { + switch viewController { + case is AttachmentApprovalViewController: + return .clear + case is ImagePickerGridController: + return .alwaysDark + case is PhotoCaptureViewController: + return .clear + default: + owsFailDebug("unexpected viewController: \(viewController)") + return nil + } } } extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) { attachmentDraftCollection.append(.camera(attachment: attachment)) - - pushApprovalViewController() + if isInBatchSelectMode { + updateButtons() + } else { + pushApprovalViewController() + } } func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) { - // TODO - // sometimes we might want this to be a "back" to the approval view - // other times we might want this to be a "close" and take me back to the CVC - // seems like we should show the "back" and have a seprate "didTapBack" delegate method or something... - self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) } } @@ -106,6 +273,10 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { extension SendMediaNavigationController: ImagePickerGridControllerDelegate { func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) { + showApprovalAfterProcessingAnyMediaLibrarySelections() + } + + func showApprovalAfterProcessingAnyMediaLibrarySelections() { let mediaLibrarySelections: [MediaLibrarySelection] = self.mediaLibrarySelections.orderedValues let backgroundBlock: (ModalActivityIndicatorViewController) -> Void = { modal in @@ -141,11 +312,15 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { let libraryMedia = MediaLibrarySelection(asset: asset, signalAttachmentPromise: attachmentPromise) mediaLibrarySelections.append(key: asset, value: libraryMedia) + + updateButtons() } func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) { if mediaLibrarySelections.hasValue(forKey: asset) { mediaLibrarySelections.remove(key: asset) + + updateButtons() } } @@ -184,19 +359,21 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat assert(viewControllers.count == 2) // regardless of which VC we're going "back" to, we're in "batch" mode at this point. - mediaLibraryViewController.isInBatchSelectMode = true - mediaLibraryViewController.collectionView?.reloadData() + isInBatchSelectMode = true + mediaLibraryViewController.batchSelectModeDidChange() - popViewController(animated: true) + popViewController(animated: true) { + self.updateButtons() + } } } -enum AttachmentDraft { +private enum AttachmentDraft { case camera(attachment: SignalAttachment) case picker(attachment: MediaLibraryAttachment) } -extension AttachmentDraft { +private extension AttachmentDraft { var attachment: SignalAttachment { switch self { case .camera(let cameraAttachment): @@ -211,7 +388,7 @@ extension AttachmentDraft { } } -struct AttachmentDraftCollection { +private struct AttachmentDraftCollection { private(set) var attachmentDrafts: [AttachmentDraft] static var empty: AttachmentDraftCollection { @@ -261,7 +438,7 @@ struct AttachmentDraftCollection { } } -struct MediaLibrarySelection: Hashable, Equatable { +private struct MediaLibrarySelection: Hashable, Equatable { let asset: PHAsset let signalAttachmentPromise: Promise @@ -281,7 +458,7 @@ struct MediaLibrarySelection: Hashable, Equatable { } } -struct MediaLibraryAttachment: Hashable, Equatable { +private struct MediaLibraryAttachment: Hashable, Equatable { let asset: PHAsset let signalAttachment: SignalAttachment @@ -293,3 +470,105 @@ struct MediaLibraryAttachment: Hashable, Equatable { return lhs.asset == rhs.asset } } + +extension SendMediaNavigationController: DoneButtonDelegate { + var doneButtonCount: Int { + return attachmentDraftCollection.count - attachmentDraftCollection.pickerAttachments.count + mediaLibrarySelections.count + } + + fileprivate func doneButtonWasTapped(_ doneButton: DoneButton) { + assert(attachmentDraftCollection.count > 0 || mediaLibrarySelections.count > 0) + showApprovalAfterProcessingAnyMediaLibrarySelections() + } +} + +private protocol DoneButtonDelegate: AnyObject { + func doneButtonWasTapped(_ doneButton: DoneButton) + var doneButtonCount: Int { get } +} + +private class DoneButton: UIView { + weak var delegate: DoneButtonDelegate? + + init() { + super.init(frame: .zero) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:))) + addGestureRecognizer(tapGesture) + + let container = UIView() + container.backgroundColor = .ows_white + container.layer.cornerRadius = 20 + container.layoutMargins = UIEdgeInsets(top: 7, leading: 8, bottom: 7, trailing: 8) + + addSubview(container) + container.autoPinEdgesToSuperviewMargins() + + let stackView = UIStackView(arrangedSubviews: [badge, chevron]) + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = 9 + + container.addSubview(stackView) + stackView.autoPinEdgesToSuperviewMargins() + } + + let numberFormatter: NumberFormatter = NumberFormatter() + + func updateCount() { + guard let delegate = delegate else { + return + } + + badgeLabel.text = numberFormatter.string(for: delegate.doneButtonCount) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Subviews + + private lazy var badge: UIView = { + let badge = CircleView() + badge.layoutMargins = UIEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4) + badge.backgroundColor = .ows_signalBlue + badge.addSubview(badgeLabel) + badgeLabel.autoPinEdgesToSuperviewMargins() + + // Constrain to be a pill that is at least a circle, and maybe wider. + badgeLabel.autoPin(toAspectRatio: 1.0, relation: .greaterThanOrEqual) + NSLayoutConstraint.autoSetPriority(.defaultLow) { + badgeLabel.autoPinToSquareAspectRatio() + } + + return badge + }() + + private lazy var badgeLabel: UILabel = { + let label = UILabel() + label.textColor = .ows_white + label.font = UIFont.ows_dynamicTypeSubheadline.ows_monospaced() + label.textAlignment = .center + return label + }() + + private lazy var chevron: UIView = { + let image: UIImage + if CurrentAppContext().isRTL { + image = #imageLiteral(resourceName: "small_chevron_left") + } else { + image = #imageLiteral(resourceName: "small_chevron_right") + } + let chevron = UIImageView(image: image.withRenderingMode(.alwaysTemplate)) + chevron.contentMode = .scaleAspectFit + chevron.tintColor = .ows_gray60 + chevron.autoSetDimensions(to: CGSize(width: 10, height: 18)) + + return chevron + }() + + @objc + func didTap(tapGesture: UITapGestureRecognizer) { + delegate?.doneButtonWasTapped(self) + } +} diff --git a/SignalMessaging/categories/UIFont+OWS.h b/SignalMessaging/categories/UIFont+OWS.h index 2e4dc57b1..741ed989b 100644 --- a/SignalMessaging/categories/UIFont+OWS.h +++ b/SignalMessaging/categories/UIFont+OWS.h @@ -56,6 +56,7 @@ NS_ASSUME_NONNULL_BEGIN - (UIFont *)ows_italic; - (UIFont *)ows_bold; - (UIFont *)ows_mediumWeight; +- (UIFont *)ows_monospaced; @end diff --git a/SignalMessaging/categories/UIFont+OWS.m b/SignalMessaging/categories/UIFont+OWS.m index 8f3675773..14b80f75c 100644 --- a/SignalMessaging/categories/UIFont+OWS.m +++ b/SignalMessaging/categories/UIFont+OWS.m @@ -229,6 +229,12 @@ NS_ASSUME_NONNULL_BEGIN return derivedFont; } +- (UIFont *)ows_monospaced +{ + return [self.class ows_monospacedDigitFontWithSize:self.pointSize]; +} + + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 6ca66a471..64e24b4ce 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -43,6 +43,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); - (NSLayoutConstraint *)autoPinToSquareAspectRatio; - (NSLayoutConstraint *)autoPinToAspectRatioWithSize:(CGSize)size; - (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio; +- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio relation:(NSLayoutRelation)relation; #pragma mark - Content Hugging and Compression Resistance diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index e58f9354f..23263bd87 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -2,8 +2,8 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // -#import "OWSMath.h" #import "UIView+OWS.h" +#import "OWSMath.h" #import #import #import @@ -148,6 +148,11 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) } - (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio +{ + return [self autoPinToAspectRatio:ratio relation:NSLayoutRelationEqual]; +} + +- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio relation:(NSLayoutRelation)relation { // Clamp to ensure view has reasonable aspect ratio. CGFloat clampedRatio = CGFloatClamp(ratio, 0.05f, 95.0f); @@ -158,7 +163,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) self.translatesAutoresizingMaskIntoConstraints = NO; NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual + relatedBy:relation toItem:self attribute:NSLayoutAttributeHeight multiplier:clampedRatio