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
 | 
						|
    }
 | 
						|
}
 |