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.
		
		
		
		
		
			
		
			
				
	
	
		
			678 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			678 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import UIKit
 | 
						|
import Combine
 | 
						|
import AVFoundation
 | 
						|
import SessionUIKit
 | 
						|
import SignalUtilitiesKit
 | 
						|
import SignalCoreKit
 | 
						|
import SessionUtilitiesKit
 | 
						|
 | 
						|
protocol PhotoCaptureViewControllerDelegate: AnyObject {
 | 
						|
    func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment)
 | 
						|
    func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController)
 | 
						|
}
 | 
						|
 | 
						|
enum PhotoCaptureError: Error {
 | 
						|
    case assertionError(description: String)
 | 
						|
    case initializationFailed
 | 
						|
    case captureFailed
 | 
						|
}
 | 
						|
 | 
						|
extension PhotoCaptureError: LocalizedError {
 | 
						|
    var localizedDescription: String {
 | 
						|
        switch self {
 | 
						|
        case .initializationFailed:
 | 
						|
            return NSLocalizedString("PHOTO_CAPTURE_UNABLE_TO_INITIALIZE_CAMERA", comment: "alert title")
 | 
						|
        case .captureFailed:
 | 
						|
            return NSLocalizedString("PHOTO_CAPTURE_UNABLE_TO_CAPTURE_IMAGE", comment: "alert title")
 | 
						|
        case .assertionError:
 | 
						|
            return NSLocalizedString("PHOTO_CAPTURE_GENERIC_ERROR", comment: "alert title, generic error preventing user from capturing a photo")
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
class PhotoCaptureViewController: OWSViewController {
 | 
						|
 | 
						|
    weak var delegate: PhotoCaptureViewControllerDelegate?
 | 
						|
 | 
						|
    private var photoCapture: PhotoCapture!
 | 
						|
 | 
						|
    deinit {
 | 
						|
        UIDevice.current.endGeneratingDeviceOrientationNotifications()
 | 
						|
        if let photoCapture = photoCapture {
 | 
						|
            photoCapture.stopCapture()
 | 
						|
                .sinkUntilComplete(
 | 
						|
                    receiveCompletion: { result in
 | 
						|
                        switch result {
 | 
						|
                            case .failure: break
 | 
						|
                            case .finished: Logger.debug("stopCapture completed")
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                )
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Overrides
 | 
						|
 | 
						|
    override func loadView() {
 | 
						|
        self.view = UIView()
 | 
						|
        self.view.themeBackgroundColor = .newConversation_background
 | 
						|
    }
 | 
						|
 | 
						|
    override func viewDidLoad() {
 | 
						|
        super.viewDidLoad()
 | 
						|
        setupPhotoCapture()
 | 
						|
        setupOrientationMonitoring()
 | 
						|
        
 | 
						|
        updateNavigationItems()
 | 
						|
        updateFlashModeControl()
 | 
						|
 | 
						|
        let initialCaptureOrientation = AVCaptureVideoOrientation(deviceOrientation: UIDevice.current.orientation) ?? .portrait
 | 
						|
        updateIconOrientations(isAnimated: false, captureOrientation: initialCaptureOrientation)
 | 
						|
 | 
						|
        view.addGestureRecognizer(pinchZoomGesture)
 | 
						|
        view.addGestureRecognizer(focusGesture)
 | 
						|
        view.addGestureRecognizer(doubleTapToSwitchCameraGesture)
 | 
						|
    }
 | 
						|
 | 
						|
    override var prefersStatusBarHidden: Bool {
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: -
 | 
						|
    var isRecordingMovie: Bool = false
 | 
						|
    let recordingTimerView = RecordingTimerView()
 | 
						|
 | 
						|
    func updateNavigationItems() {
 | 
						|
        if isRecordingMovie {
 | 
						|
            navigationItem.leftBarButtonItem = nil
 | 
						|
            navigationItem.rightBarButtonItems = nil
 | 
						|
            navigationItem.titleView = recordingTimerView
 | 
						|
            recordingTimerView.sizeToFit()
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            navigationItem.titleView = nil
 | 
						|
            navigationItem.leftBarButtonItem = dismissControl.barButtonItem
 | 
						|
            let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
 | 
						|
            fixedSpace.width = 16
 | 
						|
 | 
						|
            navigationItem.rightBarButtonItems = [switchCameraControl.barButtonItem, fixedSpace, flashModeControl.barButtonItem]
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
 | 
						|
    // If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
 | 
						|
    // the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
 | 
						|
    override public var canBecomeFirstResponder: Bool {
 | 
						|
        Logger.debug("")
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
 | 
						|
        return .portrait
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Views
 | 
						|
 | 
						|
    let captureButton = CaptureButton()
 | 
						|
    var previewView: CapturePreviewView!
 | 
						|
 | 
						|
    class PhotoControl {
 | 
						|
        let button: OWSButton
 | 
						|
        let barButtonItem: UIBarButtonItem
 | 
						|
 | 
						|
        init(imageName: String, block: @escaping () -> Void) {
 | 
						|
            self.button = OWSButton(imageName: imageName, tintColor: .white, block: block)
 | 
						|
            button.autoPinToSquareAspectRatio()
 | 
						|
            button.themeShadowColor = .black
 | 
						|
            button.layer.shadowOffset = CGSize.zero
 | 
						|
            button.layer.shadowOpacity = 0.35
 | 
						|
            button.layer.shadowRadius = 4
 | 
						|
 | 
						|
            self.barButtonItem = UIBarButtonItem(customView: button)
 | 
						|
        }
 | 
						|
 | 
						|
        func setImage(imageName: String) {
 | 
						|
            button.setImage(imageName: imageName)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private lazy var dismissControl: PhotoControl = {
 | 
						|
        return PhotoControl(imageName: "X") { [weak self] in
 | 
						|
            self?.didTapClose()
 | 
						|
        }
 | 
						|
    }()
 | 
						|
 | 
						|
    private lazy var switchCameraControl: PhotoControl = {
 | 
						|
        return PhotoControl(imageName: "ic_switch_camera") { [weak self] in
 | 
						|
            self?.didTapSwitchCamera()
 | 
						|
        }
 | 
						|
    }()
 | 
						|
 | 
						|
    private lazy var flashModeControl: PhotoControl = {
 | 
						|
        return PhotoControl(imageName: "ic_flash_mode_auto") { [weak self] in
 | 
						|
            self?.didTapFlashMode()
 | 
						|
        }
 | 
						|
    }()
 | 
						|
 | 
						|
    lazy var pinchZoomGesture: UIPinchGestureRecognizer = {
 | 
						|
        return UIPinchGestureRecognizer(target: self, action: #selector(didPinchZoom(pinchGesture:)))
 | 
						|
    }()
 | 
						|
 | 
						|
    lazy var focusGesture: UITapGestureRecognizer = {
 | 
						|
        return UITapGestureRecognizer(target: self, action: #selector(didTapFocusExpose(tapGesture:)))
 | 
						|
    }()
 | 
						|
 | 
						|
    lazy var doubleTapToSwitchCameraGesture: UITapGestureRecognizer = {
 | 
						|
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTapToSwitchCamera(tapGesture:)))
 | 
						|
        tapGesture.numberOfTapsRequired = 2
 | 
						|
        return tapGesture
 | 
						|
    }()
 | 
						|
 | 
						|
    // MARK: - Events
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didTapClose() {
 | 
						|
        self.delegate?.photoCaptureViewControllerDidCancel(self)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didTapSwitchCamera() {
 | 
						|
        Logger.debug("")
 | 
						|
        switchCamera()
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didDoubleTapToSwitchCamera(tapGesture: UITapGestureRecognizer) {
 | 
						|
        Logger.debug("")
 | 
						|
        switchCamera()
 | 
						|
    }
 | 
						|
 | 
						|
    private func switchCamera() {
 | 
						|
        UIView.animate(withDuration: 0.2) {
 | 
						|
            let epsilonToForceCounterClockwiseRotation: CGFloat = 0.00001
 | 
						|
            self.switchCameraControl.button.transform = self.switchCameraControl.button.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation)
 | 
						|
        }
 | 
						|
        
 | 
						|
        photoCapture.switchCamera()
 | 
						|
            .receive(on: DispatchQueue.main)
 | 
						|
            .sinkUntilComplete(
 | 
						|
                receiveCompletion: { [weak self] result in
 | 
						|
                    switch result {
 | 
						|
                        case .finished: break
 | 
						|
                        case .failure(let error): self?.showFailureUI(error: error)
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            )
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didTapFlashMode() {
 | 
						|
        Logger.debug("")
 | 
						|
        photoCapture.switchFlashMode()
 | 
						|
            .receive(on: DispatchQueue.main)
 | 
						|
            .sinkUntilComplete(
 | 
						|
                receiveCompletion: { [weak self] _ in
 | 
						|
                    self?.updateFlashModeControl()
 | 
						|
                }
 | 
						|
            )
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didPinchZoom(pinchGesture: UIPinchGestureRecognizer) {
 | 
						|
        switch pinchGesture.state {
 | 
						|
        case .began: fallthrough
 | 
						|
        case .changed:
 | 
						|
            photoCapture.updateZoom(scaleFromPreviousZoomFactor: pinchGesture.scale)
 | 
						|
        case .ended:
 | 
						|
            photoCapture.completeZoom(scaleFromPreviousZoomFactor: pinchGesture.scale)
 | 
						|
        default:
 | 
						|
            break
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didTapFocusExpose(tapGesture: UITapGestureRecognizer) {
 | 
						|
        let viewLocation = tapGesture.location(in: view)
 | 
						|
        let devicePoint = previewView.previewLayer.captureDevicePointConverted(fromLayerPoint: viewLocation)
 | 
						|
        photoCapture.focus(with: .autoFocus, exposureMode: .autoExpose, at: devicePoint, monitorSubjectAreaChange: true)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Orientation
 | 
						|
 | 
						|
    private func setupOrientationMonitoring() {
 | 
						|
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()
 | 
						|
 | 
						|
        NotificationCenter.default.addObserver(
 | 
						|
            self,
 | 
						|
            selector: #selector(didChangeDeviceOrientation),
 | 
						|
            name: UIDevice.orientationDidChangeNotification,
 | 
						|
            object: UIDevice.current
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    var lastKnownCaptureOrientation: AVCaptureVideoOrientation = .portrait
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didChangeDeviceOrientation(notification: Notification) {
 | 
						|
        let currentOrientation = UIDevice.current.orientation
 | 
						|
 | 
						|
        if let captureOrientation = AVCaptureVideoOrientation(deviceOrientation: currentOrientation) {
 | 
						|
            // since the "face up" and "face down" orientations aren't reflected in the photo output,
 | 
						|
            // we need to capture the last known _other_ orientation so we can reflect the appropriate
 | 
						|
            // portrait/landscape in our captured photos.
 | 
						|
            Logger.verbose("lastKnownCaptureOrientation: \(lastKnownCaptureOrientation)->\(captureOrientation)")
 | 
						|
            lastKnownCaptureOrientation = captureOrientation
 | 
						|
            updateIconOrientations(isAnimated: true, captureOrientation: captureOrientation)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: -
 | 
						|
 | 
						|
    private func updateIconOrientations(isAnimated: Bool, captureOrientation: AVCaptureVideoOrientation) {
 | 
						|
        Logger.verbose("captureOrientation: \(captureOrientation)")
 | 
						|
 | 
						|
        let transformFromOrientation: CGAffineTransform
 | 
						|
        switch captureOrientation {
 | 
						|
            case .portrait: transformFromOrientation = .identity
 | 
						|
            case .portraitUpsideDown: transformFromOrientation = CGAffineTransform(rotationAngle: .pi)
 | 
						|
            case .landscapeLeft: transformFromOrientation = CGAffineTransform(rotationAngle: .halfPi)
 | 
						|
            case .landscapeRight: transformFromOrientation = CGAffineTransform(rotationAngle: -1 * .halfPi)
 | 
						|
            @unknown default: transformFromOrientation = .identity
 | 
						|
        }
 | 
						|
 | 
						|
        // Don't "unrotate" the switch camera icon if the front facing camera had been selected.
 | 
						|
        let tranformFromCameraType: CGAffineTransform = photoCapture.desiredPosition == .front ? CGAffineTransform(rotationAngle: -.pi) : .identity
 | 
						|
 | 
						|
        let updateOrientation = {
 | 
						|
            self.flashModeControl.button.transform = transformFromOrientation
 | 
						|
            self.switchCameraControl.button.transform   = transformFromOrientation.concatenating(tranformFromCameraType)
 | 
						|
        }
 | 
						|
 | 
						|
        if isAnimated {
 | 
						|
            UIView.animate(withDuration: 0.3, animations: updateOrientation)
 | 
						|
        } else {
 | 
						|
            updateOrientation()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private func setupPhotoCapture() {
 | 
						|
        photoCapture = PhotoCapture()
 | 
						|
        photoCapture.delegate = self
 | 
						|
        captureButton.delegate = photoCapture
 | 
						|
        previewView = CapturePreviewView(session: photoCapture.session)
 | 
						|
 | 
						|
        photoCapture.startCapture()
 | 
						|
            .receive(on: DispatchQueue.main)
 | 
						|
            .sinkUntilComplete(
 | 
						|
                receiveCompletion: { [weak self] result in
 | 
						|
                    switch result {
 | 
						|
                        case .finished: self?.showCaptureUI()
 | 
						|
                        case .failure(let error): self?.showFailureUI(error: error)
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            )
 | 
						|
    }
 | 
						|
 | 
						|
    private func showCaptureUI() {
 | 
						|
        Logger.debug("")
 | 
						|
        view.addSubview(previewView)
 | 
						|
        if UIDevice.current.hasIPhoneXNotch {
 | 
						|
            previewView.autoPinEdgesToSuperviewEdges()
 | 
						|
        } else {
 | 
						|
            previewView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 0, bottom: 40, trailing: 0))
 | 
						|
        }
 | 
						|
 | 
						|
        view.addSubview(captureButton)
 | 
						|
        captureButton.autoHCenterInSuperview()
 | 
						|
        captureButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: SendMediaNavigationController.bottomButtonsCenterOffset).isActive = true
 | 
						|
    }
 | 
						|
 | 
						|
    private func showFailureUI(error: Error) {
 | 
						|
        Logger.error("error: \(error)")
 | 
						|
        let modal: ConfirmationModal = ConfirmationModal(
 | 
						|
            info: ConfirmationModal.Info(
 | 
						|
                title: CommonStrings.errorAlertTitle,
 | 
						|
                body: .text(error.localizedDescription),
 | 
						|
                cancelTitle: CommonStrings.dismissButton,
 | 
						|
                cancelStyle: .alert_text,
 | 
						|
                afterClosed: { [weak self] in self?.dismiss(animated: true) }
 | 
						|
            )
 | 
						|
        )
 | 
						|
        
 | 
						|
        present(modal, animated: true)
 | 
						|
    }
 | 
						|
 | 
						|
    private func updateFlashModeControl() {
 | 
						|
        let imageName: String
 | 
						|
        switch photoCapture.flashMode {
 | 
						|
        case .auto:
 | 
						|
            imageName = "ic_flash_mode_auto"
 | 
						|
        case .on:
 | 
						|
            imageName = "ic_flash_mode_on"
 | 
						|
        case .off:
 | 
						|
            imageName = "ic_flash_mode_off"
 | 
						|
        default: preconditionFailure()
 | 
						|
        }
 | 
						|
 | 
						|
        self.flashModeControl.setImage(imageName: imageName)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension PhotoCaptureViewController: PhotoCaptureDelegate {
 | 
						|
 | 
						|
    // MARK: - Photo
 | 
						|
 | 
						|
    func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment) {
 | 
						|
        delegate?.photoCaptureViewController(self, didFinishProcessingAttachment: attachment)
 | 
						|
    }
 | 
						|
 | 
						|
    func photoCapture(_ photoCapture: PhotoCapture, processingDidError error: Error) {
 | 
						|
        showFailureUI(error: error)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Video
 | 
						|
 | 
						|
    func photoCaptureDidBeginVideo(_ photoCapture: PhotoCapture) {
 | 
						|
        isRecordingMovie = true
 | 
						|
        updateNavigationItems()
 | 
						|
        recordingTimerView.startCounting()
 | 
						|
    }
 | 
						|
 | 
						|
    func photoCaptureDidCompleteVideo(_ photoCapture: PhotoCapture) {
 | 
						|
        isRecordingMovie = false
 | 
						|
        recordingTimerView.stopCounting()
 | 
						|
        updateNavigationItems()
 | 
						|
    }
 | 
						|
 | 
						|
    func photoCaptureDidCancelVideo(_ photoCapture: PhotoCapture) {
 | 
						|
        owsFailDebug("If we ever allow this, we should test.")
 | 
						|
        isRecordingMovie = false
 | 
						|
        recordingTimerView.stopCounting()
 | 
						|
        updateNavigationItems()
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: -
 | 
						|
 | 
						|
    var zoomScaleReferenceHeight: CGFloat? {
 | 
						|
        return view.bounds.height
 | 
						|
    }
 | 
						|
 | 
						|
    var captureOrientation: AVCaptureVideoOrientation {
 | 
						|
        return lastKnownCaptureOrientation
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: - Views
 | 
						|
 | 
						|
protocol CaptureButtonDelegate: AnyObject {
 | 
						|
    // MARK: Photo
 | 
						|
    func didTapCaptureButton(_ captureButton: CaptureButton)
 | 
						|
 | 
						|
    // MARK: Video
 | 
						|
    func didBeginLongPressCaptureButton(_ captureButton: CaptureButton)
 | 
						|
    func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton)
 | 
						|
    func didCancelLongPressCaptureButton(_ captureButton: CaptureButton)
 | 
						|
 | 
						|
    var zoomScaleReferenceHeight: CGFloat? { get }
 | 
						|
    func longPressCaptureButton(_ captureButton: CaptureButton, didUpdateZoomAlpha zoomAlpha: CGFloat)
 | 
						|
}
 | 
						|
 | 
						|
class CaptureButton: UIView {
 | 
						|
 | 
						|
    let innerButton = CircleView()
 | 
						|
 | 
						|
    var tapGesture: UITapGestureRecognizer!
 | 
						|
 | 
						|
    var longPressGesture: UILongPressGestureRecognizer!
 | 
						|
    let longPressDuration = 0.5
 | 
						|
 | 
						|
    let zoomIndicator = CircleView()
 | 
						|
 | 
						|
    weak var delegate: CaptureButtonDelegate?
 | 
						|
 | 
						|
    let defaultDiameter: CGFloat = ScaleFromIPhone5To7Plus(60, 80)
 | 
						|
    let recordingDiameter: CGFloat = ScaleFromIPhone5To7Plus(68, 120)
 | 
						|
    var innerButtonSizeConstraints: [NSLayoutConstraint]!
 | 
						|
    var zoomIndicatorSizeConstraints: [NSLayoutConstraint]!
 | 
						|
 | 
						|
    override init(frame: CGRect) {
 | 
						|
        super.init(frame: frame)
 | 
						|
 | 
						|
        tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
 | 
						|
        innerButton.addGestureRecognizer(tapGesture)
 | 
						|
 | 
						|
        longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
 | 
						|
        longPressGesture.minimumPressDuration = longPressDuration
 | 
						|
        innerButton.addGestureRecognizer(longPressGesture)
 | 
						|
 | 
						|
        addSubview(innerButton)
 | 
						|
        innerButtonSizeConstraints = autoSetDimensions(to: CGSize(width: defaultDiameter, height: defaultDiameter))
 | 
						|
        innerButton.themeBackgroundColor = .white
 | 
						|
        innerButton.layer.shadowOffset = .zero
 | 
						|
        innerButton.layer.shadowOpacity = 0.33
 | 
						|
        innerButton.layer.shadowRadius = 2
 | 
						|
        innerButton.alpha = 0.33
 | 
						|
        innerButton.autoPinEdgesToSuperviewEdges()
 | 
						|
 | 
						|
        addSubview(zoomIndicator)
 | 
						|
        zoomIndicatorSizeConstraints = zoomIndicator.autoSetDimensions(to: CGSize(width: defaultDiameter, height: defaultDiameter))
 | 
						|
        zoomIndicator.isUserInteractionEnabled = false
 | 
						|
        zoomIndicator.themeBorderColor = .white
 | 
						|
        zoomIndicator.layer.borderWidth = 1.5
 | 
						|
        zoomIndicator.autoAlignAxis(.horizontal, toSameAxisOf: innerButton)
 | 
						|
        zoomIndicator.autoAlignAxis(.vertical, toSameAxisOf: innerButton)
 | 
						|
    }
 | 
						|
 | 
						|
    required init?(coder aDecoder: NSCoder) {
 | 
						|
        fatalError("init(coder:) has not been implemented")
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Gestures
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didTap(_ gesture: UITapGestureRecognizer) {
 | 
						|
        delegate?.didTapCaptureButton(self)
 | 
						|
    }
 | 
						|
 | 
						|
    var initialTouchLocation: CGPoint?
 | 
						|
 | 
						|
    @objc
 | 
						|
    func didLongPress(_ gesture: UILongPressGestureRecognizer) {
 | 
						|
        Logger.verbose("")
 | 
						|
 | 
						|
        guard let gestureView = gesture.view else {
 | 
						|
            owsFailDebug("gestureView was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        switch gesture.state {
 | 
						|
        case .possible: break
 | 
						|
        case .began:
 | 
						|
            initialTouchLocation = gesture.location(in: gesture.view)
 | 
						|
            delegate?.didBeginLongPressCaptureButton(self)
 | 
						|
            UIView.animate(withDuration: 0.2) {
 | 
						|
                self.innerButtonSizeConstraints.forEach { $0.constant = self.recordingDiameter }
 | 
						|
                self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.recordingDiameter }
 | 
						|
                self.superview?.layoutIfNeeded()
 | 
						|
            }
 | 
						|
        case .changed:
 | 
						|
            guard let referenceHeight = delegate?.zoomScaleReferenceHeight else {
 | 
						|
                owsFailDebug("referenceHeight was unexpectedly nil")
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            guard referenceHeight > 0 else {
 | 
						|
                owsFailDebug("referenceHeight was unexpectedly <= 0")
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            guard let initialTouchLocation = initialTouchLocation else {
 | 
						|
                owsFailDebug("initialTouchLocation was unexpectedly nil")
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            let currentLocation = gesture.location(in: gestureView)
 | 
						|
            let minDistanceBeforeActivatingZoom: CGFloat = 30
 | 
						|
            let distance = initialTouchLocation.y - currentLocation.y - minDistanceBeforeActivatingZoom
 | 
						|
            let distanceForFullZoom = referenceHeight / 4
 | 
						|
            let ratio = distance / distanceForFullZoom
 | 
						|
 | 
						|
            let alpha = ratio.clamp(0, 1)
 | 
						|
 | 
						|
            Logger.verbose("distance: \(distance), alpha: \(alpha)")
 | 
						|
 | 
						|
            let zoomIndicatorDiameter = CGFloatLerp(recordingDiameter, 3, alpha)
 | 
						|
            self.zoomIndicatorSizeConstraints.forEach { $0.constant = zoomIndicatorDiameter }
 | 
						|
            zoomIndicator.superview?.layoutIfNeeded()
 | 
						|
 | 
						|
            delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: alpha)
 | 
						|
        case .ended:
 | 
						|
            UIView.animate(withDuration: 0.2) {
 | 
						|
                self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter }
 | 
						|
                self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter }
 | 
						|
 | 
						|
                self.superview?.layoutIfNeeded()
 | 
						|
            }
 | 
						|
            delegate?.didCompleteLongPressCaptureButton(self)
 | 
						|
        case .cancelled, .failed:
 | 
						|
 | 
						|
            UIView.animate(withDuration: 0.2) {
 | 
						|
                self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter }
 | 
						|
                self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter }
 | 
						|
 | 
						|
                self.superview?.layoutIfNeeded()
 | 
						|
            }
 | 
						|
            delegate?.didCancelLongPressCaptureButton(self)
 | 
						|
        default: break
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
class CapturePreviewView: UIView {
 | 
						|
 | 
						|
    let previewLayer: AVCaptureVideoPreviewLayer
 | 
						|
 | 
						|
    override var bounds: CGRect {
 | 
						|
        didSet {
 | 
						|
            previewLayer.frame = bounds
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    init(session: AVCaptureSession) {
 | 
						|
        previewLayer = AVCaptureVideoPreviewLayer(session: session)
 | 
						|
        super.init(frame: .zero)
 | 
						|
        self.contentMode = .scaleAspectFill
 | 
						|
        previewLayer.frame = bounds
 | 
						|
        layer.addSublayer(previewLayer)
 | 
						|
    }
 | 
						|
 | 
						|
    required init?(coder aDecoder: NSCoder) {
 | 
						|
        fatalError("init(coder:) has not been implemented")
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
class RecordingTimerView: UIView {
 | 
						|
 | 
						|
    let stackViewSpacing: CGFloat = 4
 | 
						|
 | 
						|
    override init(frame: CGRect) {
 | 
						|
        super.init(frame: frame)
 | 
						|
 | 
						|
        let stackView = UIStackView(arrangedSubviews: [icon, label])
 | 
						|
        stackView.axis = .horizontal
 | 
						|
        stackView.alignment = .center
 | 
						|
        stackView.spacing = stackViewSpacing
 | 
						|
 | 
						|
        addSubview(stackView)
 | 
						|
        stackView.autoPinEdgesToSuperviewMargins()
 | 
						|
 | 
						|
        updateView()
 | 
						|
    }
 | 
						|
 | 
						|
    required init?(coder aDecoder: NSCoder) {
 | 
						|
        fatalError("init(coder:) has not been implemented")
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Subviews
 | 
						|
 | 
						|
    private lazy var label: UILabel = {
 | 
						|
        let label: UILabel = UILabel()
 | 
						|
        label.font = UIFont.monospacedDigitSystemFont(ofSize: 20, weight: .regular)
 | 
						|
        label.themeTextColor = .textPrimary
 | 
						|
        label.textAlignment = .center
 | 
						|
        label.layer.shadowOffset = CGSize.zero
 | 
						|
        label.layer.shadowOpacity = 0.35
 | 
						|
        label.layer.shadowRadius = 4
 | 
						|
 | 
						|
        return label
 | 
						|
    }()
 | 
						|
 | 
						|
    static let iconWidth: CGFloat = 6
 | 
						|
 | 
						|
    private let icon: UIView = {
 | 
						|
        let icon = CircleView()
 | 
						|
        icon.layer.shadowOffset = CGSize.zero
 | 
						|
        icon.layer.shadowOpacity = 0.35
 | 
						|
        icon.layer.shadowRadius = 4
 | 
						|
        icon.themeBackgroundColor = .danger
 | 
						|
        icon.autoSetDimensions(to: CGSize(width: iconWidth, height: iconWidth))
 | 
						|
        icon.alpha = 0
 | 
						|
 | 
						|
        return icon
 | 
						|
    }()
 | 
						|
 | 
						|
    // MARK: -
 | 
						|
    var recordingStartTime: TimeInterval?
 | 
						|
 | 
						|
    func startCounting() {
 | 
						|
        recordingStartTime = CACurrentMediaTime()
 | 
						|
        timer = Timer.weakScheduledTimer(withTimeInterval: 0.1, target: self, selector: #selector(updateView), userInfo: nil, repeats: true)
 | 
						|
        UIView.animate(
 | 
						|
            withDuration: 0.5,
 | 
						|
            delay: 0,
 | 
						|
            options: [.autoreverse, .repeat],
 | 
						|
            animations: { self.icon.alpha = 1 }
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    func stopCounting() {
 | 
						|
        timer?.invalidate()
 | 
						|
        timer = nil
 | 
						|
        icon.layer.removeAllAnimations()
 | 
						|
        UIView.animate(withDuration: 0.4) {
 | 
						|
            self.icon.alpha = 0
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: -
 | 
						|
 | 
						|
    private var timer: Timer?
 | 
						|
 | 
						|
    private lazy var timeFormatter: DateFormatter = {
 | 
						|
        let formatter = DateFormatter()
 | 
						|
        formatter.dateFormat = "mm:ss"
 | 
						|
        formatter.timeZone = TimeZone(identifier: "UTC")!
 | 
						|
 | 
						|
        return formatter
 | 
						|
    }()
 | 
						|
 | 
						|
    // This method should only be called when the call state is "connected".
 | 
						|
    var recordingDuration: TimeInterval {
 | 
						|
        guard let recordingStartTime = recordingStartTime else {
 | 
						|
            return 0
 | 
						|
        }
 | 
						|
 | 
						|
        return CACurrentMediaTime() - recordingStartTime
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    private func updateView() {
 | 
						|
        let recordingDuration = self.recordingDuration
 | 
						|
        Logger.verbose("recordingDuration: \(recordingDuration)")
 | 
						|
        let durationDate = Date(timeIntervalSinceReferenceDate: recordingDuration)
 | 
						|
        label.text = timeFormatter.string(from: durationDate)
 | 
						|
    }
 | 
						|
}
 |