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.
		
		
		
		
		
			
		
			
				
	
	
		
			465 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			465 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import UIKit
 | 
						|
import AVFoundation
 | 
						|
import SessionUIKit
 | 
						|
 | 
						|
protocol AttachmentPrepViewControllerDelegate: class {
 | 
						|
    func prepViewControllerUpdateNavigationBar()
 | 
						|
 | 
						|
    func prepViewControllerUpdateControls()
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate {
 | 
						|
    // We sometimes shrink the attachment view so that it remains somewhat visible
 | 
						|
    // when the keyboard is presented.
 | 
						|
    public enum AttachmentViewScale {
 | 
						|
        case fullsize, compact
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Properties
 | 
						|
 | 
						|
    weak var prepDelegate: AttachmentPrepViewControllerDelegate?
 | 
						|
 | 
						|
    let attachmentItem: SignalAttachmentItem
 | 
						|
    var attachment: SignalAttachment {
 | 
						|
        return attachmentItem.attachment
 | 
						|
    }
 | 
						|
 | 
						|
    private var videoPlayer: OWSVideoPlayer?
 | 
						|
 | 
						|
    private(set) var mediaMessageView: MediaMessageView!
 | 
						|
    private(set) var scrollView: UIScrollView!
 | 
						|
    private(set) var contentContainer: UIView!
 | 
						|
    private(set) var playVideoButton: UIView?
 | 
						|
    private var imageEditorView: ImageEditorView?
 | 
						|
 | 
						|
    public var shouldHideControls: Bool {
 | 
						|
        guard let imageEditorView = imageEditorView else {
 | 
						|
            return false
 | 
						|
        }
 | 
						|
        return imageEditorView.shouldHideControls
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Initializers
 | 
						|
 | 
						|
    init(attachmentItem: SignalAttachmentItem) {
 | 
						|
        self.attachmentItem = attachmentItem
 | 
						|
        super.init(nibName: nil, bundle: nil)
 | 
						|
        if attachment.hasError {
 | 
						|
            owsFailDebug(attachment.error.debugDescription)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public required init?(coder aDecoder: NSCoder) {
 | 
						|
        fatalError("init(coder:) has not been implemented")
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - View Lifecycle
 | 
						|
 | 
						|
    override public func loadView() {
 | 
						|
        self.view = UIView()
 | 
						|
 | 
						|
        self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval)
 | 
						|
 | 
						|
        // Anything that should be shrunk when user pops keyboard lives in the contentContainer.
 | 
						|
        let contentContainer = UIView()
 | 
						|
        self.contentContainer = contentContainer
 | 
						|
        view.addSubview(contentContainer)
 | 
						|
        contentContainer.autoPinEdgesToSuperviewEdges()
 | 
						|
 | 
						|
        // Scroll View - used to zoom/pan on images and video
 | 
						|
        scrollView = UIScrollView()
 | 
						|
        contentContainer.addSubview(scrollView)
 | 
						|
        scrollView.delegate = self
 | 
						|
        scrollView.showsHorizontalScrollIndicator = false
 | 
						|
        scrollView.showsVerticalScrollIndicator = false
 | 
						|
 | 
						|
        // Panning should stop pretty soon after the user stops scrolling
 | 
						|
        scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
 | 
						|
 | 
						|
        // We want scroll view content up and behind the system status bar content
 | 
						|
        // but we want other content (e.g. bar buttons) to respect the top layout guide.
 | 
						|
        self.automaticallyAdjustsScrollViewInsets = false
 | 
						|
 | 
						|
        scrollView.autoPinEdgesToSuperviewEdges()
 | 
						|
 | 
						|
        let backgroundColor = Colors.navigationBarBackground
 | 
						|
        self.view.backgroundColor = backgroundColor
 | 
						|
 | 
						|
        // Create full screen container view so the scrollView
 | 
						|
        // can compute an appropriate content size in which to center
 | 
						|
        // our media view.
 | 
						|
        let containerView = UIView.container()
 | 
						|
        scrollView.addSubview(containerView)
 | 
						|
        containerView.autoPinEdgesToSuperviewEdges()
 | 
						|
        containerView.autoMatch(.height, to: .height, of: self.view)
 | 
						|
        containerView.autoMatch(.width, to: .width, of: self.view)
 | 
						|
 | 
						|
        containerView.addSubview(mediaMessageView)
 | 
						|
        mediaMessageView.autoPinEdgesToSuperviewEdges()
 | 
						|
 | 
						|
        if let imageEditorModel = attachmentItem.imageEditorModel {
 | 
						|
 | 
						|
            let imageEditorView = ImageEditorView(model: imageEditorModel, delegate: self)
 | 
						|
            if imageEditorView.configureSubviews() {
 | 
						|
                self.imageEditorView = imageEditorView
 | 
						|
 | 
						|
                mediaMessageView.isHidden = true
 | 
						|
 | 
						|
                view.addSubview(imageEditorView)
 | 
						|
                imageEditorView.autoPinEdgesToSuperviewEdges()
 | 
						|
 | 
						|
                imageEditorUpdateNavigationBar()
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // Hide the play button embedded in the MediaView and replace it with our own.
 | 
						|
        // This allows us to zoom in on the media view without zooming in on the button
 | 
						|
        if attachment.isVideo {
 | 
						|
 | 
						|
            guard let videoURL = attachment.dataUrl else {
 | 
						|
                owsFailDebug("Missing videoURL")
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            let player = OWSVideoPlayer(url: videoURL)
 | 
						|
            self.videoPlayer = player
 | 
						|
            player.delegate = self
 | 
						|
 | 
						|
            let playerView = VideoPlayerView()
 | 
						|
            playerView.player = player.avPlayer
 | 
						|
            self.mediaMessageView.addSubview(playerView)
 | 
						|
            playerView.autoPinEdgesToSuperviewEdges()
 | 
						|
 | 
						|
            let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:)))
 | 
						|
            playerView.addGestureRecognizer(pauseGesture)
 | 
						|
 | 
						|
            let progressBar = PlayerProgressBar()
 | 
						|
            progressBar.player = player.avPlayer
 | 
						|
            progressBar.delegate = self
 | 
						|
 | 
						|
            // we don't want the progress bar to zoom during "pinch-to-zoom"
 | 
						|
            // but we do want it to shrink with the media content when the user
 | 
						|
            // pops the keyboard.
 | 
						|
            contentContainer.addSubview(progressBar)
 | 
						|
 | 
						|
            progressBar.autoPinEdge(.top, to: .top, of: view)
 | 
						|
            progressBar.autoPinWidthToSuperview()
 | 
						|
            progressBar.autoSetDimension(.height, toSize: 44)
 | 
						|
 | 
						|
            self.mediaMessageView.videoPlayButton?.isHidden = true
 | 
						|
            let playButton = UIButton()
 | 
						|
            self.playVideoButton = playButton
 | 
						|
            playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "Accessibility label for button to start media playback")
 | 
						|
            playButton.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal)
 | 
						|
            playButton.contentMode = .scaleAspectFit
 | 
						|
            playButton.autoSetDimension(.width, toSize: 72)
 | 
						|
            playButton.autoSetDimension(.height, toSize: 72)
 | 
						|
 | 
						|
            let playButtonWidth = ScaleFromIPhone5(70)
 | 
						|
            playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth))
 | 
						|
            self.contentContainer.addSubview(playButton)
 | 
						|
 | 
						|
            playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
 | 
						|
            playButton.autoCenterInSuperview()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    override public func viewWillAppear(_ animated: Bool) {
 | 
						|
        Logger.debug("")
 | 
						|
 | 
						|
        super.viewWillAppear(animated)
 | 
						|
 | 
						|
        prepDelegate?.prepViewControllerUpdateNavigationBar()
 | 
						|
        prepDelegate?.prepViewControllerUpdateControls()
 | 
						|
    }
 | 
						|
 | 
						|
    override public func viewDidAppear(_ animated: Bool) {
 | 
						|
        Logger.debug("")
 | 
						|
 | 
						|
        super.viewDidAppear(animated)
 | 
						|
 | 
						|
        prepDelegate?.prepViewControllerUpdateNavigationBar()
 | 
						|
        prepDelegate?.prepViewControllerUpdateControls()
 | 
						|
    }
 | 
						|
 | 
						|
    override public func viewWillLayoutSubviews() {
 | 
						|
        Logger.debug("")
 | 
						|
        super.viewWillLayoutSubviews()
 | 
						|
 | 
						|
        // e.g. if flipping to/from landscape
 | 
						|
        updateMinZoomScaleForSize(view.bounds.size)
 | 
						|
 | 
						|
        ensureAttachmentViewScale(animated: false)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Navigation Bar
 | 
						|
 | 
						|
    public func navigationBarItems() -> [UIView] {
 | 
						|
        guard let imageEditorView = imageEditorView else {
 | 
						|
            return []
 | 
						|
        }
 | 
						|
        return imageEditorView.navigationBarItems()
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Event Handlers
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) {
 | 
						|
        assert(self.videoPlayer != nil)
 | 
						|
        self.pauseVideo()
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func playButtonTapped() {
 | 
						|
        self.playVideo()
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Video
 | 
						|
 | 
						|
    private func playVideo() {
 | 
						|
        Logger.info("")
 | 
						|
 | 
						|
        guard let videoPlayer = self.videoPlayer else {
 | 
						|
            owsFailDebug("video player was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        guard let playVideoButton = self.playVideoButton else {
 | 
						|
            owsFailDebug("playVideoButton was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
        UIView.animate(withDuration: 0.1) {
 | 
						|
            playVideoButton.alpha = 0.0
 | 
						|
        }
 | 
						|
        videoPlayer.play()
 | 
						|
    }
 | 
						|
 | 
						|
    private func pauseVideo() {
 | 
						|
        guard let videoPlayer = self.videoPlayer else {
 | 
						|
            owsFailDebug("video player was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        videoPlayer.pause()
 | 
						|
        guard let playVideoButton = self.playVideoButton else {
 | 
						|
            owsFailDebug("playVideoButton was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
        UIView.animate(withDuration: 0.1) {
 | 
						|
            playVideoButton.alpha = 1.0
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) {
 | 
						|
        guard let playVideoButton = self.playVideoButton else {
 | 
						|
            owsFailDebug("playVideoButton was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        UIView.animate(withDuration: 0.1) {
 | 
						|
            playVideoButton.alpha = 1.0
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) {
 | 
						|
        guard let videoPlayer = self.videoPlayer else {
 | 
						|
            owsFailDebug("video player was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
        videoPlayer.pause()
 | 
						|
    }
 | 
						|
 | 
						|
    public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) {
 | 
						|
        guard let videoPlayer = self.videoPlayer else {
 | 
						|
            owsFailDebug("video player was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        videoPlayer.seek(to: time)
 | 
						|
    }
 | 
						|
 | 
						|
    public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) {
 | 
						|
        guard let videoPlayer = self.videoPlayer else {
 | 
						|
            owsFailDebug("video player was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        videoPlayer.seek(to: time)
 | 
						|
        if (shouldResumePlayback) {
 | 
						|
            videoPlayer.play()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Helpers
 | 
						|
 | 
						|
    var isZoomable: Bool {
 | 
						|
        return attachment.isImage || attachment.isVideo
 | 
						|
    }
 | 
						|
 | 
						|
    func zoomOut(animated: Bool) {
 | 
						|
        if self.scrollView.zoomScale != self.scrollView.minimumZoomScale {
 | 
						|
            self.scrollView.setZoomScale(self.scrollView.minimumZoomScale, animated: animated)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // When the keyboard is popped, it can obscure the attachment view.
 | 
						|
    // so we sometimes allow resizing the attachment.
 | 
						|
    var shouldAllowAttachmentViewResizing: Bool = true
 | 
						|
 | 
						|
    var attachmentViewScale: AttachmentViewScale = .fullsize
 | 
						|
    public func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) {
 | 
						|
        self.attachmentViewScale = attachmentViewScale
 | 
						|
        ensureAttachmentViewScale(animated: animated)
 | 
						|
    }
 | 
						|
 | 
						|
    func ensureAttachmentViewScale(animated: Bool) {
 | 
						|
        let animationDuration = animated ? 0.2 : 0
 | 
						|
        guard shouldAllowAttachmentViewResizing else {
 | 
						|
            if self.contentContainer.transform != CGAffineTransform.identity {
 | 
						|
                UIView.animate(withDuration: animationDuration) {
 | 
						|
                    self.contentContainer.transform = CGAffineTransform.identity
 | 
						|
                }
 | 
						|
            }
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        switch attachmentViewScale {
 | 
						|
        case .fullsize:
 | 
						|
            guard self.contentContainer.transform != .identity else {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            UIView.animate(withDuration: animationDuration) {
 | 
						|
                self.contentContainer.transform = CGAffineTransform.identity
 | 
						|
            }
 | 
						|
        case .compact:
 | 
						|
            guard self.contentContainer.transform == .identity else {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            UIView.animate(withDuration: animationDuration) {
 | 
						|
                let kScaleFactor: CGFloat = 0.7
 | 
						|
                let scale = CGAffineTransform(scaleX: kScaleFactor, y: kScaleFactor)
 | 
						|
 | 
						|
                let originalHeight = self.scrollView.bounds.size.height
 | 
						|
 | 
						|
                // Position the new scaled item to be centered with respect
 | 
						|
                // to it's new size.
 | 
						|
                let heightDelta = originalHeight * (1 - kScaleFactor)
 | 
						|
                let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2)
 | 
						|
 | 
						|
                self.contentContainer.transform = scale.concatenating(translate)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
extension AttachmentPrepViewController: UIScrollViewDelegate {
 | 
						|
 | 
						|
    public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
 | 
						|
        if isZoomable {
 | 
						|
            return mediaMessageView
 | 
						|
        } else {
 | 
						|
            // don't zoom for audio or generic attachments.
 | 
						|
            return nil
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    fileprivate func updateMinZoomScaleForSize(_ size: CGSize) {
 | 
						|
        Logger.debug("")
 | 
						|
 | 
						|
        // Ensure bounds have been computed
 | 
						|
        mediaMessageView.layoutIfNeeded()
 | 
						|
        guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else {
 | 
						|
            Logger.warn("bad bounds")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        let widthScale = size.width / mediaMessageView.bounds.width
 | 
						|
        let heightScale = size.height / mediaMessageView.bounds.height
 | 
						|
        let minScale = min(widthScale, heightScale)
 | 
						|
        scrollView.maximumZoomScale = minScale * 5.0
 | 
						|
        scrollView.minimumZoomScale = minScale
 | 
						|
        scrollView.zoomScale = minScale
 | 
						|
    }
 | 
						|
 | 
						|
    // Keep the media view centered within the scroll view as you zoom
 | 
						|
    public func scrollViewDidZoom(_ scrollView: UIScrollView) {
 | 
						|
        // The scroll view has zoomed, so you need to re-center the contents
 | 
						|
        let scrollViewSize = self.scrollViewVisibleSize
 | 
						|
 | 
						|
        // First assume that mediaMessageView center coincides with the contents center
 | 
						|
        // This is correct when the mediaMessageView is bigger than scrollView due to zoom
 | 
						|
        var contentCenter = CGPoint(x: (scrollView.contentSize.width / 2), y: (scrollView.contentSize.height / 2))
 | 
						|
 | 
						|
        let scrollViewCenter = self.scrollViewCenter
 | 
						|
 | 
						|
        // if mediaMessageView is smaller than the scrollView visible size - fix the content center accordingly
 | 
						|
        if self.scrollView.contentSize.width < scrollViewSize.width {
 | 
						|
            contentCenter.x = scrollViewCenter.x
 | 
						|
        }
 | 
						|
 | 
						|
        if self.scrollView.contentSize.height < scrollViewSize.height {
 | 
						|
            contentCenter.y = scrollViewCenter.y
 | 
						|
        }
 | 
						|
 | 
						|
        self.mediaMessageView.center = contentCenter
 | 
						|
    }
 | 
						|
 | 
						|
    // return the scroll view center
 | 
						|
    private var scrollViewCenter: CGPoint {
 | 
						|
        let size = scrollViewVisibleSize
 | 
						|
        return CGPoint(x: (size.width / 2), y: (size.height / 2))
 | 
						|
    }
 | 
						|
 | 
						|
    // Return scrollview size without the area overlapping with tab and nav bar.
 | 
						|
    private var scrollViewVisibleSize: CGSize {
 | 
						|
        let contentInset = scrollView.contentInset
 | 
						|
        let scrollViewSize = scrollView.bounds.standardized.size
 | 
						|
        let width = scrollViewSize.width - (contentInset.left + contentInset.right)
 | 
						|
        let height = scrollViewSize.height - (contentInset.top + contentInset.bottom)
 | 
						|
        return CGSize(width: width, height: height)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: -
 | 
						|
 | 
						|
extension AttachmentPrepViewController: ImageEditorViewDelegate {
 | 
						|
    public func imageEditor(presentFullScreenView viewController: UIViewController,
 | 
						|
                            isTransparent: Bool) {
 | 
						|
 | 
						|
        let navigationController = OWSNavigationController(rootViewController: viewController)
 | 
						|
        navigationController.modalPresentationStyle = (isTransparent
 | 
						|
            ? .overFullScreen
 | 
						|
            : .fullScreen)
 | 
						|
        navigationController.ows_prefersStatusBarHidden = true
 | 
						|
        navigationController.view.backgroundColor = Colors.navigationBarBackground
 | 
						|
 | 
						|
        if let navigationBar = navigationController.navigationBar as? OWSNavigationBar {
 | 
						|
            navigationBar.overrideTheme(type: .clear)
 | 
						|
        } else {
 | 
						|
            owsFailDebug("navigationBar was nil or unexpected class")
 | 
						|
        }
 | 
						|
 | 
						|
        self.present(navigationController, animated: false) {
 | 
						|
            // Do nothing.
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public func imageEditorUpdateNavigationBar() {
 | 
						|
        prepDelegate?.prepViewControllerUpdateNavigationBar()
 | 
						|
    }
 | 
						|
 | 
						|
    public func imageEditorUpdateControls() {
 | 
						|
        prepDelegate?.prepViewControllerUpdateControls()
 | 
						|
    }
 | 
						|
}
 |