mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			821 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			821 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import Foundation
 | |
| import AVFoundation
 | |
| import MediaPlayer
 | |
| import PromiseKit
 | |
| import SessionUIKit
 | |
| import CoreServices
 | |
| 
 | |
| @objc
 | |
| public protocol AttachmentApprovalViewControllerDelegate: class {
 | |
|     func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController,
 | |
|                             didApproveAttachments attachments: [SignalAttachment], messageText: String?)
 | |
| 
 | |
|     func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController)
 | |
| 
 | |
|     func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController,
 | |
|                             didChangeMessageText newMessageText: String?)
 | |
| 
 | |
|     @objc
 | |
|     optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment)
 | |
| 
 | |
|     @objc
 | |
|     optional func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController)
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| @objc
 | |
| public enum AttachmentApprovalViewControllerMode: UInt {
 | |
|     case modal
 | |
|     case sharedNavigation
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| @objc
 | |
| public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
 | |
| 
 | |
|     // MARK: - Properties
 | |
| 
 | |
|     private let mode: AttachmentApprovalViewControllerMode
 | |
|     private let isAddMoreVisible: Bool
 | |
| 
 | |
|     public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
 | |
| 
 | |
|     public var isEditingCaptions = false {
 | |
|         didSet {
 | |
|             updateContents()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Initializers
 | |
| 
 | |
|     @available(*, unavailable, message:"use attachment: constructor instead.")
 | |
|     required public init?(coder aDecoder: NSCoder) {
 | |
|         notImplemented()
 | |
|     }
 | |
| 
 | |
|     let kSpacingBetweenItems: CGFloat = 20
 | |
| 
 | |
|     @objc
 | |
|     required public init(mode: AttachmentApprovalViewControllerMode,
 | |
|                          attachments: [SignalAttachment]) {
 | |
|         assert(attachments.count > 0)
 | |
|         self.mode = mode
 | |
|         let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )}
 | |
|         self.isAddMoreVisible = mode == .sharedNavigation
 | |
| 
 | |
|         self.attachmentItemCollection = AttachmentItemCollection(attachmentItems: attachmentItems, isAddMoreVisible: isAddMoreVisible)
 | |
| 
 | |
|         let options: [UIPageViewController.OptionsKey: Any] = [.interPageSpacing: kSpacingBetweenItems]
 | |
|         super.init(transitionStyle: .scroll,
 | |
|                    navigationOrientation: .horizontal,
 | |
|                    options: options)
 | |
|         self.dataSource = self
 | |
|         self.delegate = self
 | |
| 
 | |
|         NotificationCenter.default.addObserver(self,
 | |
|                                                selector: #selector(didBecomeActive),
 | |
|                                                name: NSNotification.Name.OWSApplicationDidBecomeActive,
 | |
|                                                object: nil)
 | |
|     }
 | |
| 
 | |
|     deinit {
 | |
|         NotificationCenter.default.removeObserver(self)
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     public class func wrappedInNavController(attachments: [SignalAttachment], approvalDelegate: AttachmentApprovalViewControllerDelegate) -> OWSNavigationController {
 | |
|         let vc = AttachmentApprovalViewController(mode: .modal, attachments: attachments)
 | |
|         vc.approvalDelegate = approvalDelegate
 | |
|         let navController = OWSNavigationController(rootViewController: vc)
 | |
|         navController.ows_prefersStatusBarHidden = true
 | |
|         return navController
 | |
|     }
 | |
| 
 | |
|     // MARK: - Notifications
 | |
| 
 | |
|     @objc func didBecomeActive() {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         updateContents()
 | |
|     }
 | |
| 
 | |
|     // MARK: - Subviews
 | |
| 
 | |
|     var galleryRailView: GalleryRailView {
 | |
|         return bottomToolView.galleryRailView
 | |
|     }
 | |
| 
 | |
|     var attachmentTextToolbar: AttachmentTextToolbar {
 | |
|         return bottomToolView.attachmentTextToolbar
 | |
|     }
 | |
| 
 | |
|     lazy var bottomToolView: AttachmentApprovalInputAccessoryView = {
 | |
|         let bottomToolView = AttachmentApprovalInputAccessoryView()
 | |
|         bottomToolView.delegate = self
 | |
| 
 | |
|         return bottomToolView
 | |
|     }()
 | |
| 
 | |
|     lazy var touchInterceptorView = UIView()
 | |
| 
 | |
|     // MARK: - View Lifecycle
 | |
| 
 | |
|     public override var prefersStatusBarHidden: Bool {
 | |
|         return true
 | |
|     }
 | |
| 
 | |
|     override public func viewDidLoad() {
 | |
|         super.viewDidLoad()
 | |
| 
 | |
|         self.view.backgroundColor = Colors.navigationBarBackground
 | |
| 
 | |
|         // avoid an unpleasant "bounce" which doesn't make sense in the context of a single item.
 | |
|         pagerScrollView?.isScrollEnabled = attachmentItems.count > 1
 | |
| 
 | |
|         // Bottom Toolbar
 | |
|         galleryRailView.delegate = self
 | |
|         attachmentTextToolbar.attachmentTextToolbarDelegate = self
 | |
| 
 | |
|         // Navigation
 | |
| 
 | |
|         self.navigationItem.title = nil
 | |
| 
 | |
|         guard let firstItem = attachmentItems.first else {
 | |
|             owsFailDebug("firstItem was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         self.setCurrentItem(firstItem, direction: .forward, animated: false)
 | |
| 
 | |
|         // layout immediately to avoid animating the layout process during the transition
 | |
|         self.currentPageViewController.view.layoutIfNeeded()
 | |
| 
 | |
|         view.addSubview(touchInterceptorView)
 | |
|         touchInterceptorView.autoPinEdgesToSuperviewEdges()
 | |
|         touchInterceptorView.isHidden = true
 | |
|         let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapTouchInterceptorView(gesture:)))
 | |
|         touchInterceptorView.addGestureRecognizer(tapGesture)
 | |
|     }
 | |
| 
 | |
|     override public func viewWillAppear(_ animated: Bool) {
 | |
|         Logger.debug("")
 | |
|         super.viewWillAppear(animated)
 | |
| 
 | |
|         guard let navigationBar = navigationController?.navigationBar as? OWSNavigationBar else {
 | |
|             owsFailDebug("navigationBar was nil or unexpected class")
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         // Loki: Set navigation bar background color
 | |
|         navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
 | |
|         navigationBar.shadowImage = UIImage()
 | |
|         navigationBar.isTranslucent = false
 | |
|         navigationBar.barTintColor = Colors.navigationBarBackground
 | |
|         navigationBar.respectsTheme = true
 | |
|         navigationBar.backgroundColor = Colors.navigationBarBackground
 | |
|         let backgroundImage = UIImage(color: Colors.navigationBarBackground)
 | |
|         navigationBar.setBackgroundImage(backgroundImage, for: .default)
 | |
| 
 | |
|         updateContents()
 | |
|     }
 | |
| 
 | |
|     override public func viewDidAppear(_ animated: Bool) {
 | |
|         Logger.debug("")
 | |
| 
 | |
|         super.viewDidAppear(animated)
 | |
| 
 | |
|         updateContents()
 | |
|     }
 | |
| 
 | |
|     override public func viewWillDisappear(_ animated: Bool) {
 | |
|         Logger.debug("")
 | |
|         super.viewWillDisappear(animated)
 | |
|     }
 | |
| 
 | |
|     private func updateContents() {
 | |
|         updateNavigationBar()
 | |
|         updateInputAccessory()
 | |
| 
 | |
|         touchInterceptorView.isHidden = !isEditingCaptions
 | |
|     }
 | |
| 
 | |
|     // MARK: - Input Accessory
 | |
| 
 | |
|     override public var inputAccessoryView: UIView? {
 | |
|         bottomToolView.layoutIfNeeded()
 | |
|         return bottomToolView
 | |
|     }
 | |
| 
 | |
|     override public var canBecomeFirstResponder: Bool {
 | |
|         return !shouldHideControls
 | |
|     }
 | |
| 
 | |
|     public func updateInputAccessory() {
 | |
|         var currentPageViewController: AttachmentPrepViewController?
 | |
|         if pageViewControllers.count == 1 {
 | |
|             currentPageViewController = pageViewControllers.first
 | |
|         }
 | |
|         let currentAttachmentItem: SignalAttachmentItem? = currentPageViewController?.attachmentItem
 | |
| 
 | |
|         let hasPresentedView = self.presentedViewController != nil
 | |
|         let isToolbarFirstResponder = bottomToolView.hasFirstResponder
 | |
|         if !shouldHideControls, !isFirstResponder, !hasPresentedView, !isToolbarFirstResponder {
 | |
|             becomeFirstResponder()
 | |
|         }
 | |
| 
 | |
|         bottomToolView.update(isEditingCaptions: isEditingCaptions,
 | |
|                               currentAttachmentItem: currentAttachmentItem,
 | |
|                               shouldHideControls: shouldHideControls)
 | |
|     }
 | |
| 
 | |
|     public var messageText: String? {
 | |
|         get {
 | |
|             return attachmentTextToolbar.messageText
 | |
|         }
 | |
|         set {
 | |
|             attachmentTextToolbar.messageText = newValue
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Navigation Bar
 | |
| 
 | |
|     public func updateNavigationBar() {
 | |
|         guard !shouldHideControls else {
 | |
|             self.navigationItem.leftBarButtonItem = nil
 | |
|             self.navigationItem.rightBarButtonItem = nil
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard !isEditingCaptions else {
 | |
|             // Hide all navigation bar items while the caption view is open.
 | |
|             self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("ATTACHMENT_APPROVAL_CAPTION_TITLE", comment: "Title for 'caption' mode of the attachment approval view."), style: .plain, target: nil, action: nil)
 | |
| 
 | |
|             let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full",
 | |
|                                                  selector: #selector(didTapCaptionDone(sender:)))
 | |
|             let navigationBarItems = [doneButton]
 | |
|             updateNavigationBar(navigationBarItems: navigationBarItems)
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         var navigationBarItems = [UIView]()
 | |
| 
 | |
|         if let viewControllers = viewControllers,
 | |
|             viewControllers.count == 1,
 | |
|             let firstViewController = viewControllers.first as? AttachmentPrepViewController {
 | |
|             navigationBarItems = firstViewController.navigationBarItems()
 | |
| 
 | |
|             // Show the caption UI if there's more than one attachment
 | |
|             // OR if the attachment already has a caption.
 | |
|             let attachmentCount = attachmentItemCollection.count
 | |
|             var shouldShowCaptionUI = attachmentCount > 0
 | |
|             if let captionText = firstViewController.attachmentItem.captionText, captionText.count > 0 {
 | |
|                 shouldShowCaptionUI = true
 | |
|             }
 | |
|             if shouldShowCaptionUI {
 | |
|                 let captionButton = navigationBarButton(imageName: "image_editor_caption",
 | |
|                                                         selector: #selector(didTapCaption(sender:)))
 | |
|                 navigationBarItems.append(captionButton)
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         updateNavigationBar(navigationBarItems: navigationBarItems)
 | |
| 
 | |
|         let hasCancel = (mode != .sharedNavigation)
 | |
|         if hasCancel {
 | |
|             // Mimic a UIBarButtonItem of type .cancel, but with a shadow.
 | |
|             let cancelButton = OWSButton(title: CommonStrings.cancelButton) { [weak self] in
 | |
|                 self?.cancelPressed()
 | |
|             }
 | |
|             cancelButton.setTitleColor(Colors.text, for: .normal)
 | |
|             if let titleLabel = cancelButton.titleLabel {
 | |
|                 titleLabel.font = UIFont.systemFont(ofSize: 17.0)
 | |
|             } else {
 | |
|                 owsFailDebug("Missing titleLabel.")
 | |
|             }
 | |
|             cancelButton.sizeToFit()
 | |
|             navigationItem.leftBarButtonItem = UIBarButtonItem(customView: cancelButton)
 | |
|         } else {
 | |
|             // Mimic a conventional back button, but with a shadow.
 | |
|             let isRTL = CurrentAppContext().isRTL
 | |
|             let imageName = isRTL ? "NavBarBackRTL" : "NavBarBack"
 | |
|             let backButton = OWSButton(imageName: imageName, tintColor: Colors.text) { [weak self] in
 | |
|                 self?.navigationController?.popViewController(animated: true)
 | |
|             }
 | |
| 
 | |
|             // Nudge closer to the left edge to match default back button item.
 | |
|             let kExtraLeftPadding: CGFloat = isRTL ? +0 : -8
 | |
| 
 | |
|             // Give some extra hit area to the back button. This is a little smaller
 | |
|             // than the default back button, but makes sense for our left aligned title
 | |
|             // view in the MessagesViewController
 | |
|             let kExtraRightPadding: CGFloat = isRTL ? -0 : +10
 | |
| 
 | |
|             // Extra hit area above/below
 | |
|             let kExtraHeightPadding: CGFloat = 4
 | |
| 
 | |
|             // Matching the default backbutton placement is tricky.
 | |
|             // We can't just adjust the imageEdgeInsets on a UIBarButtonItem directly,
 | |
|             // so we adjust the imageEdgeInsets on a UIButton, then wrap that
 | |
|             // in a UIBarButtonItem.
 | |
| 
 | |
|             backButton.contentHorizontalAlignment = .left
 | |
| 
 | |
|             // Default back button is 1.5 pixel lower than our extracted image.
 | |
|             let kTopInsetPadding: CGFloat = 1.5
 | |
|             backButton.imageEdgeInsets = UIEdgeInsets(top: kTopInsetPadding, left: kExtraLeftPadding, bottom: 0, right: 0)
 | |
| 
 | |
|             var backImageSize = CGSize.zero
 | |
|             if let backImage = UIImage(named: imageName) {
 | |
|                 backImageSize = backImage.size
 | |
|             } else {
 | |
|                 owsFailDebug("Missing backImage.")
 | |
|             }
 | |
|             backButton.frame = CGRect(origin: .zero, size: CGSize(width: backImageSize.width + kExtraRightPadding,
 | |
|                                                                   height: backImageSize.height + kExtraHeightPadding))
 | |
| 
 | |
|             // Note: using a custom leftBarButtonItem breaks the interactive pop gesture.
 | |
|             navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Control Visibility
 | |
| 
 | |
|     public var shouldHideControls: Bool {
 | |
|         guard let pageViewController = pageViewControllers.first else {
 | |
|             return false
 | |
|         }
 | |
|         return pageViewController.shouldHideControls
 | |
|     }
 | |
| 
 | |
|     // MARK: - View Helpers
 | |
| 
 | |
|     func remove(attachmentItem: SignalAttachmentItem) {
 | |
|         if attachmentItem == currentItem {
 | |
|             if let nextItem = attachmentItemCollection.itemAfter(item: attachmentItem) {
 | |
|                 setCurrentItem(nextItem, direction: .forward, animated: true)
 | |
|             } else if let prevItem = attachmentItemCollection.itemBefore(item: attachmentItem) {
 | |
|                 setCurrentItem(prevItem, direction: .reverse, animated: true)
 | |
|             } else {
 | |
|                 owsFailDebug("removing last item shouldn't be possible because rail should not be visible")
 | |
|                 return
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         guard let cell = galleryRailView.cellViews.first(where: { $0.item === attachmentItem }) else {
 | |
|             owsFailDebug("cell was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         UIView.animate(withDuration: 0.2,
 | |
|                        animations: {
 | |
|                         // shrink stack view item until it disappears
 | |
|                         cell.isHidden = true
 | |
| 
 | |
|                         // simultaneously fade out
 | |
|                         cell.alpha = 0
 | |
|         },
 | |
|                        completion: { _ in
 | |
|                         self.attachmentItemCollection.remove(item: attachmentItem)
 | |
|                         self.approvalDelegate?.attachmentApproval?(self, didRemoveAttachment: attachmentItem.attachment)
 | |
|                         self.updateMediaRail()
 | |
|         })
 | |
|     }
 | |
| 
 | |
|     lazy var pagerScrollView: UIScrollView? = {
 | |
|         // This is kind of a hack. Since we don't have first class access to the superview's `scrollView`
 | |
|         // we traverse the view hierarchy until we find it.
 | |
|         let pagerScrollView = view.subviews.first { $0 is UIScrollView } as? UIScrollView
 | |
|         assert(pagerScrollView != nil)
 | |
| 
 | |
|         return pagerScrollView
 | |
|     }()
 | |
| 
 | |
|     // MARK: - UIPageViewControllerDelegate
 | |
| 
 | |
|     public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
 | |
|         Logger.debug("")
 | |
| 
 | |
|         assert(pendingViewControllers.count == 1)
 | |
|         pendingViewControllers.forEach { viewController in
 | |
|             guard let pendingPage = viewController as? AttachmentPrepViewController else {
 | |
|                 owsFailDebug("unexpected viewController: \(viewController)")
 | |
|                 return
 | |
|             }
 | |
| 
 | |
|             // use compact scale when keyboard is popped.
 | |
|             let scale: AttachmentPrepViewController.AttachmentViewScale = self.isFirstResponder ? .fullsize : .compact
 | |
|             pendingPage.setAttachmentViewScale(scale, animated: false)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted: Bool) {
 | |
|         Logger.debug("")
 | |
| 
 | |
|         assert(previousViewControllers.count == 1)
 | |
|         previousViewControllers.forEach { viewController in
 | |
|             guard let previousPage = viewController as? AttachmentPrepViewController else {
 | |
|                 owsFailDebug("unexpected viewController: \(viewController)")
 | |
|                 return
 | |
|             }
 | |
| 
 | |
|             if transitionCompleted {
 | |
|                 previousPage.zoomOut(animated: false)
 | |
|                 updateMediaRail()
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - UIPageViewControllerDataSource
 | |
| 
 | |
|     public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
 | |
|         guard let currentViewController = viewController as? AttachmentPrepViewController else {
 | |
|             owsFailDebug("unexpected viewController: \(viewController)")
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         let currentItem = currentViewController.attachmentItem
 | |
|         guard let previousItem = attachmentItem(before: currentItem) else {
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         guard let previousPage: AttachmentPrepViewController = buildPage(item: previousItem) else {
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         return previousPage
 | |
|     }
 | |
| 
 | |
|     public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
 | |
|         Logger.debug("")
 | |
| 
 | |
|         guard let currentViewController = viewController as? AttachmentPrepViewController else {
 | |
|             owsFailDebug("unexpected viewController: \(viewController)")
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         let currentItem = currentViewController.attachmentItem
 | |
|         guard let nextItem = attachmentItem(after: currentItem) else {
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         guard let nextPage: AttachmentPrepViewController = buildPage(item: nextItem) else {
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         return nextPage
 | |
|     }
 | |
| 
 | |
|     public var currentPageViewController: AttachmentPrepViewController {
 | |
|         return pageViewControllers.first!
 | |
|     }
 | |
| 
 | |
|     public var pageViewControllers: [AttachmentPrepViewController] {
 | |
|         return super.viewControllers!.map { $0 as! AttachmentPrepViewController }
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     public override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewController.NavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) {
 | |
|         super.setViewControllers(viewControllers,
 | |
|                                  direction: direction,
 | |
|                                  animated: animated) { [weak self] (finished) in
 | |
|                                     if let completion = completion {
 | |
|                                         completion(finished)
 | |
|                                     }
 | |
|                                     self?.updateContents()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     var currentItem: SignalAttachmentItem! {
 | |
|         get {
 | |
|             return currentPageViewController.attachmentItem
 | |
|         }
 | |
|         set {
 | |
|             setCurrentItem(newValue, direction: .forward, animated: false)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private var cachedPages: [SignalAttachmentItem: AttachmentPrepViewController] = [:]
 | |
|     private func buildPage(item: SignalAttachmentItem) -> AttachmentPrepViewController? {
 | |
| 
 | |
|         if let cachedPage = cachedPages[item] {
 | |
|             Logger.debug("cache hit.")
 | |
|             return cachedPage
 | |
|         }
 | |
| 
 | |
|         Logger.debug("cache miss.")
 | |
|         let viewController = AttachmentPrepViewController(attachmentItem: item)
 | |
|         viewController.prepDelegate = self
 | |
|         cachedPages[item] = viewController
 | |
| 
 | |
|         return viewController
 | |
|     }
 | |
| 
 | |
|     private func setCurrentItem(_ item: SignalAttachmentItem, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) {
 | |
|         guard let page = self.buildPage(item: item) else {
 | |
|             owsFailDebug("unexpectedly unable to build new page")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         page.loadViewIfNeeded()
 | |
| 
 | |
|         self.setViewControllers([page], direction: direction, animated: isAnimated, completion: nil)
 | |
|         updateMediaRail()
 | |
|     }
 | |
| 
 | |
|     func updateMediaRail() {
 | |
|         guard let currentItem = self.currentItem else {
 | |
|             owsFailDebug("currentItem was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         let cellViewBuilder: (GalleryRailItem) -> GalleryRailCellView = { [weak self] railItem in
 | |
|             switch railItem {
 | |
|             case is AddMoreRailItem:
 | |
|                 return GalleryRailCellView()
 | |
|             case is SignalAttachmentItem:
 | |
|                 let cell = ApprovalRailCellView()
 | |
|                 cell.approvalRailCellDelegate = self
 | |
|                 return cell
 | |
|             default:
 | |
|                 owsFailDebug("unexpted rail item type: \(railItem)")
 | |
|                 return GalleryRailCellView()
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         galleryRailView.configureCellViews(itemProvider: attachmentItemCollection,
 | |
|                                            focusedItem: currentItem,
 | |
|                                            cellViewBuilder: cellViewBuilder)
 | |
| 
 | |
|         if isAddMoreVisible {
 | |
|             galleryRailView.isHidden = false
 | |
|         } else if attachmentItemCollection.attachmentItems.count > 1 {
 | |
|             galleryRailView.isHidden = false
 | |
|         } else {
 | |
|             galleryRailView.isHidden = true
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     let attachmentItemCollection: AttachmentItemCollection
 | |
| 
 | |
|     var attachmentItems: [SignalAttachmentItem] {
 | |
|         return attachmentItemCollection.attachmentItems
 | |
|     }
 | |
| 
 | |
|     var attachments: [SignalAttachment] {
 | |
|         return attachmentItems.map { (attachmentItem) in
 | |
|             autoreleasepool {
 | |
|                 return self.processedAttachment(forAttachmentItem: attachmentItem)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // For any attachments edited with the image editor, returns a
 | |
|     // new SignalAttachment that reflects those changes.  Otherwise,
 | |
|     // returns the original attachment.
 | |
|     //
 | |
|     // If any errors occurs in the export process, we fail over to
 | |
|     // sending the original attachment.  This seems better than trying
 | |
|     // to involve the user in resolving the issue.
 | |
|     func processedAttachment(forAttachmentItem attachmentItem: SignalAttachmentItem) -> SignalAttachment {
 | |
|         guard let imageEditorModel = attachmentItem.imageEditorModel else {
 | |
|             // Image was not edited.
 | |
|             return attachmentItem.attachment
 | |
|         }
 | |
|         guard imageEditorModel.isDirty() else {
 | |
|             // Image editor has no changes.
 | |
|             return attachmentItem.attachment
 | |
|         }
 | |
|         guard let dstImage = ImageEditorCanvasView.renderForOutput(model: imageEditorModel, transform: imageEditorModel.currentTransform()) else {
 | |
|             owsFailDebug("Could not render for output.")
 | |
|             return attachmentItem.attachment
 | |
|         }
 | |
|         var dataUTI = kUTTypeImage as String
 | |
|         guard let dstData: Data = {
 | |
|             let isLossy: Bool = attachmentItem.attachment.mimeType.caseInsensitiveCompare(OWSMimeTypeImageJpeg) == .orderedSame
 | |
|             if isLossy {
 | |
|                 dataUTI = kUTTypeJPEG as String
 | |
|                 return dstImage.jpegData(compressionQuality: 0.9)
 | |
|             } else {
 | |
|                 dataUTI = kUTTypePNG as String
 | |
|                 return dstImage.pngData()
 | |
|             }
 | |
|             }() else {
 | |
|                 owsFailDebug("Could not export for output.")
 | |
|                 return attachmentItem.attachment
 | |
|         }
 | |
|         guard let dataSource = DataSourceValue.dataSource(with: dstData, utiType: dataUTI) else {
 | |
|             owsFailDebug("Could not prepare data source for output.")
 | |
|             return attachmentItem.attachment
 | |
|         }
 | |
| 
 | |
|         // Rewrite the filename's extension to reflect the output file format.
 | |
|         var filename: String? = attachmentItem.attachment.sourceFilename
 | |
|         if let sourceFilename = attachmentItem.attachment.sourceFilename {
 | |
|             if let fileExtension: String = MIMETypeUtil.fileExtension(forUTIType: dataUTI) {
 | |
|                 filename = (sourceFilename as NSString).deletingPathExtension.appendingFileExtension(fileExtension)
 | |
|             }
 | |
|         }
 | |
|         dataSource.sourceFilename = filename
 | |
| 
 | |
|         let dstAttachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium)
 | |
|         if let attachmentError = dstAttachment.error {
 | |
|             owsFailDebug("Could not prepare attachment for output: \(attachmentError).")
 | |
|             return attachmentItem.attachment
 | |
|         }
 | |
|         // Preserve caption text.
 | |
|         dstAttachment.captionText = attachmentItem.captionText
 | |
|         return dstAttachment
 | |
|     }
 | |
| 
 | |
|     func attachmentItem(before currentItem: SignalAttachmentItem) -> SignalAttachmentItem? {
 | |
|         guard let currentIndex = attachmentItems.firstIndex(of: currentItem) else {
 | |
|             owsFailDebug("currentIndex was unexpectedly nil")
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         let index: Int = attachmentItems.index(before: currentIndex)
 | |
|         guard let previousItem = attachmentItems[safe: index] else {
 | |
|             // already at first item
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         return previousItem
 | |
|     }
 | |
| 
 | |
|     func attachmentItem(after currentItem: SignalAttachmentItem) -> SignalAttachmentItem? {
 | |
|         guard let currentIndex = attachmentItems.firstIndex(of: currentItem) else {
 | |
|             owsFailDebug("currentIndex was unexpectedly nil")
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         let index: Int = attachmentItems.index(after: currentIndex)
 | |
|         guard let nextItem = attachmentItems[safe: index] else {
 | |
|             // already at last item
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         return nextItem
 | |
|     }
 | |
| 
 | |
|     // MARK: - Event Handlers
 | |
| 
 | |
|     @objc
 | |
|     func didTapTouchInterceptorView(gesture: UITapGestureRecognizer) {
 | |
|         Logger.info("")
 | |
| 
 | |
|         isEditingCaptions = false
 | |
|     }
 | |
| 
 | |
|     private func cancelPressed() {
 | |
|         self.approvalDelegate?.attachmentApprovalDidCancel(self)
 | |
|     }
 | |
| 
 | |
|     @objc func didTapCaption(sender: UIButton) {
 | |
|         Logger.verbose("")
 | |
| 
 | |
|         isEditingCaptions = true
 | |
|     }
 | |
| 
 | |
|     @objc func didTapCaptionDone(sender: UIButton) {
 | |
|         Logger.verbose("")
 | |
| 
 | |
|         isEditingCaptions = false
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
 | |
|     func attachmentTextToolbarDidBeginEditing(_ attachmentTextToolbar: AttachmentTextToolbar) {
 | |
|         currentPageViewController.setAttachmentViewScale(.compact, animated: true)
 | |
|     }
 | |
| 
 | |
|     func attachmentTextToolbarDidEndEditing(_ attachmentTextToolbar: AttachmentTextToolbar) {
 | |
|         currentPageViewController.setAttachmentViewScale(.fullsize, animated: true)
 | |
|     }
 | |
| 
 | |
|     func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) {
 | |
|         // Toolbar flickers in and out if there are errors
 | |
|         // and remains visible momentarily after share extension is dismissed.
 | |
|         // It's easiest to just hide it at this point since we're done with it.
 | |
|         currentPageViewController.shouldAllowAttachmentViewResizing = false
 | |
|         attachmentTextToolbar.isUserInteractionEnabled = false
 | |
|         attachmentTextToolbar.isHidden = true
 | |
| 
 | |
|         approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, messageText: attachmentTextToolbar.messageText)
 | |
|     }
 | |
| 
 | |
|     func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) {
 | |
|         approvalDelegate?.attachmentApproval(self, didChangeMessageText: attachmentTextToolbar.messageText)
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate {
 | |
|     func prepViewControllerUpdateNavigationBar() {
 | |
|         updateNavigationBar()
 | |
|     }
 | |
| 
 | |
|     func prepViewControllerUpdateControls() {
 | |
|         updateInputAccessory()
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: GalleryRail
 | |
| 
 | |
| extension SignalAttachmentItem: GalleryRailItem {
 | |
|     func buildRailItemView() -> UIView {
 | |
|         let imageView = UIImageView()
 | |
|         imageView.contentMode = .scaleAspectFill
 | |
| 
 | |
|         getThumbnailImage().map { image in
 | |
|             imageView.image = image
 | |
|         }.retainUntilComplete()
 | |
| 
 | |
|         return imageView
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension AttachmentItemCollection: GalleryRailItemProvider {
 | |
|     var railItems: [GalleryRailItem] {
 | |
|         if isAddMoreVisible {
 | |
|             return self.attachmentItems + [AddMoreRailItem()]
 | |
|         } else {
 | |
|             return self.attachmentItems
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension AttachmentApprovalViewController: GalleryRailViewDelegate {
 | |
|     public func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem) {
 | |
|         if imageRailItem is AddMoreRailItem {
 | |
|             self.approvalDelegate?.attachmentApprovalDidTapAddMore?(self)
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard let targetItem = imageRailItem as? SignalAttachmentItem else {
 | |
|             owsFailDebug("unexpected imageRailItem: \(imageRailItem)")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard let currentIndex = attachmentItems.firstIndex(of: currentItem) else {
 | |
|             owsFailDebug("currentIndex was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard let targetIndex = attachmentItems.firstIndex(of: targetItem) else {
 | |
|             owsFailDebug("targetIndex was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         let direction: UIPageViewController.NavigationDirection = currentIndex < targetIndex ? .forward : .reverse
 | |
| 
 | |
|         self.setCurrentItem(targetItem, direction: direction, animated: true)
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| enum KeyboardScenario {
 | |
|     case hidden, editingMessage, editingCaption
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension AttachmentApprovalViewController: ApprovalRailCellViewDelegate {
 | |
|     func approvalRailCellView(_ approvalRailCellView: ApprovalRailCellView, didRemoveItem attachmentItem: SignalAttachmentItem) {
 | |
|         remove(attachmentItem: attachmentItem)
 | |
|     }
 | |
| 
 | |
|     func canRemoveApprovalRailCellView(_ approvalRailCellView: ApprovalRailCellView) -> Bool {
 | |
|         return self.attachmentItems.count > 1
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| extension AttachmentApprovalViewController: AttachmentApprovalInputAccessoryViewDelegate {
 | |
|     public func attachmentApprovalInputUpdateMediaRail() {
 | |
|         updateMediaRail()
 | |
|     }
 | |
| 
 | |
|     public func attachmentApprovalInputStartEditingCaptions() {
 | |
|         isEditingCaptions = true
 | |
|     }
 | |
| 
 | |
|     public func attachmentApprovalInputStopEditingCaptions() {
 | |
|         isEditingCaptions = false
 | |
|     }
 | |
| }
 |