Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTextInputMediaRecordingButton",
module_name = "ChatTextInputMediaRecordingButton",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/LegacyComponents",
"//submodules/AccountContext",
"//submodules/ChatInterfaceState",
"//submodules/AudioBlob",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ComponentFlow",
"//submodules/Components/LottieAnimationComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,173 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AppBundle
import ObjCRuntimeUtils
private let innerCircleDiameter: CGFloat = 110.0
private let outerCircleDiameter = innerCircleDiameter + 50.0
private let outerCircleMinScale = innerCircleDiameter / outerCircleDiameter
private let innerCircleImage = generateFilledCircleImage(diameter: innerCircleDiameter, color: UIColor(rgb: 0x0088ff))
private let outerCircleImage = generateFilledCircleImage(diameter: outerCircleDiameter, color: UIColor(rgb: 0x0088ff, alpha: 0.2))
private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: .white)!
private final class ChatTextInputAudioRecordingOverlayDisplayLinkTarget: NSObject {
private let f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
super.init()
}
@objc func displayLinkEvent() {
self.f()
}
}
final class ChatTextInputAudioRecordingOverlay {
private weak var anchorView: UIView?
private let containerNode: ASDisplayNode
private let circleContainerNode: ASDisplayNode
private let innerCircleNode: ASImageNode
private let outerCircleNode: ASImageNode
private let iconNode: ASImageNode
var animationStartTime: Double?
var displayLink: CADisplayLink?
var currentLevel: CGFloat = 0.0
var inputLevel: CGFloat = 0.0
var animatedIn = false
var dismissFactor: CGFloat = 1.0 {
didSet {
let scale = max(0.3, min(self.dismissFactor, 1.0))
self.circleContainerNode.transform = CATransform3DMakeScale(scale, scale, 1.0)
}
}
init(anchorView: UIView) {
self.anchorView = anchorView
self.containerNode = ASDisplayNode()
self.containerNode.isLayerBacked = true
self.circleContainerNode = ASDisplayNode()
self.circleContainerNode.isLayerBacked = true
self.outerCircleNode = ASImageNode()
self.outerCircleNode.displayWithoutProcessing = true
self.outerCircleNode.displaysAsynchronously = false
self.outerCircleNode.isLayerBacked = true
self.outerCircleNode.image = outerCircleImage
self.outerCircleNode.frame = CGRect(origin: CGPoint(x: -outerCircleDiameter / 2.0, y: -outerCircleDiameter / 2.0), size: CGSize(width: outerCircleDiameter, height: outerCircleDiameter))
self.innerCircleNode = ASImageNode()
self.innerCircleNode.displayWithoutProcessing = true
self.innerCircleNode.displaysAsynchronously = false
self.innerCircleNode.isLayerBacked = true
self.innerCircleNode.image = innerCircleImage
self.innerCircleNode.frame = CGRect(origin: CGPoint(x: -innerCircleDiameter / 2.0, y: -innerCircleDiameter / 2.0), size: CGSize(width: innerCircleDiameter, height: innerCircleDiameter))
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.iconNode.isLayerBacked = true
self.iconNode.image = micIcon
self.iconNode.frame = CGRect(origin: CGPoint(x: -micIcon.size.width / 2.0, y: -micIcon.size.height / 2.0), size: micIcon.size)
self.circleContainerNode.addSubnode(self.outerCircleNode)
self.circleContainerNode.addSubnode(self.innerCircleNode)
self.containerNode.addSubnode(self.circleContainerNode)
self.containerNode.addSubnode(self.iconNode)
}
deinit {
self.displayLink?.invalidate()
}
func present(in window: UIWindow) {
if let anchorView = self.anchorView, let anchorSuperview = anchorView.superview {
if let displayLink = self.displayLink {
displayLink.invalidate()
}
self.displayLink = CADisplayLink(target: ChatTextInputAudioRecordingOverlayDisplayLinkTarget({ [weak self] in
self?.displayLinkEvent()
}), selector: #selector(ChatTextInputAudioRecordingOverlayDisplayLinkTarget.displayLinkEvent))
let convertedCenter = anchorSuperview.convert(anchorView.center, to: window)
self.containerNode.position = CGPoint(x: convertedCenter.x, y: convertedCenter.y)
window.addSubnode(self.containerNode)
self.innerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
self.outerCircleNode.layer.transform = CATransform3DMakeScale(outerCircleMinScale, outerCircleMinScale, 1.0)
self.outerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: outerCircleMinScale as NSNumber, keyPath: "transform.scale", duration: 0.5)
self.innerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15)
self.outerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15)
self.iconNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15)
self.animatedIn = true
self.animationStartTime = CACurrentMediaTime()
self.displayLink?.add(to: RunLoop.main, forMode: .common)
self.displayLink?.isPaused = false
}
}
func dismiss() {
self.displayLink?.invalidate()
self.displayLink = nil
var innerCompleted = false
var outerCompleted = false
var iconCompleted = false
var containerNodeRef: ASDisplayNode? = self.containerNode
let completion: () -> Void = {
if let containerNode = containerNodeRef, innerCompleted, outerCompleted, iconCompleted {
containerNode.removeFromSupernode()
containerNodeRef = nil
}
}
self.innerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
self.innerCircleNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in
innerCompleted = true
completion()
})
var currentScaleValue: CGFloat = outerCircleMinScale
if let currentScale = self.outerCircleNode.layer.floatValue(forKeyPath: "transform.scale") {
currentScaleValue = CGFloat(currentScale.floatValue)
}
self.outerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
self.outerCircleNode.layer.animateScale(from: currentScaleValue, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in
outerCompleted = true
completion()
})
self.iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { _ in
iconCompleted = true
completion()
})
}
private func displayLinkEvent() {
let t = CACurrentMediaTime()
if let animationStartTime = self.animationStartTime {
if t > animationStartTime + 0.5 {
self.currentLevel = self.currentLevel * 0.8 + self.inputLevel * 0.2
let scale = outerCircleMinScale + self.currentLevel * (1.0 - outerCircleMinScale)
self.outerCircleNode.transform = CATransform3DMakeScale(scale, scale, 1.0)
}
}
}
func addImmediateMicLevel(_ level: CGFloat) {
self.inputLevel = level
}
}
@@ -0,0 +1,666 @@
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 LegacyInstantVideoController
import GlassBackgroundComponent
import ComponentDisplayAdapters
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
}
}
}
public final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButton, TGModernConversationInputMicButtonDelegate {
private let context: AccountContext
private var theme: PresentationTheme
private let useDarkTheme: Bool
private let pause: Bool
private let strings: PresentationStrings
public var mode: ChatTextInputMediaRecordingButtonMode = .audio
public var statusBarHost: StatusBarHost?
public let presentController: (ViewController) -> Void
public var recordingDisabled: () -> Void = { }
public var beginRecording: () -> Void = { }
public var endRecording: (Bool) -> Void = { _ in }
public var stopRecording: () -> Void = { }
public var offsetRecordingControls: () -> Void = { }
public var switchMode: () -> Void = { }
public var updateLocked: (Bool) -> Void = { _ in }
public var updateCancelTranslation: () -> Void = { }
private var modeTimeoutTimer: SwiftSignalKit.Timer?
private let animationView: ComponentView<Empty>
public var animationOutput: UIImageView? {
didSet {
if let view = self.animationView.view as? LottieComponent.View {
view.output = self.animationOutput
}
}
}
private var recordingOverlay: ChatTextInputAudioRecordingOverlay?
private var startTouchLocation: CGPoint?
fileprivate var controlsOffset: CGFloat = 0.0
public private(set) var cancelTranslation: CGFloat = 0.0
private var micLevelDisposable: MetaDisposable?
private weak var currentPresenter: UIView?
public var hasShadow: Bool = false {
didSet {
self.updateShadow()
}
}
public var hidesOnLock: Bool = false {
didSet {
if self.hidesOnLock {
self.setHidesPanelOnLock()
}
}
}
private func updateShadow() {
if let view = self.animationView.view {
if self.hasShadow {
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
view.layer.shadowRadius = 2.0
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.35
} else {
view.layer.shadowRadius = 0.0
view.layer.shadowColor = UIColor.clear.cgColor
view.layer.shadowOpacity = 0.0
}
}
}
public var contentContainer: (UIView, CGRect)? {
if let _ = self.currentPresenter {
return (self.micDecoration, self.micDecoration.bounds)
} else {
return nil
}
}
public 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?.addMicLevel(CGFloat(level))
}
}))
} else if self.videoRecordingStatus == nil {
self.micLevelDisposable?.set(nil)
}
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
}
}
}
public 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?.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)
)
let theme = self.hidesOnLock ? defaultDarkColorPresentationTheme : self.theme
blobView.setColor(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, useDarkTheme: self.useDarkTheme, pause: self.pause, strings: self.strings)
lockView.addTarget(self, action: #selector(handleStopTap), for: .touchUpInside)
self.micLockValue = lockView
return lockView
}
}
public init(context: AccountContext, theme: PresentationTheme, useDarkTheme: Bool = false, pause: Bool = false, strings: PresentationStrings, presentController: @escaping (ViewController) -> Void) {
self.context = context
self.theme = theme
self.useDarkTheme = useDarkTheme
self.pause = pause
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 public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let micLevelDisposable = self.micLevelDisposable {
micLevelDisposable.dispose()
}
if let recordingOverlay = self.recordingOverlay {
recordingOverlay.dismiss()
}
}
public 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?
let theme = self.hidesOnLock ? defaultDarkColorPresentationTheme : self.theme
switch self.mode {
case .audio:
self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(theme)
image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(theme)
case .video:
self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(theme)
image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(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 animationTintColor = self.useDarkTheme ? .white : self.theme.chat.inputPanel.panelControlColor
let _ = self.animationView.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
color: animationTintColor
)),
environment: {},
containerSize: animationFrame.size
)
if let view = self.animationView.view as? LottieComponent.View {
view.isUserInteractionEnabled = false
if view.superview == nil {
self.insertSubview(view, at: 0)
view.output = self.animationOutput
self.updateShadow()
}
view.setMonochromaticEffect(tintColor: animationTintColor)
view.frame = animationFrame
if previousMode != mode {
view.playOnce()
}
}
if let animationOutput = self.animationOutput {
animationOutput.frame = animationFrame
}
}
public 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)
}
public override func createLockPanelView() -> (UIView & TGModernConversationInputMicButtonLockPanelView)! {
let isDark: Bool
let tintColor: UIColor
if self.hidesOnLock {
isDark = false
tintColor = UIColor(white: 0.0, alpha: 0.5)
} else {
isDark = self.theme.overallDarkAppearance
tintColor = self.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)
}
let view = WrapperBlurrredBackgroundView(size: CGSize(width: 40.0, height: 72.0), isDark: isDark, tintColor: tintColor)
return view
}
public func cancelRecording() {
self.isEnabled = false
self.isEnabled = true
}
public 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()
}
}
public func micButtonInteractionCancelled(_ velocity: CGPoint) {
//print("\(CFAbsoluteTimeGetCurrent()) cancelled")
self.modeTimeoutTimer?.invalidate()
self.endRecording(false)
}
public 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)
}
public func micButtonInteractionUpdate(_ offset: CGPoint) {
self.controlsOffset = offset.x
self.offsetRecordingControls()
}
public func micButtonInteractionUpdateCancelTranslation(_ translation: CGFloat) {
self.cancelTranslation = translation
self.updateCancelTranslation()
}
public func micButtonInteractionLocked() {
self.updateLocked(true)
}
public func micButtonInteractionRequestedLockedAction() {
}
public func micButtonInteractionStopped() {
self.stopRecording()
}
public func micButtonShouldLock() -> Bool {
return true
}
public func micButtonPresenter() -> TGModernConversationInputMicButtonPresentation! {
let presenter = ChatTextInputMediaRecordingButtonPresenter(statusBarHost: self.statusBarHost, presentController: self.presentController)
presenter.button = self
self.currentPresenter = presenter.view()
return presenter
}
public func micButtonDecoration() -> (UIView & TGModernConversationInputMicButtonDecoration)! {
return micDecoration
}
public func micButtonLock() -> (UIView & TGModernConversationInputMicButtonLock)! {
return micLock
}
@objc private func handleStopTap() {
micButtonInteractionStopped()
}
public func lock() {
super._commitLocked()
}
override public 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)
if let animationOutput = self.animationOutput {
transition.updateAlpha(layer: animationOutput.layer, alpha: 0.0)
transition.updateTransformScale(layer: animationOutput.layer, scale: 0.3)
}
}
}
override public 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)
if let animationOutput = self.animationOutput {
transition.updateAlpha(layer: animationOutput.layer, alpha: 1.0)
transition.updateTransformScale(layer: animationOutput.layer, scale: 1.0)
}
}
}
}
private var previousSize = CGSize()
public 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.bounds = CGRect(origin: .zero, size: iconSize)
view.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
if let animationOutput = self.animationOutput {
animationOutput.bounds = view.bounds
animationOutput.center = view.center
}
}
}
}
}
private class WrapperBlurrredBackgroundView: UIView, TGModernConversationInputMicButtonLockPanelView {
let isDark: Bool
let glassTintColor: UIColor
let view: GlassBackgroundView
init(size: CGSize, isDark: Bool, tintColor: UIColor) {
self.isDark = isDark
self.glassTintColor = tintColor
let view = GlassBackgroundView()
view.frame = CGRect(origin: CGPoint(), size: size)
view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: .init(kind: .panel, color: self.glassTintColor), transition: .immediate)
self.view = view
super.init(frame: CGRect(origin: CGPoint(), size: size))
self.addSubview(view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var frame: CGRect {
get {
return super.frame
} set {
super.frame = newValue
self.view.frame = CGRect(origin: CGPoint(), size: newValue.size)
self.view.update(size: newValue.size, cornerRadius: min(newValue.width, newValue.height) * 0.5, isDark: self.isDark, tintColor: .init(kind: .panel, color: self.glassTintColor), transition: .immediate)
}
}
func update(_ size: CGSize) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateFrame(view: self.view, frame: CGRect(origin: CGPoint(), size: size))
self.view.update(size: size, cornerRadius: min(size.width, size.height) * 0.5, isDark: self.isDark, tintColor: .init(kind: .panel, color: self.glassTintColor), transition: ComponentTransition(transition))
}
}
@@ -0,0 +1,94 @@
import UIKit
import LegacyComponents
import AppBundle
import Lottie
import TelegramPresentationData
final class LockView: UIButton, TGModernConversationInputMicButtonLock {
private let useDarkTheme: Bool
private let pause: Bool
private let idleView: AnimationView
private let lockingView: AnimationView
init(frame: CGRect, theme: PresentationTheme, useDarkTheme: Bool = false, pause: Bool = false, strings: PresentationStrings) {
self.useDarkTheme = useDarkTheme
self.pause = pause
if let url = getAppBundle().url(forResource: "LockWait", withExtension: "json"), let animation = Animation.filepath(url.path) {
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.loopMode = .autoReverse
view.backgroundColor = .clear
view.isOpaque = false
self.idleView = view
} else {
self.idleView = AnimationView()
}
if let url = getAppBundle().url(forResource: self.pause ? "LockPause" : "Lock", withExtension: "json"), let animation = Animation.filepath(url.path) {
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.backgroundColor = .clear
view.isOpaque = false
self.lockingView = view
} else {
self.lockingView = AnimationView()
}
super.init(frame: frame)
accessibilityLabel = strings.VoiceOver_Recording_StopAndPreview
addSubview(idleView)
idleView.frame = bounds
addSubview(lockingView)
lockingView.frame = bounds
updateTheme(theme)
updateLockness(0)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateLockness(_ lockness: CGFloat) {
idleView.isHidden = lockness > 0
if lockness > 0 && idleView.isAnimationPlaying {
idleView.stop()
} else if lockness == 0 && !idleView.isAnimationPlaying {
idleView.play()
}
lockingView.isHidden = !idleView.isHidden
lockingView.currentProgress = lockness
}
func updateTheme(_ theme: PresentationTheme) {
for keypath in idleView.allKeypaths(predicate: { $0.keys.last == "Color" }) {
idleView.setValueProvider(ColorValueProvider(theme.chat.inputPanel.panelControlColor.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
for keypath in lockingView.allKeypaths(predicate: { $0.keys.last == "Color" }) {
lockingView.setValueProvider(ColorValueProvider(theme.chat.inputPanel.panelControlColor.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
//
// [
// "Path.Path.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
// "Path.Path.Заливка 1": theme.chat.inputPanel.panelBackgroundColor.withAlphaComponent(1.0),
// "Rectangle.Rectangle.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
// "Rectangle.Заливка 1": theme.chat.inputPanel.panelControlAccentColor,
// "Path 4.Path 4.Обводка 1": theme.chat.inputPanel.panelControlAccentColor
// ].forEach { key, value in
// lockingView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
// }
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let superTest = super.hitTest(point, with: event)
if superTest === lockingView {
return self
}
return superTest
}
}