Swiftgram/submodules/TelegramUI/Sources/ChatTextInputMediaRecordingButton.swift
Ilya Laktyushin 071ecf8a60 Various fixes
2023-03-13 18:54:01 +04:00

541 lines
20 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import LegacyComponents
import AccountContext
import ChatInterfaceState
import AudioBlob
import ChatPresentationInterfaceState
import ComponentFlow
import LottieAnimationComponent
import LottieComponent
import AccountContext
private let offsetThreshold: CGFloat = 10.0
private let dismissOffsetThreshold: CGFloat = 70.0
private func findTargetView(_ view: UIView, point: CGPoint) -> UIView? {
if view.bounds.contains(point) && view.tag == 0x01f2bca {
return view
}
for subview in view.subviews {
let frame = subview.frame
if let result = findTargetView(subview, point: point.offsetBy(dx: -frame.minX, dy: -frame.minY)) {
return result
}
}
return nil
}
private final class ChatTextInputMediaRecordingButtonPresenterContainer: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = findTargetView(self, point: point) {
return result
}
for subview in self.subviews {
if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) {
return result
}
}
return super.hitTest(point, with: event)
}
}
private final class ChatTextInputMediaRecordingButtonPresenterController: ViewController {
private var controllerNode: ChatTextInputMediaRecordingButtonPresenterControllerNode {
return self.displayNode as! ChatTextInputMediaRecordingButtonPresenterControllerNode
}
var containerView: UIView? {
didSet {
if self.isNodeLoaded {
self.controllerNode.containerView = self.containerView
}
}
}
override func loadDisplayNode() {
self.displayNode = ChatTextInputMediaRecordingButtonPresenterControllerNode()
if let containerView = self.containerView {
self.controllerNode.containerView = containerView
}
}
}
private final class ChatTextInputMediaRecordingButtonPresenterControllerNode: ViewControllerTracingNode {
var containerView: UIView? {
didSet {
if self.containerView !== oldValue {
if self.isNodeLoaded, let containerView = oldValue, containerView.superview === self.view {
containerView.removeFromSuperview()
}
if self.isNodeLoaded, let containerView = self.containerView {
self.view.addSubview(containerView)
}
}
}
}
override func didLoad() {
super.didLoad()
if let containerView = self.containerView {
self.view.addSubview(containerView)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let containerView = self.containerView {
if let result = containerView.hitTest(point, with: event), result !== containerView {
return result
}
}
return nil
}
}
private final class ChatTextInputMediaRecordingButtonPresenter : NSObject, TGModernConversationInputMicButtonPresentation {
private let statusBarHost: StatusBarHost?
private let presentController: (ViewController) -> Void
let container: ChatTextInputMediaRecordingButtonPresenterContainer
private var presentationController: ChatTextInputMediaRecordingButtonPresenterController?
private var timer: SwiftSignalKit.Timer?
fileprivate weak var button: ChatTextInputMediaRecordingButton?
init(statusBarHost: StatusBarHost?, presentController: @escaping (ViewController) -> Void) {
self.statusBarHost = statusBarHost
self.presentController = presentController
self.container = ChatTextInputMediaRecordingButtonPresenterContainer()
}
deinit {
self.container.removeFromSuperview()
if let presentationController = self.presentationController {
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
self.presentationController = nil
}
self.timer?.invalidate()
}
func view() -> UIView! {
return self.container
}
func setUserInteractionEnabled(_ enabled: Bool) {
self.container.isUserInteractionEnabled = enabled
}
func present() {
let windowIsVisible: (UIWindow) -> Bool = { window in
return !window.frame.height.isZero
}
if let statusBarHost = self.statusBarHost, let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) {
keyboardWindow.addSubview(self.container)
self.timer = SwiftSignalKit.Timer(timeout: 0.05, repeat: true, completion: { [weak self] in
if let keyboardWindow = LegacyComponentsGlobals.provider().applicationKeyboardWindow(), windowIsVisible(keyboardWindow) {
} else {
self?.present()
}
}, queue: Queue.mainQueue())
self.timer?.start()
} else {
var presentNow = false
if self.presentationController == nil {
let presentationController = ChatTextInputMediaRecordingButtonPresenterController(navigationBarPresentationData: nil)
presentationController.statusBar.statusBarStyle = .Ignore
self.presentationController = presentationController
presentNow = true
}
self.presentationController?.containerView = self.container
if let presentationController = self.presentationController, presentNow {
self.presentController(presentationController)
}
if let timer = self.timer {
self.button?.reset()
timer.invalidate()
}
}
}
func dismiss() {
self.timer?.invalidate()
self.container.removeFromSuperview()
if let presentationController = self.presentationController {
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
self.presentationController = nil
}
}
}
final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButton, TGModernConversationInputMicButtonDelegate {
private let context: AccountContext
private var theme: PresentationTheme
private let strings: PresentationStrings
var mode: ChatTextInputMediaRecordingButtonMode = .audio
var statusBarHost: StatusBarHost?
let presentController: (ViewController) -> Void
var recordingDisabled: () -> Void = { }
var beginRecording: () -> Void = { }
var endRecording: (Bool) -> Void = { _ in }
var stopRecording: () -> Void = { }
var offsetRecordingControls: () -> Void = { }
var switchMode: () -> Void = { }
var updateLocked: (Bool) -> Void = { _ in }
var updateCancelTranslation: () -> Void = { }
private var modeTimeoutTimer: SwiftSignalKit.Timer?
private let animationView: ComponentView<Empty>
private var recordingOverlay: ChatTextInputAudioRecordingOverlay?
private var startTouchLocation: CGPoint?
fileprivate var controlsOffset: CGFloat = 0.0
private(set) var cancelTranslation: CGFloat = 0.0
private var micLevelDisposable: MetaDisposable?
private weak var currentPresenter: UIView?
var contentContainer: (UIView, CGRect)? {
if let _ = self.currentPresenter {
return (self.micDecoration, self.micDecoration.bounds)
} else {
return nil
}
}
var audioRecorder: ManagedAudioRecorder? {
didSet {
if self.audioRecorder !== oldValue {
if self.micLevelDisposable == nil {
micLevelDisposable = MetaDisposable()
}
if let audioRecorder = self.audioRecorder {
self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in
Queue.mainQueue().async {
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
self?.addMicLevel(CGFloat(level))
}
}))
} else if self.videoRecordingStatus == nil {
self.micLevelDisposable?.set(nil)
}
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
}
}
}
var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
didSet {
if self.videoRecordingStatus !== oldValue {
if self.micLevelDisposable == nil {
micLevelDisposable = MetaDisposable()
}
if let videoRecordingStatus = self.videoRecordingStatus {
self.micLevelDisposable?.set(videoRecordingStatus.micLevel.start(next: { [weak self] level in
Queue.mainQueue().async {
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
self?.addMicLevel(CGFloat(level))
}
}))
} else if self.audioRecorder == nil {
self.micLevelDisposable?.set(nil)
}
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
}
}
}
private var hasRecorder: Bool = false {
didSet {
if self.hasRecorder != oldValue {
if self.hasRecorder {
self.animateIn()
} else {
self.animateOut(false)
}
}
}
}
private var micDecorationValue: VoiceBlobView?
private var micDecoration: (UIView & TGModernConversationInputMicButtonDecoration) {
if let micDecorationValue = self.micDecorationValue {
return micDecorationValue
} else {
let blobView = VoiceBlobView(
frame: CGRect(origin: CGPoint(), size: CGSize(width: 220.0, height: 220.0)),
maxLevel: 4,
smallBlobRange: (0.45, 0.55),
mediumBlobRange: (0.52, 0.87),
bigBlobRange: (0.57, 1.00)
)
blobView.setColor(self.theme.chat.inputPanel.actionControlFillColor)
self.micDecorationValue = blobView
return blobView
}
}
private var micLockValue: (UIView & TGModernConversationInputMicButtonLock)?
private var micLock: UIView & TGModernConversationInputMicButtonLock {
if let current = self.micLockValue {
return current
} else {
let lockView = LockView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 60.0)), theme: self.theme, strings: self.strings)
lockView.addTarget(self, action: #selector(handleStopTap), for: .touchUpInside)
self.micLockValue = lockView
return lockView
}
}
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, presentController: @escaping (ViewController) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.animationView = ComponentView<Empty>()
self.presentController = presentController
super.init(frame: CGRect())
self.disablesInteractiveTransitionGestureRecognizer = true
self.pallete = legacyInputMicPalette(from: theme)
self.disablesInteractiveTransitionGestureRecognizer = true
self.updateMode(mode: self.mode, animated: false, force: true)
self.delegate = self
self.isExclusiveTouch = false;
self.centerOffset = CGPoint(x: 0.0, y: -1.0 + UIScreenPixel)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool) {
self.updateMode(mode: mode, animated: animated, force: false)
}
private func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool, force: Bool) {
let previousMode = self.mode
if mode != self.mode || force {
self.mode = mode
self.updateAnimation(previousMode: previousMode)
}
}
private func updateAnimation(previousMode: ChatTextInputMediaRecordingButtonMode) {
let image: UIImage?
switch self.mode {
case .audio:
self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme)
image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
case .video:
self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme)
image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
}
let size = self.bounds.size
let iconSize: CGSize
if let image = image {
iconSize = image.size
} else {
iconSize = size
}
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
let animationName: String
switch self.mode {
case .audio:
animationName = "anim_videoToMic"
case .video:
animationName = "anim_micToVideo"
}
let _ = animationView.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
color: self.theme.chat.inputPanel.panelControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0)
)),
environment: {},
containerSize: animationFrame.size
)
if let view = animationView.view as? LottieComponent.View {
view.isUserInteractionEnabled = false
if view.superview == nil {
self.insertSubview(view, at: 0)
}
view.frame = animationFrame
if previousMode != mode {
view.playOnce()
}
}
}
func updateTheme(theme: PresentationTheme) {
self.theme = theme
self.updateAnimation(previousMode: self.mode)
self.pallete = legacyInputMicPalette(from: theme)
self.micDecorationValue?.setColor(self.theme.chat.inputPanel.actionControlFillColor)
(self.micLockValue as? LockView)?.updateTheme(theme)
}
deinit {
if let micLevelDisposable = self.micLevelDisposable {
micLevelDisposable.dispose()
}
if let recordingOverlay = self.recordingOverlay {
recordingOverlay.dismiss()
}
}
func cancelRecording() {
self.isEnabled = false
self.isEnabled = true
}
func micButtonInteractionBegan() {
if self.fadeDisabled {
self.recordingDisabled()
} else {
//print("\(CFAbsoluteTimeGetCurrent()) began")
self.modeTimeoutTimer?.invalidate()
let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.19, repeat: false, completion: { [weak self] in
if let strongSelf = self {
strongSelf.modeTimeoutTimer = nil
strongSelf.beginRecording()
}
}, queue: Queue.mainQueue())
self.modeTimeoutTimer = modeTimeoutTimer
modeTimeoutTimer.start()
}
}
func micButtonInteractionCancelled(_ velocity: CGPoint) {
//print("\(CFAbsoluteTimeGetCurrent()) cancelled")
self.modeTimeoutTimer?.invalidate()
self.endRecording(false)
}
func micButtonInteractionCompleted(_ velocity: CGPoint) {
//print("\(CFAbsoluteTimeGetCurrent()) completed")
if let modeTimeoutTimer = self.modeTimeoutTimer {
//print("\(CFAbsoluteTimeGetCurrent()) switch")
modeTimeoutTimer.invalidate()
self.modeTimeoutTimer = nil
self.switchMode()
}
self.endRecording(true)
}
func micButtonInteractionUpdate(_ offset: CGPoint) {
self.controlsOffset = offset.x
self.offsetRecordingControls()
}
func micButtonInteractionUpdateCancelTranslation(_ translation: CGFloat) {
self.cancelTranslation = translation
self.updateCancelTranslation()
}
func micButtonInteractionLocked() {
self.updateLocked(true)
}
func micButtonInteractionRequestedLockedAction() {
}
func micButtonInteractionStopped() {
self.stopRecording()
}
func micButtonShouldLock() -> Bool {
return true
}
func micButtonPresenter() -> TGModernConversationInputMicButtonPresentation! {
let presenter = ChatTextInputMediaRecordingButtonPresenter(statusBarHost: self.statusBarHost, presentController: self.presentController)
presenter.button = self
self.currentPresenter = presenter.view()
return presenter
}
func micButtonDecoration() -> (UIView & TGModernConversationInputMicButtonDecoration)! {
return micDecoration
}
func micButtonLock() -> (UIView & TGModernConversationInputMicButtonLock)! {
return micLock
}
@objc private func handleStopTap() {
micButtonInteractionStopped()
}
override func animateIn() {
super.animateIn()
if self.context.sharedContext.energyUsageSettings.fullTranslucency {
micDecoration.isHidden = false
micDecoration.startAnimating()
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut)
if let layer = self.animationView.view?.layer {
transition.updateAlpha(layer: layer, alpha: 0.0)
transition.updateTransformScale(layer: layer, scale: 0.3)
}
}
override func animateOut(_ toSmallSize: Bool) {
super.animateOut(toSmallSize)
micDecoration.stopAnimating()
if toSmallSize {
micDecoration.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.03, delay: 0.15, removeOnCompletion: false)
} else {
micDecoration.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
let transition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut)
if let layer = self.animationView.view?.layer {
transition.updateAlpha(layer: layer, alpha: 1.0)
transition.updateTransformScale(layer: layer, scale: 1.0)
}
}
}
private var previousSize = CGSize()
func layoutItems() {
let size = self.bounds.size
if size != self.previousSize {
self.previousSize = size
if let view = self.animationView.view {
let iconSize = view.bounds.size
view.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
}
}
}
}