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.
		
		
		
		
		
			
		
			
				
	
	
		
			773 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			773 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import Foundation
 | |
| import AVFoundation
 | |
| import MediaPlayer
 | |
| import CoreServices
 | |
| import SessionUIKit
 | |
| import SessionMessagingKit
 | |
| import SignalCoreKit
 | |
| import SessionUtilitiesKit
 | |
| 
 | |
| public protocol AttachmentApprovalViewControllerDelegate: AnyObject {
 | |
|     func attachmentApproval(
 | |
|         _ attachmentApproval: AttachmentApprovalViewController,
 | |
|         didApproveAttachments attachments: [SignalAttachment],
 | |
|         forThreadId threadId: String,
 | |
|         messageText: String?,
 | |
|         using dependencies: Dependencies
 | |
|     )
 | |
| 
 | |
|     func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController)
 | |
| 
 | |
|     func attachmentApproval(
 | |
|         _ attachmentApproval: AttachmentApprovalViewController,
 | |
|         didChangeMessageText newMessageText: String?
 | |
|     )
 | |
| 
 | |
|     func attachmentApproval(
 | |
|         _ attachmentApproval: AttachmentApprovalViewController,
 | |
|         didRemoveAttachment attachment: SignalAttachment
 | |
|     )
 | |
| 
 | |
|     func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController)
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| @objc
 | |
| public enum AttachmentApprovalViewControllerMode: UInt {
 | |
|     case modal
 | |
|     case sharedNavigation
 | |
| }
 | |
| 
 | |
| // MARK: -
 | |
| 
 | |
| public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
 | |
|     public override var preferredStatusBarStyle: UIStatusBarStyle {
 | |
|         return ThemeManager.currentTheme.statusBarStyle
 | |
|     }
 | |
|     
 | |
|     public enum Mode: UInt {
 | |
|         case modal
 | |
|         case sharedNavigation
 | |
|     }
 | |
| 
 | |
|     // MARK: - Properties
 | |
| 
 | |
|     private let mode: Mode
 | |
|     private let threadId: String
 | |
|     private let isAddMoreVisible: Bool
 | |
| 
 | |
|     public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
 | |
| 
 | |
|     public var isEditingCaptions = false {
 | |
|         didSet { updateContents() }
 | |
|     }
 | |
|     
 | |
|     let attachmentItemCollection: AttachmentItemCollection
 | |
| 
 | |
|     var attachmentItems: [SignalAttachmentItem] {
 | |
|         return attachmentItemCollection.attachmentItems
 | |
|     }
 | |
| 
 | |
|     var attachments: [SignalAttachment] {
 | |
|         return attachmentItems.map { (attachmentItem) in
 | |
|             autoreleasepool {
 | |
|                 return self.processedAttachment(forAttachmentItem: attachmentItem)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public var pageViewControllers: [AttachmentPrepViewController]? {
 | |
|         return viewControllers?.compactMap { $0 as? AttachmentPrepViewController }
 | |
|     }
 | |
|     
 | |
|     public var currentPageViewController: AttachmentPrepViewController? {
 | |
|         return pageViewControllers?.first
 | |
|     }
 | |
|     
 | |
|     var currentItem: SignalAttachmentItem? {
 | |
|         get { return currentPageViewController?.attachmentItem }
 | |
|         set { setCurrentItem(newValue, direction: .forward, animated: false) }
 | |
|     }
 | |
|     
 | |
|     private var cachedPages: [SignalAttachmentItem: AttachmentPrepViewController] = [:]
 | |
| 
 | |
|     public var shouldHideControls: Bool {
 | |
|         guard let pageViewController: AttachmentPrepViewController = pageViewControllers?.first else {
 | |
|             return false
 | |
|         }
 | |
|         
 | |
|         return pageViewController.shouldHideControls
 | |
|     }
 | |
|     
 | |
|     override public var inputAccessoryView: UIView? {
 | |
|         bottomToolView.layoutIfNeeded()
 | |
|         return bottomToolView
 | |
|     }
 | |
| 
 | |
|     override public var canBecomeFirstResponder: Bool {
 | |
|         return !shouldHideControls
 | |
|     }
 | |
|     
 | |
|     public var messageText: String? {
 | |
|         get { return bottomToolView.attachmentTextToolbar.messageText }
 | |
|         set { bottomToolView.attachmentTextToolbar.messageText = newValue }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Initializers
 | |
| 
 | |
|     @available(*, unavailable, message:"use attachment: constructor instead.")
 | |
|     required public init?(coder aDecoder: NSCoder) {
 | |
|         notImplemented()
 | |
|     }
 | |
| 
 | |
|     required public init(
 | |
|         mode: Mode,
 | |
|         threadId: String,
 | |
|         attachments: [SignalAttachment]
 | |
|     ) {
 | |
|         assert(attachments.count > 0)
 | |
|         self.mode = mode
 | |
|         self.threadId = threadId
 | |
|         let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )}
 | |
|         self.isAddMoreVisible = (mode == .sharedNavigation)
 | |
| 
 | |
|         self.attachmentItemCollection = AttachmentItemCollection(attachmentItems: attachmentItems, isAddMoreVisible: isAddMoreVisible)
 | |
| 
 | |
|         super.init(
 | |
|             transitionStyle: .scroll,
 | |
|             navigationOrientation: .horizontal,
 | |
|             options: [
 | |
|                 .interPageSpacing: kSpacingBetweenItems
 | |
|             ]
 | |
|         )
 | |
|         self.dataSource = self
 | |
|         self.delegate = self
 | |
| 
 | |
|         NotificationCenter.default.addObserver(
 | |
|             self,
 | |
|             selector: #selector(didBecomeActive),
 | |
|             name: .OWSApplicationDidBecomeActive,
 | |
|             object: nil
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     deinit {
 | |
|         NotificationCenter.default.removeObserver(self)
 | |
|     }
 | |
| 
 | |
|     public class func wrappedInNavController(
 | |
|         threadId: String,
 | |
|         attachments: [SignalAttachment],
 | |
|         approvalDelegate: AttachmentApprovalViewControllerDelegate
 | |
|     ) -> UINavigationController {
 | |
|         let vc = AttachmentApprovalViewController(mode: .modal, threadId: threadId, attachments: attachments)
 | |
|         vc.approvalDelegate = approvalDelegate
 | |
|         
 | |
|         let navController = StyledNavigationController(rootViewController: vc)
 | |
|         
 | |
|         return navController
 | |
|     }
 | |
| 
 | |
|     // MARK: - UI
 | |
|     
 | |
|     private let kSpacingBetweenItems: CGFloat = 20
 | |
|     
 | |
|     private lazy var bottomToolView: AttachmentApprovalInputAccessoryView = {
 | |
|         let bottomToolView = AttachmentApprovalInputAccessoryView()
 | |
|         bottomToolView.delegate = self
 | |
|         bottomToolView.attachmentTextToolbar.attachmentTextToolbarDelegate = self
 | |
|         bottomToolView.galleryRailView.delegate = self
 | |
| 
 | |
|         return bottomToolView
 | |
|     }()
 | |
| 
 | |
|     private var galleryRailView: GalleryRailView { return bottomToolView.galleryRailView }
 | |
| 
 | |
|     private lazy var touchInterceptorView: UIView = {
 | |
|         let view: UIView = UIView()
 | |
|         view.translatesAutoresizingMaskIntoConstraints = false
 | |
|         view.isHidden = true
 | |
|         
 | |
|         let tapGesture = UITapGestureRecognizer(
 | |
|             target: self,
 | |
|             action: #selector(didTapTouchInterceptorView(gesture:))
 | |
|         )
 | |
|         view.addGestureRecognizer(tapGesture)
 | |
|         
 | |
|         return view
 | |
|     }()
 | |
| 
 | |
|     private 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: - Lifecycle
 | |
| 
 | |
|     override public func viewDidLoad() {
 | |
|         super.viewDidLoad()
 | |
| 
 | |
|         self.view.themeBackgroundColor = .newConversation_background
 | |
|         
 | |
|         // Avoid an unpleasant "bounce" which doesn't make sense in the context of a single item.
 | |
|         pagerScrollView?.isScrollEnabled = (attachmentItems.count > 1)
 | |
| 
 | |
|         guard let firstItem = attachmentItems.first else {
 | |
|             owsFailDebug("firstItem was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         self.setCurrentItem(firstItem, direction: .forward, animated: false)
 | |
|         
 | |
|         view.addSubview(touchInterceptorView)
 | |
| 
 | |
|         // layout immediately to avoid animating the layout process during the transition
 | |
|         UIView.performWithoutAnimation {
 | |
|             self.currentPageViewController?.view.layoutIfNeeded()
 | |
|         }
 | |
|         
 | |
|         // If the first item is just text, or is a URL and LinkPreviews are disabled
 | |
|         // then just fill the 'message' box with it
 | |
|         if firstItem.attachment.isText || (firstItem.attachment.isUrl && LinkPreview.previewUrl(for: firstItem.attachment.text()) == nil) {
 | |
|             bottomToolView.attachmentTextToolbar.messageText = firstItem.attachment.text()
 | |
|         }
 | |
| 
 | |
|         setupLayout()
 | |
|     }
 | |
| 
 | |
|     override public func viewWillAppear(_ animated: Bool) {
 | |
|         Logger.debug("")
 | |
|         super.viewWillAppear(animated)
 | |
| 
 | |
|         updateContents()
 | |
|     }
 | |
| 
 | |
|     override public func viewDidAppear(_ animated: Bool) {
 | |
|         Logger.debug("")
 | |
| 
 | |
|         super.viewDidAppear(animated)
 | |
| 
 | |
|         updateContents()
 | |
|     }
 | |
|     
 | |
|     // MARK: - Layout
 | |
|     
 | |
|     private func setupLayout() {
 | |
|         touchInterceptorView.autoPinEdgesToSuperviewEdges()
 | |
|     }
 | |
|     
 | |
|     // MARK: - Notifications
 | |
| 
 | |
|     @objc func didBecomeActive() {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         updateContents()
 | |
|     }
 | |
|     
 | |
|     // MARK: - Contents
 | |
|     
 | |
|     private func updateContents() {
 | |
|         updateNavigationBar()
 | |
|         updateInputAccessory()
 | |
| 
 | |
|         touchInterceptorView.isHidden = !isEditingCaptions
 | |
|     }
 | |
| 
 | |
|     // MARK: - Input Accessory
 | |
| 
 | |
|     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
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     // 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 for 'caption' mode of the attachment approval view."
 | |
|                 title: "ATTACHMENT_APPROVAL_CAPTION_TITLE".localized(),
 | |
|                 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 viewControllers?.count == 1, let firstViewController: AttachmentPrepViewController = 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.
 | |
|             if attachmentItemCollection.count > 0, (firstViewController.attachmentItem.captionText?.count ?? 0) > 0 {
 | |
|                 let captionButton = navigationBarButton(
 | |
|                     imageName: "image_editor_caption",
 | |
|                     selector: #selector(didTapCaption(sender:))
 | |
|                 )
 | |
|                 navigationBarItems.append(captionButton)
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         updateNavigationBar(navigationBarItems: navigationBarItems)
 | |
| 
 | |
|         if mode != .sharedNavigation {
 | |
|             // Mimic a UIBarButtonItem of type .cancel, but with a shadow.
 | |
|             let cancelButton = OWSButton(title: CommonStrings.cancelButton) { [weak self] in
 | |
|                 self?.cancelPressed()
 | |
|             }
 | |
|             cancelButton.titleLabel?.font = .systemFont(ofSize: 17.0)
 | |
|             cancelButton.setThemeTitleColor(.textPrimary, for: .normal)
 | |
|             cancelButton.setThemeTitleColor(.textSecondary, for: .highlighted)
 | |
|             cancelButton.sizeToFit()
 | |
|             navigationItem.leftBarButtonItem = UIBarButtonItem(customView: cancelButton)
 | |
|         }
 | |
|         else {
 | |
|             navigationItem.leftBarButtonItem = nil
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - View Helpers
 | |
| 
 | |
|     func remove(attachmentItem: SignalAttachmentItem) {
 | |
|         if attachmentItem.isEqual(to: 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
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         self.attachmentItemCollection.remove(item: attachmentItem)
 | |
|         self.approvalDelegate?.attachmentApproval(self, didRemoveAttachment: attachmentItem.attachment)
 | |
|         self.updateMediaRail()
 | |
|     }
 | |
| 
 | |
|     // 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
 | |
|     }
 | |
| 
 | |
|     @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
 | |
|             completion?(finished)
 | |
|             self?.updateContents()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     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 item: SignalAttachmentItem = item, let page = self.buildPage(item: item) else {
 | |
|             Logger.error("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 {
 | |
|             Logger.error("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:
 | |
|                     Logger.error("unexpted rail item type: \(railItem)")
 | |
|                     return GalleryRailCellView()
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         galleryRailView.configureCellViews(
 | |
|             album: (attachmentItemCollection.attachmentItems as [GalleryRailItem])
 | |
|                 .appending(attachmentItemCollection.isAddMoreVisible ?
 | |
|                     AddMoreRailItem() :
 | |
|                     nil
 | |
|                 ),
 | |
|             focusedItem: currentItem,
 | |
|             cellViewBuilder: cellViewBuilder
 | |
|         )
 | |
| 
 | |
|         if isAddMoreVisible {
 | |
|             galleryRailView.isHidden = false
 | |
|         }
 | |
|         else if attachmentItemCollection.attachmentItems.count > 1 {
 | |
|             galleryRailView.isHidden = false
 | |
|         }
 | |
|         else {
 | |
|             galleryRailView.isHidden = true
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // 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
 | |
|         let maybeDstData: 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()
 | |
|             }
 | |
|         }()
 | |
|         
 | |
|         guard let dstData: Data = maybeDstData 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) {}
 | |
| 
 | |
|     func attachmentTextToolbarDidEndEditing(_ attachmentTextToolbar: AttachmentTextToolbar) {}
 | |
| 
 | |
|     func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar, using dependencies: Dependencies) {
 | |
|         // 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, forThreadId: threadId, messageText: attachmentTextToolbar.messageText, using: dependencies)
 | |
|     }
 | |
| 
 | |
|     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.image = getThumbnailImage()
 | |
|         imageView.themeBackgroundColor = .backgroundSecondary
 | |
|         imageView.contentMode = .scaleAspectFill
 | |
|         
 | |
|         return imageView
 | |
|     }
 | |
|     
 | |
|     func isEqual(to other: GalleryRailItem?) -> Bool {
 | |
|         guard let otherAttachmentItem: SignalAttachmentItem = other as? SignalAttachmentItem else { return false }
 | |
|         
 | |
|         return (self.attachment == otherAttachmentItem.attachment)
 | |
|     }
 | |
| }
 | |
| 
 | |
| // 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 currentItem: SignalAttachmentItem = currentItem, 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: -
 | |
| 
 | |
| 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
 | |
|     }
 | |
| }
 |