Swiftgram/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift

353 lines
18 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import UniversalMediaPlayer
import AppBundle
import ContextUI
import AnimationUI
import ManagedAnimationNode
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
}
final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
let deleteButton: HighlightableButtonNode
let binNode: AnimationNode
let sendButton: HighlightTrackingButtonNode
private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
let playButton: HighlightableButtonNode
private let playPauseIconNode: PlayPauseIconNode
private let waveformButton: ASButtonNode
let waveformBackgroundNode: ASImageNode
private let waveformNode: AudioWaveformNode
private let waveformForegroundNode: AudioWaveformNode
let waveformScubberNode: MediaPlayerScrubbingNode
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var mediaPlayer: MediaPlayer?
let durationLabel: MediaPlayerTimeTextNode
private let statusDisposable = MetaDisposable()
private(set) var gestureRecognizer: ContextGesture?
init(theme: PresentationTheme) {
self.deleteButton = HighlightableButtonNode()
self.deleteButton.displaysAsynchronously = false
self.binNode = AnimationNode(
animation: "BinBlue",
colors: [
"Cap11.Cap2.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
"Bin 5.Bin.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
"Cap12.Cap1.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
"Line15.Line1.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
"Line13.Line3.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
"Line14.Line2.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
"Line13.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
]
)
self.sendButton = HighlightTrackingButtonNode()
self.sendButton.displaysAsynchronously = false
self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(theme), for: [])
self.waveformBackgroundNode = ASImageNode()
self.waveformBackgroundNode.isLayerBacked = true
self.waveformBackgroundNode.displaysAsynchronously = false
self.waveformBackgroundNode.displayWithoutProcessing = true
self.waveformBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 33.0, color: theme.chat.inputPanel.actionControlFillColor)
self.playButton = HighlightableButtonNode()
self.playButton.displaysAsynchronously = false
self.playPauseIconNode = PlayPauseIconNode()
self.playPauseIconNode.enqueueState(.play, animated: false)
self.playPauseIconNode.customColor = theme.chat.inputPanel.actionControlForegroundColor
self.waveformButton = ASButtonNode()
self.waveformButton.accessibilityTraits.insert(.startsMediaSession)
self.waveformNode = AudioWaveformNode()
self.waveformNode.isLayerBacked = true
self.waveformForegroundNode = AudioWaveformNode()
self.waveformForegroundNode.isLayerBacked = true
self.waveformScubberNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode))
self.durationLabel = MediaPlayerTimeTextNode(textColor: theme.chat.inputPanel.actionControlForegroundColor)
self.durationLabel.alignment = .right
self.durationLabel.mode = .normal
super.init()
self.addSubnode(self.deleteButton)
self.deleteButton.addSubnode(self.binNode)
self.addSubnode(self.waveformBackgroundNode)
self.addSubnode(self.sendButton)
self.addSubnode(self.waveformScubberNode)
self.addSubnode(self.playButton)
self.addSubnode(self.durationLabel)
self.addSubnode(self.waveformButton)
self.playButton.addSubnode(self.playPauseIconNode)
self.sendButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.sendButton.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false)
} else if let presentationLayer = strongSelf.sendButton.layer.presentation() {
strongSelf.sendButton.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
}
}
}
self.deleteButton.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside])
self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: [.touchUpInside])
self.waveformButton.addTarget(self, action: #selector(self.waveformPressed), forControlEvents: .touchUpInside)
}
deinit {
self.mediaPlayer?.pause()
self.statusDisposable.dispose()
}
override func didLoad() {
super.didLoad()
let gestureRecognizer = ContextGesture(target: nil, action: nil)
self.sendButton.view.addGestureRecognizer(gestureRecognizer)
self.gestureRecognizer = gestureRecognizer
gestureRecognizer.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButton, gesture)
}
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
if self.presentationInterfaceState != interfaceState {
var updateWaveform = false
if self.presentationInterfaceState?.recordedMediaPreview != interfaceState.recordedMediaPreview {
updateWaveform = true
}
if self.presentationInterfaceState?.strings !== interfaceState.strings {
self.deleteButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextDelete
self.sendButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextSend
self.waveformButton.accessibilityLabel = interfaceState.strings.VoiceOver_Chat_RecordPreviewVoiceMessage
}
self.presentationInterfaceState = interfaceState
if let recordedMediaPreview = interfaceState.recordedMediaPreview, updateWaveform {
self.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor.withAlphaComponent(0.5), gravity: .center, waveform: recordedMediaPreview.waveform)
self.waveformForegroundNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor, gravity: .center, waveform: recordedMediaPreview.waveform)
if self.mediaPlayer != nil {
self.mediaPlayer?.pause()
}
if let context = self.context {
let mediaManager = context.sharedContext.mediaManager
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, resourceReference: .standalone(resource: recordedMediaPreview.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true)
mediaPlayer.actionAtEnd = .action{ [weak mediaPlayer] in
mediaPlayer?.seek(timestamp: 0.0)
}
self.mediaPlayer = mediaPlayer
self.durationLabel.defaultDuration = Double(recordedMediaPreview.duration)
self.durationLabel.status = mediaPlayer.status
self.waveformScubberNode.status = mediaPlayer.status
self.statusDisposable.set((mediaPlayer.status
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
switch status.status {
case .playing, .buffering(_, true, _, _):
strongSelf.playPauseIconNode.enqueueState(.pause, animated: true)
default:
strongSelf.playPauseIconNode.enqueueState(.play, animated: true)
}
}
}))
}
}
}
let panelHeight = defaultHeight(metrics: metrics)
transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: 1), size: CGSize(width: 40.0, height: 40)))
transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel, y: 2 - UIScreenPixel), size: CGSize(width: 44.0, height: 44)))
self.binNode.frame = self.deleteButton.bounds
var isScheduledMessages = false
if case .scheduledMessages = interfaceState.subject {
isScheduledMessages = true
}
if let slowmodeState = interfaceState.slowmodeState, !isScheduledMessages {
let sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode
if let current = self.sendButtonRadialStatusNode {
sendButtonRadialStatusNode = current
} else {
sendButtonRadialStatusNode = ChatSendButtonRadialStatusNode(color: interfaceState.theme.chat.inputPanel.panelControlAccentColor)
sendButtonRadialStatusNode.alpha = self.sendButton.alpha
self.sendButtonRadialStatusNode = sendButtonRadialStatusNode
self.addSubnode(sendButtonRadialStatusNode)
}
transition.updateSublayerTransformScale(layer: self.sendButton.layer, scale: CGPoint(x: 0.7575, y: 0.7575))
sendButtonRadialStatusNode.frame = CGRect(origin: CGPoint(x: self.sendButton.frame.midX - 33.0 / 2.0, y: self.sendButton.frame.midY - 33.0 / 2.0), size: CGSize(width: 33.0, height: 33.0))
sendButtonRadialStatusNode.slowmodeState = slowmodeState
} else {
if let sendButtonRadialStatusNode = self.sendButtonRadialStatusNode {
self.sendButtonRadialStatusNode = nil
sendButtonRadialStatusNode.removeFromSupernode()
}
transition.updateSublayerTransformScale(layer: self.sendButton.layer, scale: CGPoint(x: 1.0, y: 1.0))
}
transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0)))
self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: -2.0, y: -1.0), size: CGSize(width: 26.0, height: 26.0))
let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 90.0, height: 33.0))
transition.updateFrame(node: self.waveformBackgroundNode, frame: waveformBackgroundFrame)
transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 90.0, height: panelHeight)))
transition.updateFrame(node: self.waveformScubberNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0 + 35.0, y: 7.0 + floor((33.0 - 13.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset - 90.0 - 45.0 - 40.0, height: 13.0)))
transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: width - rightInset - 90.0 - 4.0, y: 15.0), size: CGSize(width: 35.0, height: 20.0)))
prevInputPanelNode?.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight))
if let prevTextInputPanelNode = self.prevInputPanelNode as? ChatTextInputPanelNode {
self.prevInputPanelNode = nil
if let audioRecordingDotNode = prevTextInputPanelNode.audioRecordingDotNode {
let startAlpha = CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1.0)
audioRecordingDotNode.layer.removeAllAnimations()
audioRecordingDotNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
audioRecordingDotNode.layer.animateAlpha(from: startAlpha, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
if let audioRecordingTimeNode = prevTextInputPanelNode.audioRecordingTimeNode {
audioRecordingTimeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
audioRecordingTimeNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
let timePosition = audioRecordingTimeNode.position
audioRecordingTimeNode.layer.animatePosition(from: timePosition, to: CGPoint(x: timePosition.x - 20, y: timePosition.y), duration: 0.15, removeOnCompletion: false)
}
if let audioRecordingCancelIndicator = prevTextInputPanelNode.audioRecordingCancelIndicator {
audioRecordingCancelIndicator.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
prevTextInputPanelNode.actionButtons.micButton.animateOut(true)
self.deleteButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15)
self.deleteButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.playButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1)
self.playButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
self.durationLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.1)
self.waveformScubberNode.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1)
self.waveformScubberNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
self.waveformBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.waveformBackgroundNode.layer.animateFrame(
from: self.sendButton.frame.insetBy(dx: 5.5, dy: 5.5),
to: waveformBackgroundFrame,
duration: 0.2,
delay: 0.12,
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue,
removeOnCompletion: false
) { [weak self, weak prevTextInputPanelNode] finished in
if finished, prevTextInputPanelNode?.supernode === self {
prevTextInputPanelNode?.removeFromSupernode()
}
}
}
return panelHeight
}
override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool {
return prevInputPanelNode is ChatTextInputPanelNode
}
@objc func deletePressed() {
self.mediaPlayer?.pause()
self.interfaceInteraction?.deleteRecordedMedia()
}
@objc func sendPressed() {
self.interfaceInteraction?.sendRecordedMedia(false)
}
@objc func waveformPressed() {
self.mediaPlayer?.togglePlayPause()
}
override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
return defaultHeight(metrics: metrics)
}
func frameForInputActionButton() -> CGRect? {
return self.sendButton.frame
}
}
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .pause
init() {
super.init(size: CGSize(width: 28.0, height: 28.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}