mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
6eeb0ec7ac
Copy tweak Added a toast when copying the sessionId or group URL (fixes to the toast UI as well) Fixed the new conversation screen styling Fixed the styling of the various attachment screens Updated the buttons on the attachment screen to behave like the input view buttons Removed the old OWSNavigationBar and OWSNavigationController (logic was buggy and not actually needed in most cases)
659 lines
22 KiB
Swift
659 lines
22 KiB
Swift
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
|
|
import UIKit
|
|
import AVFoundation
|
|
import PromiseKit
|
|
import SessionUIKit
|
|
import SignalUtilitiesKit
|
|
|
|
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().done {
|
|
Logger.debug("stopCapture completed")
|
|
}.retainUntilComplete()
|
|
}
|
|
}
|
|
|
|
// 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().catch { error in
|
|
self.showFailureUI(error: error)
|
|
}.retainUntilComplete()
|
|
}
|
|
|
|
@objc
|
|
func didTapFlashMode() {
|
|
Logger.debug("")
|
|
photoCapture.switchFlashMode().done {
|
|
self.updateFlashModeControl()
|
|
}.retainUntilComplete()
|
|
}
|
|
|
|
@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)
|
|
}
|
|
|
|
// 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()
|
|
.done { [weak self] in
|
|
self?.showCaptureUI()
|
|
}
|
|
.catch { [weak self] error in
|
|
self?.showFailureUI(error: error)
|
|
}
|
|
.retainUntilComplete()
|
|
}
|
|
|
|
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,
|
|
explanation: 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 = .ows_monospacedDigitFont(withSize: 20)
|
|
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)
|
|
}
|
|
}
|