// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import NVActivityIndicatorView import SessionUIKit import SessionMessagingKit public final class VoiceMessageView: UIView { private static let width: CGFloat = 160 private static let toggleContainerSize: CGFloat = 20 private static let inset = Values.smallSpacing // MARK: - UI private lazy var progressViewRightConstraint = progressView.pin(.right, to: .right, of: self, withInset: -VoiceMessageView.width) private lazy var progressView: UIView = { let result: UIView = UIView() result.backgroundColor = UIColor.black.withAlphaComponent(0.2) return result }() private lazy var toggleImageView: UIImageView = { let result: UIImageView = UIImageView(image: UIImage(named: "Play")) result.contentMode = .scaleAspectFit result.set(.width, to: 8) result.set(.height, to: 8) return result }() private lazy var loader: NVActivityIndicatorView = { let result: NVActivityIndicatorView = NVActivityIndicatorView( frame: .zero, type: .circleStrokeSpin, color: Colors.text, padding: nil ) result.set(.width, to: VoiceMessageView.toggleContainerSize + 2) result.set(.height, to: VoiceMessageView.toggleContainerSize + 2) return result }() private lazy var countdownLabelContainer: UIView = { let result: UIView = UIView() result.backgroundColor = .white result.layer.masksToBounds = true result.set(.height, to: VoiceMessageView.toggleContainerSize) result.set(.width, to: 44) return result }() private lazy var countdownLabel: UILabel = { let result: UILabel = UILabel() result.textColor = .black result.font = .systemFont(ofSize: Values.smallFontSize) result.text = "0:00" return result }() private lazy var speedUpLabel: UILabel = { let result: UILabel = UILabel() result.textColor = .black result.font = .systemFont(ofSize: Values.smallFontSize) result.alpha = 0 result.text = "1.5x" result.textAlignment = .center return result }() // MARK: - Lifecycle init() { super.init(frame: CGRect.zero) setUpViewHierarchy() } override init(frame: CGRect) { preconditionFailure("Use init(viewItem:) instead.") } required init?(coder: NSCoder) { preconditionFailure("Use init(viewItem:) instead.") } private func setUpViewHierarchy() { let toggleContainerSize = VoiceMessageView.toggleContainerSize let inset = VoiceMessageView.inset // Width & height set(.width, to: VoiceMessageView.width) // Toggle let toggleContainer: UIView = UIView() toggleContainer.backgroundColor = .white toggleContainer.set(.width, to: toggleContainerSize) toggleContainer.set(.height, to: toggleContainerSize) toggleContainer.addSubview(toggleImageView) toggleImageView.center(in: toggleContainer) toggleContainer.layer.cornerRadius = (toggleContainerSize / 2) toggleContainer.layer.masksToBounds = true // Line let lineView = UIView() lineView.backgroundColor = .white lineView.set(.height, to: 1) // Countdown label countdownLabelContainer.addSubview(countdownLabel) countdownLabel.center(in: countdownLabelContainer) // Speed up label countdownLabelContainer.addSubview(speedUpLabel) speedUpLabel.center(in: countdownLabelContainer) // Constraints addSubview(progressView) progressView.pin(.left, to: .left, of: self) progressView.pin(.top, to: .top, of: self) progressViewRightConstraint.isActive = true progressView.pin(.bottom, to: .bottom, of: self) addSubview(toggleContainer) toggleContainer.pin(.left, to: .left, of: self, withInset: inset) toggleContainer.pin(.top, to: .top, of: self, withInset: inset) toggleContainer.pin(.bottom, to: .bottom, of: self, withInset: -inset) addSubview(lineView) lineView.pin(.left, to: .right, of: toggleContainer) lineView.center(.vertical, in: self) addSubview(countdownLabelContainer) countdownLabelContainer.pin(.left, to: .right, of: lineView) countdownLabelContainer.pin(.right, to: .right, of: self, withInset: -inset) countdownLabelContainer.center(.vertical, in: self) addSubview(loader) loader.center(in: toggleContainer) } public override func layoutSubviews() { super.layoutSubviews() countdownLabelContainer.layer.cornerRadius = (countdownLabelContainer.bounds.height / 2) } // MARK: - Updating public func update( with attachment: Attachment, isPlaying: Bool, progress: TimeInterval, playbackRate: Double, oldPlaybackRate: Double ) { switch attachment.state { case .downloaded, .uploaded: loader.isHidden = true loader.stopAnimating() toggleImageView.image = (isPlaying ? UIImage(named: "Pause") : UIImage(named: "Play")) countdownLabel.text = OWSFormat.formatDurationSeconds(max(0, Int(floor(attachment.duration.defaulting(to: 0) - progress)))) guard let duration: TimeInterval = attachment.duration, duration > 0, progress > 0 else { return progressViewRightConstraint.constant = -VoiceMessageView.width } let fraction: Double = (progress / duration) progressViewRightConstraint.constant = -(VoiceMessageView.width * (1 - fraction)) // If the playback rate changed then show the 'speedUpLabel' briefly guard playbackRate > oldPlaybackRate else { return } UIView.animate(withDuration: 0.25) { [weak self] in self?.countdownLabel.alpha = 0 self?.speedUpLabel.alpha = 1 } DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1250)) { UIView.animate(withDuration: 0.25) { [weak self] in self?.countdownLabel.alpha = 1 self?.speedUpLabel.alpha = 0 } } default: if !loader.isAnimating { loader.startAnimating() } } } }