Swiftgram/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift
2024-01-22 17:12:48 +04:00

741 lines
35 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
import ChatPresentationInterfaceState
import ChatSendButtonRadialStatusNode
import AudioWaveformNode
import ChatInputPanelNode
import TooltipUI
import TelegramNotices
import ComponentFlow
import MediaScrubberComponent
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
}
final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
let ignoreHit: (UIView, CGPoint) -> Bool
init(ignoreHit: @escaping (UIView, CGPoint) -> Bool) {
self.ignoreHit = ignoreHit
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func maybeDismissContent(point: CGPoint) {
for subview in self.subviews.reversed() {
if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) {
return
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subview in self.subviews.reversed() {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
}
if event == nil || self.ignoreHit(self, point) {
return nil
}
return nil
}
}
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
let scrubber = ComponentView<Empty>()
var viewOnce = false
let viewOnceButton: ChatRecordingViewOnceButtonNode
let recordMoreButton: ChatRecordingViewOnceButtonNode
private let waveformNode: AudioWaveformNode
private let waveformForegroundNode: AudioWaveformNode
let waveformScrubberNode: 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.isExclusiveTouch = true
self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(theme), for: [])
self.viewOnceButton = ChatRecordingViewOnceButtonNode(icon: .viewOnce)
self.recordMoreButton = ChatRecordingViewOnceButtonNode(icon: .recordMore)
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.waveformScrubberNode = 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.viewForOverlayContent = ChatRecordingPreviewViewForOverlayContent(
ignoreHit: { [weak self] view, point in
guard let strongSelf = self else {
return false
}
if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil {
return true
}
if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY {
return true
}
return false
}
)
self.addSubnode(self.deleteButton)
self.deleteButton.addSubnode(self.binNode)
self.addSubnode(self.waveformBackgroundNode)
self.addSubnode(self.sendButton)
self.addSubnode(self.waveformScrubberNode)
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.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside])
self.recordMoreButton.addTarget(self, action: #selector(self.recordMorePressed), 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.shouldBegin = { [weak self] _ in
if let self, self.viewOnce {
return false
}
return true
}
gestureRecognizer.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButton, gesture)
}
if let viewForOverlayContent = self.viewForOverlayContent {
viewForOverlayContent.addSubnode(self.viewOnceButton)
viewForOverlayContent.addSubnode(self.recordMoreButton)
}
self.view.disablesInteractiveTransitionGestureRecognizer = true
}
private func maybePresentViewOnceTooltip() {
guard let context = self.context else {
return
}
let _ = (ApplicationSpecificNotice.getVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue).startStandalone(next: { [weak self] counter in
guard let self, let interfaceState = self.presentationInterfaceState else {
return
}
if counter >= 3 {
return
}
Queue.mainQueue().after(0.3) {
self.displayViewOnceTooltip(text: interfaceState.strings.Chat_TapToPlayVoiceMessageOnceTooltip, hasIcon: true)
}
let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager).startStandalone()
})
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
var isFirstTime = false
if self.presentationInterfaceState == nil {
isFirstTime = true
}
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, let context = self.context {
switch recordedMediaPreview {
case let .audio(audio):
self.waveformButton.isHidden = false
self.waveformBackgroundNode.isHidden = false
self.waveformForegroundNode.isHidden = false
self.waveformScrubberNode.isHidden = false
self.playButton.isHidden = false
self.durationLabel.isHidden = false
if let view = self.scrubber.view, view.superview != nil {
view.removeFromSuperview()
}
if updateWaveform {
self.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor.withAlphaComponent(0.5), gravity: .center, waveform: audio.waveform)
self.waveformForegroundNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor, gravity: .center, waveform: audio.waveform)
if self.mediaPlayer != nil {
self.mediaPlayer?.pause()
}
let mediaManager = context.sharedContext.mediaManager
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: .standalone(resource: audio.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(audio.duration)
self.durationLabel.status = mediaPlayer.status
self.waveformScrubberNode.status = mediaPlayer.status
self.statusDisposable.set((mediaPlayer.status
|> deliverOnMainQueue).startStrict(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)
}
}
}))
}
case let .video(video):
self.waveformButton.isHidden = true
self.waveformBackgroundNode.isHidden = true
self.waveformForegroundNode.isHidden = true
self.waveformScrubberNode.isHidden = true
self.playButton.isHidden = true
self.durationLabel.isHidden = true
let scrubberSize = self.scrubber.update(
transition: .immediate,
component: AnyComponent(
MediaScrubberComponent(
context: context,
style: .videoMessage,
theme: interfaceState.theme,
generationTimestamp: 0,
position: 0,
minDuration: 1.0,
maxDuration: 60.0,
isPlaying: false,
tracks: [
MediaScrubberComponent.Track(
id: 0,
content: .video(frames: video.frames, framesUpdateTimestamp: video.framesUpdateTimestamp),
duration: Double(video.duration),
trimRange: video.trimRange,
offset: nil,
isMain: true
)
],
positionUpdated: { _, _ in },
trackTrimUpdated: { _, start, end, updatedEnd, apply in
video.control.updateTrimRange(start, end, updatedEnd, apply)
},
trackOffsetUpdated: { _, _, _ in },
trackLongPressed: { _, _ in }
)
),
environment: {},
forceUpdate: false,
containerSize: CGSize(width: min(424, width - leftInset - rightInset - 45.0 * 2.0), height: 33.0)
)
if let view = self.scrubber.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.bounds = CGRect(origin: .zero, size: scrubberSize)
}
}
}
}
if let view = self.scrubber.view {
view.frame = CGRect(origin: CGPoint(x: max(leftInset + 45.0, floorToScreenPixels((width - view.bounds.width) / 2.0)), y: 7.0 - UIScreenPixel), size: view.bounds.size)
}
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 viewOnceOffset: CGFloat = 0.0
if interfaceState.interfaceState.replyMessageSubject != nil {
viewOnceOffset = -35.0
}
let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme)
let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -64.0 - 53.0 + viewOnceOffset), size: viewOnceSize)
transition.updateFrame(node: self.viewOnceButton, frame: viewOnceButtonFrame)
let recordMoreSize = self.recordMoreButton.update(theme: interfaceState.theme)
let recordMoreButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -64.0 + viewOnceOffset), size: recordMoreSize)
transition.updateFrame(node: self.recordMoreButton, frame: recordMoreButtonFrame)
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.waveformScrubberNode, 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
self.viewOnceButton.isHidden = prevTextInputPanelNode.viewOnceButton.isHidden
self.viewOnce = prevTextInputPanelNode.viewOnce
self.viewOnceButton.update(isSelected: self.viewOnce, animated: false)
prevTextInputPanelNode.viewOnceButton.isHidden = true
prevTextInputPanelNode.viewOnce = false
self.recordMoreButton.isEnabled = false
self.viewOnceButton.layer.animatePosition(from: prevTextInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
prevTextInputPanelNode.viewOnceButton.isHidden = false
prevTextInputPanelNode.viewOnceButton.update(isSelected: false, animated: false)
Queue.mainQueue().after(0.3) {
self.recordMoreButton.isEnabled = true
}
})
self.recordMoreButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.recordMoreButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
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)
if let view = self.scrubber.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: 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.waveformScrubberNode.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1)
self.waveformScrubberNode.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 prevTextInputPanelNode?.supernode === self {
prevTextInputPanelNode?.removeFromSupernode()
prevTextInputPanelNode?.finishedTransitionToPreview = true
prevTextInputPanelNode?.requestLayout()
}
}
}
if isFirstTime, !self.viewOnceButton.isHidden {
self.maybePresentViewOnceTooltip()
}
return panelHeight
}
override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool {
return prevInputPanelNode is ChatTextInputPanelNode
}
@objc func deletePressed() {
self.viewOnce = false
self.tooltipController?.dismiss()
self.mediaPlayer?.pause()
self.interfaceInteraction?.deleteRecordedMedia()
}
@objc func sendPressed() {
self.tooltipController?.dismiss()
self.interfaceInteraction?.sendRecordedMedia(false, self.viewOnce)
self.viewOnce = false
}
private weak var tooltipController: TooltipScreen?
@objc private func viewOncePressed() {
guard let context = self.context, let interfaceState = self.presentationInterfaceState else {
return
}
self.viewOnce = !self.viewOnce
self.viewOnceButton.update(isSelected: self.viewOnce, animated: true)
self.tooltipController?.dismiss()
if self.viewOnce {
self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip, hasIcon: true)
let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager, count: 3).startStandalone()
}
}
@objc private func recordMorePressed() {
self.tooltipController?.dismiss()
self.interfaceInteraction?.resumeMediaRecording()
}
private func displayViewOnceTooltip(text: String, hasIcon: Bool) {
guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else {
return
}
let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize())
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .markdown(text: text),
balancedTextLayout: true,
constrainWidth: 240.0,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small,
icon: hasIcon ? .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil) : nil,
location: .point(location, .right),
displayDuration: .default,
inset: 8.0,
cornerRadius: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
parentController.present(tooltipController, in: .current)
}
@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
}
}
}
}
final class ChatRecordingViewOnceButtonNode: HighlightTrackingButtonNode {
enum Icon {
case viewOnce
case recordMore
}
private let icon: Icon
private let backgroundNode: ASImageNode
private let iconNode: ASImageNode
private var theme: PresentationTheme?
init(icon: Icon) {
self.icon = icon
self.backgroundNode = ASImageNode()
self.backgroundNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.isUserInteractionEnabled = false
super.init(pointerStyle: .default)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.iconNode)
self.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
if highlighted {
self.layer.removeAnimation(forKey: "sublayerTransform")
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateTransformScale(node: self, scale: topScale)
} else {
let transition = ContainedViewLayoutTransition.immediate
transition.updateTransformScale(node: self, scale: 1.0)
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
}
private var innerIsSelected = false
func update(isSelected: Bool, animated: Bool = false) {
guard let theme = self.theme else {
return
}
let updated = self.iconNode.image == nil || self.innerIsSelected != isSelected
self.innerIsSelected = isSelected
if animated, updated && self.iconNode.image != nil, let snapshot = self.iconNode.view.snapshotContentTree() {
self.view.addSubview(snapshot)
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshot.removeFromSuperview()
})
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if updated {
if case .viewOnce = self.icon {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor)
}
}
}
func update(theme: PresentationTheme) -> CGSize {
let size = CGSize(width: 44.0, height: 44.0)
let innerSize = CGSize(width: 40.0, height: 40.0)
if self.theme !== theme {
self.theme = theme
self.backgroundNode.image = generateFilledCircleImage(diameter: innerSize.width, color: theme.rootController.navigationBar.opaqueBackgroundColor, strokeColor: theme.chat.inputPanel.panelSeparatorColor, strokeWidth: 0.5, backgroundColor: nil)
switch self.icon {
case .viewOnce:
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceEnabled" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor)
case .recordMore:
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: theme.chat.inputPanel.panelControlAccentColor)
}
}
if let backgroundImage = self.backgroundNode.image {
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - backgroundImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - backgroundImage.size.height / 2.0)), size: backgroundImage.size)
self.backgroundNode.frame = backgroundFrame
}
if let iconImage = self.iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - iconImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - iconImage.size.height / 2.0)), size: iconImage.size)
self.iconNode.frame = iconFrame
}
return size
}
}