diff --git a/Telegram/BUILD b/Telegram/BUILD
index de6d5c940d..ef5301ade1 100644
--- a/Telegram/BUILD
+++ b/Telegram/BUILD
@@ -1798,6 +1798,8 @@ plist_fragment(
We need this so that you can share photos and videos from your photo library.
NSSiriUsageDescription
You can use Siri to send messages.
+ NSSpeechRecognitionUsageDescription
+ We need this to transcribe audio messages on your request.
NSUserActivityTypes
INSendMessageIntent
diff --git a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png
index 221eb7e7ee..2e52591bc3 100644
Binary files a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png and b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png differ
diff --git a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png
index 98776bf3d9..fb138b5228 100644
Binary files a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png and b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png differ
diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift
index c91b6a73df..d91976ebc4 100644
--- a/submodules/ChatListUI/Sources/ChatListController.swift
+++ b/submodules/ChatListUI/Sources/ChatListController.swift
@@ -661,7 +661,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
],
- loop: true
+ mode: .animating(loop: true)
)
progressValue = progress
@@ -682,7 +682,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0),
],
- loop: false
+ mode: .animating(loop: false)
)
progressValue = 1.0
diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift
index 4d68c2df3d..fb3955c404 100644
--- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift
+++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift
@@ -20,7 +20,7 @@ public final class BundleIconComponent: Component {
if lhs.tintColor != rhs.tintColor {
return false
}
- return false
+ return true
}
public final class View: UIImageView {
diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift
index 1668f33593..39e1c73d84 100644
--- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift
+++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift
@@ -6,16 +6,20 @@ import HierarchyTrackingLayer
public final class LottieAnimationComponent: Component {
public struct Animation: Equatable {
+ public enum Mode: Equatable {
+ case still
+ case animating(loop: Bool)
+ case animateTransitionFromPrevious
+ }
+
public var name: String
- public var loop: Bool
- public var isAnimating: Bool
+ public var mode: Mode
public var colors: [String: UIColor]
- public init(name: String, colors: [String: UIColor], loop: Bool, isAnimating: Bool = true) {
+ public init(name: String, colors: [String: UIColor], mode: Mode) {
self.name = name
self.colors = colors
- self.loop = loop
- self.isAnimating = isAnimating
+ self.mode = mode
}
}
@@ -55,6 +59,7 @@ public final class LottieAnimationComponent: Component {
private var colorCallbacks: [LOTColorValueCallback] = []
private var animationView: LOTAnimationView?
+ private var didPlayToCompletion: Bool = false
private let hierarchyTrackingLayer: HierarchyTrackingLayer
@@ -100,12 +105,22 @@ public final class LottieAnimationComponent: Component {
}
func update(component: LottieAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
+ var updatePlayback = false
+
if self.component?.animation != component.animation {
+ if let animationView = self.animationView {
+ if case .animateTransitionFromPrevious = component.animation.mode, !animationView.isAnimationPlaying, !self.didPlayToCompletion {
+ animationView.play { _ in
+ }
+ }
+ }
+
if let animationView = self.animationView, animationView.isAnimationPlaying {
animationView.completionBlock = { [weak self] _ in
guard let strongSelf = self else {
return
}
+ strongSelf.didPlayToCompletion = true
let _ = strongSelf.update(component: component, availableSize: availableSize, transition: transition)
}
animationView.loopAnimation = false
@@ -113,14 +128,22 @@ public final class LottieAnimationComponent: Component {
self.component = component
self.animationView?.removeFromSuperview()
+ self.didPlayToCompletion = false
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) {
let view = LOTAnimationView(model: composition, in: getAppBundle())
- view.loopAnimation = component.animation.loop
+ switch component.animation.mode {
+ case .still, .animateTransitionFromPrevious:
+ view.loopAnimation = false
+ case let .animating(loop):
+ view.loopAnimation = loop
+ }
view.animationSpeed = 1.0
view.backgroundColor = .clear
view.isOpaque = false
+ //view.logHierarchyKeypaths()
+
for (key, value) in component.animation.colors {
let colorCallback = LOTColorValueCallback(color: value.cgColor)
self.colorCallbacks.append(colorCallback)
@@ -129,6 +152,8 @@ public final class LottieAnimationComponent: Component {
self.animationView = view
self.addSubview(view)
+
+ updatePlayback = true
}
}
}
@@ -146,14 +171,16 @@ public final class LottieAnimationComponent: Component {
if let animationView = self.animationView {
animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
- if component.animation.isAnimating {
- if !animationView.isAnimationPlaying {
- animationView.play { _ in
+ if updatePlayback {
+ if case .animating = component.animation.mode {
+ if !animationView.isAnimationPlaying {
+ animationView.play { _ in
+ }
+ }
+ } else {
+ if animationView.isAnimationPlaying {
+ animationView.stop()
}
- }
- } else {
- if animationView.isAnimationPlaying {
- animationView.stop()
}
}
}
diff --git a/submodules/Display/Source/SimpleLayer.swift b/submodules/Display/Source/SimpleLayer.swift
index ad942b5cd6..cc8804c1db 100644
--- a/submodules/Display/Source/SimpleLayer.swift
+++ b/submodules/Display/Source/SimpleLayer.swift
@@ -8,7 +8,15 @@ public final class NullActionClass: NSObject, CAAction {
public let nullAction = NullActionClass()
open class SimpleLayer: CALayer {
+ public var didEnterHierarchy: (() -> Void)?
+ public var didExitHierarchy: (() -> Void)?
+
override open func action(forKey event: String) -> CAAction? {
+ if event == kCAOnOrderIn {
+ self.didEnterHierarchy?()
+ } else if event == kCAOnOrderOut {
+ self.didExitHierarchy?()
+ }
return nullAction
}
@@ -26,7 +34,15 @@ open class SimpleLayer: CALayer {
}
open class SimpleShapeLayer: CAShapeLayer {
+ public var didEnterHierarchy: (() -> Void)?
+ public var didExitHierarchy: (() -> Void)?
+
override open func action(forKey event: String) -> CAAction? {
+ if event == kCAOnOrderIn {
+ self.didEnterHierarchy?()
+ } else if event == kCAOnOrderOut {
+ self.didExitHierarchy?()
+ }
return nullAction
}
diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift
index 5680cd0609..6c3d94b45d 100644
--- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift
+++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift
@@ -191,7 +191,9 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
case .left:
leftInset += 62.0
case .right:
- leftInset += 0.0
+ if item.icon == nil {
+ leftInset += 16.0
+ }
}
let iconInset: CGFloat = 62.0
diff --git a/submodules/Media/ConvertOpusToAAC/BUILD b/submodules/Media/ConvertOpusToAAC/BUILD
new file mode 100644
index 0000000000..6719023723
--- /dev/null
+++ b/submodules/Media/ConvertOpusToAAC/BUILD
@@ -0,0 +1,20 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "ConvertOpusToAAC",
+ module_name = "ConvertOpusToAAC",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
+ "//submodules/FFMpegBinding:FFMpegBinding",
+ "//submodules/MediaPlayer:UniversalMediaPlayer",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift b/submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift
new file mode 100644
index 0000000000..89166bd242
--- /dev/null
+++ b/submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift
@@ -0,0 +1,69 @@
+import Foundation
+import UniversalMediaPlayer
+import AVFoundation
+import SwiftSignalKit
+
+public func convertOpusToAAC(sourcePath: String, allocateTempFile: @escaping () -> String) -> Signal {
+ return Signal { subscriber in
+ var isCancelled = false
+ let queue = Queue()
+
+ queue.async {
+ do {
+ let audioSource = SoftwareAudioSource(path: sourcePath)
+
+ let outputPath = allocateTempFile()
+
+ let assetWriter = try AVAssetWriter(outputURL: URL(fileURLWithPath: outputPath), fileType: .m4a)
+
+ var channelLayout = AudioChannelLayout()
+ memset(&channelLayout, 0, MemoryLayout.size)
+ channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono
+
+ let outputSettings: [String: Any] = [
+ AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+ AVSampleRateKey: 48000,
+ AVEncoderBitRateKey: 96000,
+ AVNumberOfChannelsKey: 1,
+ AVChannelLayoutKey: NSData(bytes: &channelLayout, length: MemoryLayout.size)
+ ]
+
+ let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: outputSettings)
+ assetWriter.add(audioInput)
+
+ assetWriter.startWriting()
+ assetWriter.startSession(atSourceTime: .zero)
+
+ let finishWriting: () -> Void = {
+ assetWriter.finishWriting(completionHandler: {
+ subscriber.putNext(outputPath)
+ subscriber.putCompletion()
+ })
+ }
+
+ audioInput.requestMediaDataWhenReady(on: queue.queue, using: {
+ if audioInput.isReadyForMoreMediaData {
+ if !isCancelled, let sampleBuffer = audioSource.readSampleBuffer() {
+ if !audioInput.append(sampleBuffer) {
+ audioInput.markAsFinished()
+ finishWriting()
+ return
+ }
+ } else {
+ audioInput.markAsFinished()
+ finishWriting()
+ }
+ }
+ })
+ } catch let e {
+ print("Error: \(e)")
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+ }
+ }
+
+ return ActionDisposable {
+ isCancelled = true
+ }
+ }
+}
diff --git a/submodules/Media/LocalAudioTranscription/BUILD b/submodules/Media/LocalAudioTranscription/BUILD
new file mode 100644
index 0000000000..ba35a1d26e
--- /dev/null
+++ b/submodules/Media/LocalAudioTranscription/BUILD
@@ -0,0 +1,18 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "LocalAudioTranscription",
+ module_name = "LocalAudioTranscription",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift b/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift
new file mode 100644
index 0000000000..e48e3b46c1
--- /dev/null
+++ b/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift
@@ -0,0 +1,73 @@
+import Foundation
+import SwiftSignalKit
+import Speech
+
+private var sharedRecognizer: Any?
+
+public func transcribeAudio(path: String) -> Signal {
+ return Signal { subscriber in
+ let disposable = MetaDisposable()
+
+ if #available(iOS 13.0, *) {
+ SFSpeechRecognizer.requestAuthorization { (status) in
+ switch status {
+ case .notDetermined:
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+ case .restricted:
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+ case .denied:
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+ case .authorized:
+ let speechRecognizer: SFSpeechRecognizer
+ if let sharedRecognizer = sharedRecognizer as? SFSpeechRecognizer {
+ speechRecognizer = sharedRecognizer
+ } else {
+ guard let speechRecognizerValue = SFSpeechRecognizer(locale: Locale(identifier: "ru-RU")), speechRecognizerValue.isAvailable else {
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+
+ return
+ }
+ speechRecognizerValue.defaultTaskHint = .unspecified
+ sharedRecognizer = speechRecognizerValue
+ speechRecognizer = speechRecognizerValue
+
+ speechRecognizer.supportsOnDeviceRecognition = false
+ }
+
+ let request = SFSpeechURLRecognitionRequest(url: URL(fileURLWithPath: path))
+ request.requiresOnDeviceRecognition = speechRecognizer.supportsOnDeviceRecognition
+ request.shouldReportPartialResults = false
+
+ let task = speechRecognizer.recognitionTask(with: request, resultHandler: { result, error in
+ if let result = result {
+ subscriber.putNext(result.bestTranscription.formattedString)
+ subscriber.putCompletion()
+ } else {
+ print("transcribeAudio: \(String(describing: error))")
+
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+ }
+ })
+
+ disposable.set(ActionDisposable {
+ task.cancel()
+ })
+ @unknown default:
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+ }
+ }
+ } else {
+ subscriber.putNext(nil)
+ subscriber.putCompletion()
+ }
+
+ return disposable
+ }
+ |> runOn(.mainQueue())
+}
diff --git a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift
index c3d0d2b570..260e5e54f3 100644
--- a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift
+++ b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift
@@ -9,6 +9,8 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
private let audioFrame: FFMpegAVFrame
private var resetDecoderOnNextFrame = true
+ private let formatDescription: CMAudioFormatDescription
+
private var delayedFrames: [MediaTrackFrame] = []
init(codecContext: FFMpegAVCodecContext, sampleRate: Int = 44100, channelCount: Int = 2) {
@@ -16,6 +18,27 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
self.audioFrame = FFMpegAVFrame()
self.swrContext = FFMpegSWResample(sourceChannelCount: Int(codecContext.channels()), sourceSampleRate: Int(codecContext.sampleRate()), sourceSampleFormat: codecContext.sampleFormat(), destinationChannelCount: channelCount, destinationSampleRate: sampleRate, destinationSampleFormat: FFMPEG_AV_SAMPLE_FMT_S16)
+
+ var outputDescription = AudioStreamBasicDescription(
+ mSampleRate: Float64(sampleRate),
+ mFormatID: kAudioFormatLinearPCM,
+ mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked,
+ mBytesPerPacket: UInt32(2 * channelCount),
+ mFramesPerPacket: 1,
+ mBytesPerFrame: UInt32(2 * channelCount),
+ mChannelsPerFrame: UInt32(channelCount),
+ mBitsPerChannel: 16,
+ mReserved: 0
+ )
+
+ var channelLayout = AudioChannelLayout()
+ memset(&channelLayout, 0, MemoryLayout.size)
+ channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono
+
+ var formatDescription: CMAudioFormatDescription?
+ CMAudioFormatDescriptionCreate(allocator: nil, asbd: &outputDescription, layoutSize: MemoryLayout.size, layout: &channelLayout, magicCookieSize: 0, magicCookie: nil, extensions: nil, formatDescriptionOut: &formatDescription)
+
+ self.formatDescription = formatDescription!
}
func decodeRaw(frame: MediaTrackDecodableFrame) -> Data? {
@@ -112,13 +135,18 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
return nil
}
- var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: pts, decodeTimeStamp: pts)
+ //var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: pts, decodeTimeStamp: pts)
var sampleBuffer: CMSampleBuffer?
- var sampleSize = data.count
- guard CMSampleBufferCreate(allocator: nil, dataBuffer: blockBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: nil, sampleCount: 1, sampleTimingEntryCount: 1, sampleTimingArray: &timingInfo, sampleSizeEntryCount: 1, sampleSizeArray: &sampleSize, sampleBufferOut: &sampleBuffer) == noErr else {
+ //var sampleSize = data.count
+
+ guard CMAudioSampleBufferCreateReadyWithPacketDescriptions(allocator: nil, dataBuffer: blockBuffer!, formatDescription: self.formatDescription, sampleCount: Int(data.count / 2), presentationTimeStamp: pts, packetDescriptions: nil, sampleBufferOut: &sampleBuffer) == noErr else {
return nil
}
+ /*guard CMSampleBufferCreate(allocator: nil, dataBuffer: blockBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: self.formatDescription, sampleCount: Int(frame.duration), sampleTimingEntryCount: 1, sampleTimingArray: &timingInfo, sampleSizeEntryCount: 1, sampleSizeArray: &sampleSize, sampleBufferOut: &sampleBuffer) == noErr else {
+ return nil
+ }*/
+
let resetDecoder = self.resetDecoderOnNextFrame
self.resetDecoderOnNextFrame = false
diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift
index 990282fe74..4b6e0de4a2 100644
--- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift
+++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift
@@ -710,6 +710,10 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
}
}
+ public func update(size: CGSize, animator: ControlledTransitionAnimator) {
+ self.updateProgressAnimations(animator: animator)
+ }
+
public func updateColors(backgroundColor: UIColor, foregroundColor: UIColor) {
switch self.contentNodes {
case let .standard(node):
@@ -736,8 +740,8 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
}
}
- private func updateProgressAnimations() {
- self.updateProgress()
+ private func updateProgressAnimations(animator: ControlledTransitionAnimator? = nil) {
+ self.updateProgress(animator: animator)
let needsAnimation: Bool
@@ -794,7 +798,7 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
})
}
- private func updateProgress() {
+ private func updateProgress(animator: ControlledTransitionAnimator? = nil) {
let bounds = self.bounds
var isPlaying = false
@@ -832,10 +836,11 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
node.containerNode.frame = CGRect(origin: CGPoint(), size: bounds.size)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - node.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: node.lineHeight))
+ let foregroundContentFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height))
+
node.backgroundNode.position = backgroundFrame.center
node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size)
- let foregroundContentFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height))
node.foregroundContentNode.position = foregroundContentFrame.center
node.foregroundContentNode.bounds = CGRect(origin: CGPoint(), size: foregroundContentFrame.size)
@@ -963,8 +968,14 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: bounds.size.width, height: bounds.size.height))
- node.backgroundNode.frame = backgroundFrame
- node.foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height))
+
+ if let animator = animator {
+ animator.updateFrame(layer: node.backgroundNode.layer, frame: backgroundFrame, completion: nil)
+ animator.updateFrame(layer: node.foregroundContentNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)), completion: nil)
+ } else {
+ node.backgroundNode.frame = backgroundFrame
+ node.foregroundContentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height))
+ }
let timestampAndDuration: (timestamp: Double, duration: Double)?
if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) {
diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift
index 754f61f689..3a1233245a 100644
--- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift
+++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift
@@ -446,6 +446,19 @@ public final class SoftwareAudioSource {
}
}
+ public func readSampleBuffer() -> CMSampleBuffer? {
+ guard let audioStream = self.audioStream, let _ = self.avFormatContext else {
+ return nil
+ }
+
+ let (decodableFrame, _) = self.readDecodableFrame()
+ if let decodableFrame = decodableFrame {
+ return audioStream.decoder.decode(frame: decodableFrame)?.sampleBuffer
+ } else {
+ return nil
+ }
+ }
+
public func readEncodedFrame() -> (Data, Int)? {
guard let _ = self.audioStream, let _ = self.avFormatContext else {
return nil
diff --git a/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m b/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m
index 54eef47d40..417818f43c 100644
--- a/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m
+++ b/submodules/MtProtoKit/Sources/MTBindKeyMessageService.m
@@ -147,10 +147,11 @@
id parsedMessage = [MTInternalMessageParser parseMessage:rpcResultMessage.data];
if ([parsedMessage isKindOfClass:[MTRpcError class]]) {
+ MTRpcError *rpcError = (MTRpcError *)parsedMessage;
if (MTLogEnabled()) {
- MTRpcError *rpcError = (MTRpcError *)parsedMessage;
MTLog(@"[MTRequestMessageService#%p response for %" PRId64 " is error: %d: %@]", self, _currentMessageId, (int)rpcError.errorCode, rpcError.errorDescription);
}
+ MTShortLog(@"[MTRequestMessageService#%p response for %" PRId64 " is error: %d: %@]", self, _currentMessageId, (int)rpcError.errorCode, rpcError.errorDescription);
}
//boolTrue#997275b5 = Bool;
diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift
index c4c35430ef..f2b4f99da9 100644
--- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift
+++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift
@@ -178,6 +178,8 @@ final class ShimmerEffectForegroundNode: ASDisplayNode {
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
+ private var globalTimeOffset = true
+ private var duration: Double?
override init() {
self.imageNodeContainer = ASDisplayNode()
@@ -212,17 +214,19 @@ final class ShimmerEffectForegroundNode: ASDisplayNode {
self.updateAnimation()
}
- func update(backgroundColor: UIColor, foregroundColor: UIColor, horizontal: Bool = false) {
+ func update(backgroundColor: UIColor, foregroundColor: UIColor, horizontal: Bool, effectSize: CGFloat?, globalTimeOffset: Bool, duration: Double?) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), self.currentHorizontal == horizontal {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.currentHorizontal = horizontal
+ self.globalTimeOffset = globalTimeOffset
+ self.duration = duration
let image: UIImage?
if horizontal {
- image = generateImage(CGSize(width: 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
+ image = generateImage(CGSize(width: effectSize ?? 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
@@ -304,18 +308,22 @@ final class ShimmerEffectForegroundNode: ASDisplayNode {
}
if horizontal {
- let gradientHeight: CGFloat = 320.0
+ let gradientHeight: CGFloat = self.imageNode.image?.size.width ?? 320.0
self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height))
- let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
+ let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: duration ?? 1.3, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
- animation.beginTime = 1.0
+ if self.globalTimeOffset {
+ animation.beginTime = 1.0
+ }
self.imageNode.layer.add(animation, forKey: "shimmer")
} else {
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
- let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
+ let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: duration ?? 1.3, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
- animation.beginTime = 1.0
+ if self.globalTimeOffset {
+ animation.beginTime = 1.0
+ }
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
@@ -339,6 +347,7 @@ public final class ShimmerEffectNode: ASDisplayNode {
private var currentForegroundColor: UIColor?
private var currentShimmeringColor: UIColor?
private var currentHorizontal: Bool?
+ private var currentEffectSize: CGFloat?
private var currentSize = CGSize()
override public init() {
@@ -361,8 +370,8 @@ public final class ShimmerEffectNode: ASDisplayNode {
self.effectNode.updateAbsoluteRect(rect, within: containerSize)
}
- public func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], horizontal: Bool = false, size: CGSize) {
- if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), horizontal == self.currentHorizontal, self.currentSize == size {
+ public func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], horizontal: Bool = false, effectSize: CGFloat? = nil, globalTimeOffset: Bool = true, duration: Double? = nil, size: CGSize) {
+ if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), horizontal == self.currentHorizontal, effectSize == self.currentEffectSize, self.currentSize == size {
return
}
@@ -375,7 +384,7 @@ public final class ShimmerEffectNode: ASDisplayNode {
self.backgroundNode.backgroundColor = foregroundColor
- self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor, horizontal: horizontal)
+ self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor, horizontal: horizontal, effectSize: effectSize, globalTimeOffset: globalTimeOffset, duration: duration)
self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
diff --git a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift
index a5b37eb8c3..ec091e971a 100644
--- a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift
+++ b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift
@@ -81,7 +81,7 @@ public class StickerShimmerEffectNode: ASDisplayNode {
self.backgroundNode.backgroundColor = foregroundColor
- self.effectNode.update(backgroundColor: backgroundColor == nil ? .clear : foregroundColor, foregroundColor: shimmeringColor, horizontal: true)
+ self.effectNode.update(backgroundColor: backgroundColor == nil ? .clear : foregroundColor, foregroundColor: shimmeringColor, horizontal: true, effectSize: nil, globalTimeOffset: true, duration: nil)
let bounds = CGRect(origin: CGPoint(), size: size)
let image = generateImage(size, rotatedContext: { size, context in
diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift
index b0c40c955d..bd73637b3e 100644
--- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift
+++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift
@@ -808,8 +808,7 @@ public final class MediaStreamComponent: CombinedComponent {
"Point 3.Group 1.Fill 1": whiteColor,
"Point 1.Group 1.Fill 1": whiteColor
],
- loop: false,
- isAnimating: false
+ mode: .still
),
size: CGSize(width: 22.0, height: 22.0)
).tagged(moreAnimationTag))),
diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
index ea07ece437..e8b2b58f6a 100644
--- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
+++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
@@ -40,8 +40,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .phoneCall(callId: callId, discardReason: discardReason, duration: duration, isVideo: isVideo))
case .messageActionEmpty:
return nil
- case let .messageActionPaymentSent(_, currency, totalAmount, _):
- return TelegramMediaAction(action: .paymentSent(currency: currency, totalAmount: totalAmount))
+ case let .messageActionPaymentSent(_, currency, totalAmount, invoiceSlug):
+ return TelegramMediaAction(action: .paymentSent(currency: currency, totalAmount: totalAmount, invoiceSlug: invoiceSlug))
case .messageActionPaymentSentMe:
return nil
case .messageActionScreenshotTaken:
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift
index 9ca8e87253..8038226c87 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift
@@ -39,7 +39,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case messageAutoremoveTimeoutUpdated(Int32)
case gameScore(gameId: Int64, score: Int32)
case phoneCall(callId: Int64, discardReason: PhoneCallDiscardReason?, duration: Int32?, isVideo: Bool)
- case paymentSent(currency: String, totalAmount: Int64)
+ case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?)
case customText(text: String, entities: [MessageTextEntity])
case botDomainAccessGranted(domain: String)
case botSentSecureValues(types: [SentSecureValueType])
@@ -88,7 +88,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
}
self = .phoneCall(callId: decoder.decodeInt64ForKey("i", orElse: 0), discardReason: discardReason, duration: decoder.decodeInt32ForKey("d", orElse: 0), isVideo: decoder.decodeInt32ForKey("vc", orElse: 0) != 0)
case 15:
- self = .paymentSent(currency: decoder.decodeStringForKey("currency", orElse: ""), totalAmount: decoder.decodeInt64ForKey("ta", orElse: 0))
+ self = .paymentSent(currency: decoder.decodeStringForKey("currency", orElse: ""), totalAmount: decoder.decodeInt64ForKey("ta", orElse: 0), invoiceSlug: decoder.decodeOptionalStringForKey("invoiceSlug"))
case 16:
self = .customText(text: decoder.decodeStringForKey("text", orElse: ""), entities: decoder.decodeObjectArrayWithDecoderForKey("ent"))
case 17:
@@ -172,10 +172,15 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encodeInt32(13, forKey: "_rawValue")
encoder.encodeInt64(gameId, forKey: "i")
encoder.encodeInt32(score, forKey: "s")
- case let .paymentSent(currency, totalAmount):
+ case let .paymentSent(currency, totalAmount, invoiceSlug):
encoder.encodeInt32(15, forKey: "_rawValue")
encoder.encodeString(currency, forKey: "currency")
encoder.encodeInt64(totalAmount, forKey: "ta")
+ if let invoiceSlug = invoiceSlug {
+ encoder.encodeString(invoiceSlug, forKey: "invoiceSlug")
+ } else {
+ encoder.encodeNil(forKey: "invoiceSlug")
+ }
case let .phoneCall(callId, discardReason, duration, isVideo):
encoder.encodeInt32(14, forKey: "_rawValue")
encoder.encodeInt64(callId, forKey: "i")
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift
index e10e5bec12..fa04f9b11e 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift
@@ -97,7 +97,11 @@ private class AdMessagesHistoryContextImpl {
self.opaqueId = try container.decode(Data.self, forKey: .opaqueId)
- self.messageType = (try container.decodeIfPresent(MessageType.self, forKey: .messageType)) ?? .sponsored
+ if let messageType = try container.decodeIfPresent(Int32.self, forKey: .messageType) {
+ self.messageType = MessageType(rawValue: messageType) ?? .sponsored
+ } else {
+ self.messageType = .sponsored
+ }
self.text = try container.decode(String.self, forKey: .text)
self.textEntities = try container.decode([MessageTextEntity].self, forKey: .textEntities)
@@ -116,7 +120,7 @@ private class AdMessagesHistoryContextImpl {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.opaqueId, forKey: .opaqueId)
- try container.encode(self.messageType, forKey: .messageType)
+ try container.encode(self.messageType.rawValue, forKey: .messageType)
try container.encode(self.text, forKey: .text)
try container.encode(self.textEntities, forKey: .textEntities)
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
index c48f8f36a1..82c63a1b8e 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
@@ -322,6 +322,10 @@ public extension TelegramEngine {
return _internal_translate(network: self.account.network, text: text, fromLang: fromLang, toLang: toLang)
}
+ public func transcribeAudio(messageId: MessageId) -> Signal {
+ return _internal_transcribeAudio(postbox: self.account.postbox, network: self.account.network, messageId: messageId)
+ }
+
public func requestWebView(peerId: PeerId, botId: PeerId, url: String?, payload: String?, themeParams: [String: Any]?, fromMenu: Bool, replyToMessageId: MessageId?) -> Signal {
return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId)
}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift
index c5f0b8dfac..b484e669fe 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift
@@ -28,3 +28,28 @@ func _internal_translate(network: Network, text: String, fromLang: String?, toLa
}
}
}
+
+func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal {
+ return postbox.transaction { transaction -> Api.InputPeer? in
+ return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
+ }
+ |> mapToSignal { inputPeer -> Signal in
+ guard let inputPeer = inputPeer else {
+ return .single(nil)
+ }
+ return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
+ |> map(Optional.init)
+ |> `catch` { _ -> Signal in
+ return .single(nil)
+ }
+ |> mapToSignal { result -> Signal in
+ guard let result = result else {
+ return .single(nil)
+ }
+ switch result {
+ case let .transcribedAudio(string):
+ return .single(string)
+ }
+ }
+ }
+}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift
index 9dc91627d3..16b181d959 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift
@@ -470,17 +470,23 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa
for media in message.media {
if let action = media as? TelegramMediaAction {
if case .paymentSent = action.action {
- for attribute in message.attributes {
- if let reply = attribute as? ReplyMessageAttribute {
- switch source {
- case let .message(messageId):
+ switch source {
+ case let .slug(slug):
+ for media in message.media {
+ if let action = media as? TelegramMediaAction, case let .paymentSent(_, _, invoiceSlug?) = action.action, invoiceSlug == slug {
+ if case let .Id(id) = message.id {
+ receiptMessageId = id
+ }
+ }
+ }
+ case let .message(messageId):
+ for attribute in message.attributes {
+ if let reply = attribute as? ReplyMessageAttribute {
if reply.messageId == messageId {
if case let .Id(id) = message.id {
receiptMessageId = id
}
}
- case .slug:
- break
}
}
}
diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift
index 3a85d5a109..1603d1fc33 100644
--- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift
+++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift
@@ -437,7 +437,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
var argumentAttributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])
argumentAttributes[1] = MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [:])
attributedString = addAttributesToStringWithRanges(formatWithArgumentRanges(baseString, ranges, [authorName, gameTitle ?? ""]), body: bodyAttributes, argumentAttributes: argumentAttributes)
- case let .paymentSent(currency, totalAmount):
+ case let .paymentSent(currency, totalAmount, _):
var invoiceMessage: EngineMessage?
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] {
diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD
index 064119386a..10eb67012b 100644
--- a/submodules/TelegramUI/BUILD
+++ b/submodules/TelegramUI/BUILD
@@ -271,6 +271,10 @@ swift_library(
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
+ "//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent:AudioTranscriptionButtonComponent",
+ "//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent",
+ "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
+ "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
diff --git a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/BUILD b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/BUILD
new file mode 100644
index 0000000000..142864139d
--- /dev/null
+++ b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/BUILD
@@ -0,0 +1,22 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "AudioTranscriptionButtonComponent",
+ module_name = "AudioTranscriptionButtonComponent",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/ComponentFlow:ComponentFlow",
+ "//submodules/AppBundle:AppBundle",
+ "//submodules/Display:Display",
+ "//submodules/TelegramPresentationData:TelegramPresentationData",
+ "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift
new file mode 100644
index 0000000000..bf66270433
--- /dev/null
+++ b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift
@@ -0,0 +1,188 @@
+import Foundation
+import UIKit
+import ComponentFlow
+import AppBundle
+import Display
+import TelegramPresentationData
+import LottieAnimationComponent
+
+public final class AudioTranscriptionButtonComponent: Component {
+ public enum TranscriptionState {
+ case possible
+ case inProgress
+ case expanded
+ case collapsed
+ }
+
+ public let theme: PresentationThemePartedColors
+ public let transcriptionState: TranscriptionState
+ public let pressed: () -> Void
+
+ public init(
+ theme: PresentationThemePartedColors,
+ transcriptionState: TranscriptionState,
+ pressed: @escaping () -> Void
+ ) {
+ self.theme = theme
+ self.transcriptionState = transcriptionState
+ self.pressed = pressed
+ }
+
+ public static func ==(lhs: AudioTranscriptionButtonComponent, rhs: AudioTranscriptionButtonComponent) -> Bool {
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ if lhs.transcriptionState != rhs.transcriptionState {
+ return false
+ }
+ return true
+ }
+
+ public final class View: UIButton {
+ private var component: AudioTranscriptionButtonComponent?
+
+ private let backgroundLayer: SimpleLayer
+ private var inProgressLayer: SimpleShapeLayer?
+ private let animationView: ComponentHostView
+
+ override init(frame: CGRect) {
+ self.backgroundLayer = SimpleLayer()
+ self.animationView = ComponentHostView()
+ self.animationView.isUserInteractionEnabled = false
+
+ super.init(frame: frame)
+
+ self.backgroundLayer.masksToBounds = true
+ self.backgroundLayer.cornerRadius = 10.0
+ self.layer.addSublayer(self.backgroundLayer)
+
+ self.addSubview(self.animationView)
+
+ self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
+ }
+
+ required public init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc private func pressed() {
+ self.component?.pressed()
+ }
+
+ func update(component: AudioTranscriptionButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize {
+ let size = CGSize(width: 30.0, height: 30.0)
+
+ let foregroundColor = component.theme.bubble.withWallpaper.reactionActiveBackground
+
+ if self.component?.transcriptionState != component.transcriptionState {
+ switch component.transcriptionState {
+ case .inProgress:
+ if self.inProgressLayer == nil {
+ let inProgressLayer = SimpleShapeLayer()
+ inProgressLayer.isOpaque = false
+ inProgressLayer.backgroundColor = nil
+ inProgressLayer.fillColor = nil
+ inProgressLayer.lineCap = .round
+ inProgressLayer.lineWidth = 1.0
+
+ let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: CGSize(width: 30.0, height: 30.0)), cornerRadius: 9.0).cgPath
+ inProgressLayer.path = path
+
+ self.inProgressLayer = inProgressLayer
+
+ inProgressLayer.didEnterHierarchy = { [weak inProgressLayer] in
+ guard let inProgressLayer = inProgressLayer else {
+ return
+ }
+ let endAnimation = CABasicAnimation(keyPath: "strokeEnd")
+ endAnimation.fromValue = CGFloat(0.0) as NSNumber
+ endAnimation.toValue = CGFloat(1.0) as NSNumber
+ endAnimation.duration = 1.25
+ endAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
+ endAnimation.fillMode = .forwards
+ endAnimation.repeatCount = .infinity
+ inProgressLayer.add(endAnimation, forKey: "strokeEnd")
+
+ let startAnimation = CABasicAnimation(keyPath: "strokeStart")
+ startAnimation.fromValue = CGFloat(0.0) as NSNumber
+ startAnimation.toValue = CGFloat(1.0) as NSNumber
+ startAnimation.duration = 1.25
+ startAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
+ startAnimation.fillMode = .forwards
+ startAnimation.repeatCount = .infinity
+ inProgressLayer.add(startAnimation, forKey: "strokeStart")
+ }
+
+ self.layer.addSublayer(inProgressLayer)
+ }
+ default:
+ if let inProgressLayer = self.inProgressLayer {
+ self.inProgressLayer = nil
+ if case .none = transition.animation {
+ inProgressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak inProgressLayer] _ in
+ inProgressLayer?.removeFromSuperlayer()
+ })
+ } else {
+ inProgressLayer.removeFromSuperlayer()
+ }
+ }
+ }
+
+ let animationName: String
+ switch component.transcriptionState {
+ case .possible:
+ animationName = "voiceToText"
+ case .inProgress:
+ animationName = "voiceToText"
+ case .collapsed:
+ animationName = "voiceToText"
+ case .expanded:
+ animationName = "textToVoice"
+ }
+ let animationSize = self.animationView.update(
+ transition: transition,
+ component: AnyComponent(LottieAnimationComponent(
+ animation: LottieAnimationComponent.Animation(
+ name: animationName,
+ colors: [
+ "icon.Group 3.Stroke 1": foregroundColor,
+ "icon.Group 1.Stroke 1": foregroundColor,
+ "icon.Group 4.Stroke 1": foregroundColor,
+ "icon.Group 2.Stroke 1": foregroundColor,
+ "Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor,
+ "Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor,
+ "Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor,
+ "Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor,
+ ],
+ mode: .animateTransitionFromPrevious
+ ),
+ size: CGSize(width: 30.0, height: 30.0)
+ )),
+ environment: {},
+ containerSize: CGSize(width: 30.0, height: 30.0)
+ )
+ self.animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.width - animationSize.height) / 2.0)), size: animationSize)
+ }
+
+ self.backgroundLayer.backgroundColor = component.theme.bubble.withWallpaper.reactionInactiveBackground.cgColor
+ self.inProgressLayer?.strokeColor = foregroundColor.cgColor
+
+ self.component = component
+
+ self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
+ if let inProgressLayer = self.inProgressLayer {
+ inProgressLayer.frame = CGRect(origin: CGPoint(), size: size)
+ }
+
+ return CGSize(width: min(availableSize.width, size.width), height: min(availableSize.height, size.height))
+ }
+ }
+
+ public func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, transition: transition)
+ }
+}
diff --git a/submodules/TelegramUI/Components/AudioWaveformComponent/BUILD b/submodules/TelegramUI/Components/AudioWaveformComponent/BUILD
new file mode 100644
index 0000000000..4b3666fef4
--- /dev/null
+++ b/submodules/TelegramUI/Components/AudioWaveformComponent/BUILD
@@ -0,0 +1,20 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "AudioWaveformComponent",
+ module_name = "AudioWaveformComponent",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/ComponentFlow:ComponentFlow",
+ "//submodules/AppBundle:AppBundle",
+ "//submodules/Display:Display",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift
new file mode 100644
index 0000000000..78229550d4
--- /dev/null
+++ b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift
@@ -0,0 +1,63 @@
+import Foundation
+import UIKit
+import ComponentFlow
+import Display
+
+public final class AudioWaveformComponent: Component {
+ public let backgroundColor: UIColor
+ public let foregroundColor: UIColor
+ public let samples: Data
+ public let peak: Int32
+
+ public init(
+ backgroundColor: UIColor,
+ foregroundColor: UIColor,
+ samples: Data,
+ peak: Int32
+ ) {
+ self.backgroundColor = backgroundColor
+ self.foregroundColor = foregroundColor
+ self.samples = samples
+ self.peak = peak
+ }
+
+ public static func ==(lhs: AudioWaveformComponent, rhs: AudioWaveformComponent) -> Bool {
+ if lhs.backgroundColor !== rhs.backgroundColor {
+ return false
+ }
+ if lhs.foregroundColor != rhs.foregroundColor {
+ return false
+ }
+ if lhs.samples != rhs.samples {
+ return false
+ }
+ if lhs.peak != rhs.peak {
+ return false
+ }
+ return true
+ }
+
+ public final class View: UIView {
+ private var component: AudioWaveformComponent?
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ required public init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(component: AudioWaveformComponent, availableSize: CGSize, transition: Transition) -> CGSize {
+ return CGSize(width: availableSize.width, height: availableSize.height)
+ }
+ }
+
+ public func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, transition: transition)
+ }
+}
diff --git a/submodules/TelegramUI/Resources/Animations/textToVoice.json b/submodules/TelegramUI/Resources/Animations/textToVoice.json
new file mode 100644
index 0000000000..df8c19a437
--- /dev/null
+++ b/submodules/TelegramUI/Resources/Animations/textToVoice.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":60,"ip":0,"op":20,"w":300,"h":300,"nm":"Comp 8","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Artboard Copy 2 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2,"l":2},"a":{"a":0,"k":[150,150,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[176.7,163.3],[220,163.3]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":6,"s":[50]},{"t":19,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":6,"s":[50]},{"t":19,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13.3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-17.5,35],[17.5,0],[-17.5,-35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[50]},{"t":19,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[50]},{"t":19,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":10,"s":[13.3]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[65.8,150],"to":[8.333,0],"ti":[-8.333,0]},{"t":19,"s":[115.8,150]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63.3,150],[130,150]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[100]},{"t":19,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":10,"s":[13.3]}],"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[-50,0],"to":[8.333,0],"ti":[-8.333,0]},{"t":19,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Artboard Copy Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2,"l":2},"a":{"a":0,"k":[150,150,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-60,30],[-0.1,-30],[0,-30],[60,30]],"c":false}]},{"t":19,"s":[{"i":[[0,0],[0,0],[-1.637,-4.908],[0,0]],"o":[[0,0],[1.382,-4.16],[0,0],[0,0]],"v":[[13.3,40],[43.581,-46.516],[53.019,-46.516],[83.3,40]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[16.7]},{"t":19,"s":[13.3]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[150,150],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Resources/Animations/voiceToText.json b/submodules/TelegramUI/Resources/Animations/voiceToText.json
new file mode 100644
index 0000000000..df6c472de2
--- /dev/null
+++ b/submodules/TelegramUI/Resources/Animations/voiceToText.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":60,"ip":0,"op":20,"w":300,"h":300,"nm":"Comp 7","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[0]},{"t":19,"s":[-90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[150,150,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":12,"s":[150,178,0],"to":[0,0,0],"ti":[0,0,0]},{"t":19,"s":[150,150,0]}],"ix":2,"l":2},"a":{"a":0,"k":[150,150,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[176.7,163.3],[220,163.3]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[0]},{"t":12,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13.3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.606,-0.912],[-0.503,-1.436],[0,0]],"o":[[0,0],[0.912,-2.606],[1.436,0.503],[0,0],[0,0]],"v":[[-35,45.248],[-4.719,-41.268],[1.652,-44.336],[4.719,-41.268],[35,45.248]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[198.3,144.752],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-17.5,35],[17.5,0],[-17.5,-35]],"c":false}]},{"t":19,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[4.2,60],[64.2,0],[4.2,-60]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[13.3]},{"t":19,"s":[16.7]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[115.8,150],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[63.3,150],[130,150]],"c":false}]},{"t":19,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[93.3,150],[160,150]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[0]},{"t":12,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.592156862745,0.592156862745,0.592156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13.3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift
index 3cf96b13bc..231776a8eb 100644
--- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift
+++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift
@@ -583,6 +583,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
dateAndStatusType: statusType,
displayReactions: false,
messageSelection: nil,
+ layoutConstants: layoutConstants,
constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)
))
refineContentFileLayout = refineLayout
diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift
index ffe13fabdd..c9cf6b1d94 100644
--- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift
+++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift
@@ -135,6 +135,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
dateAndStatusType: statusType,
displayReactions: true,
messageSelection: item.message.groupingKey != nil ? selection : nil,
+ layoutConstants: layoutConstants,
constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)
))
diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift
index 771bf69fa1..b5e6aa87c0 100644
--- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift
+++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift
@@ -18,6 +18,12 @@ import MusicAlbumArtResources
import AudioBlob
import ContextUI
import ChatPresentationInterfaceState
+import ComponentFlow
+import AudioTranscriptionButtonComponent
+import AudioWaveformComponent
+import ShimmerEffect
+import ConvertOpusToAAC
+import LocalAudioTranscription
private struct FetchControls {
let fetch: (Bool) -> Void
@@ -43,6 +49,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let dateAndStatusType: ChatMessageDateAndStatusType?
let displayReactions: Bool
let messageSelection: Bool?
+ let layoutConstants: ChatMessageItemLayoutConstants
let constrainedSize: CGSize
init(
@@ -63,6 +70,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
dateAndStatusType: ChatMessageDateAndStatusType?,
displayReactions: Bool,
messageSelection: Bool?,
+ layoutConstants: ChatMessageItemLayoutConstants,
constrainedSize: CGSize
) {
self.context = context
@@ -82,6 +90,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.dateAndStatusType = dateAndStatusType
self.displayReactions = displayReactions
self.messageSelection = messageSelection
+ self.layoutConstants = layoutConstants
self.constrainedSize = constrainedSize
}
}
@@ -95,7 +104,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private let fetchingCompactTextNode: ImmediateTextNode
private let waveformNode: AudioWaveformNode
private let waveformForegroundNode: AudioWaveformNode
+ private var waveformShimmerNode: ShimmerEffectNode?
+ private var waveformMaskNode: AudioWaveformNode?
private var waveformScrubbingNode: MediaPlayerScrubbingNode?
+ private var audioTranscriptionButton: ComponentHostView?
+ private let textNode: TextNode
let dateAndStatusNode: ChatMessageDateAndStatusNode
private let consumableContentNode: ASImageNode
@@ -157,6 +170,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var streamingCacheStatusFrame: CGRect?
private var fileIconImage: UIImage?
+ private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .possible
+ private var transcribedText: String?
+ private var transcribeDisposable: Disposable?
+
override init() {
self.titleNode = TextNode()
self.titleNode.displaysAsynchronously = false
@@ -189,6 +206,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.waveformForegroundNode = AudioWaveformNode()
self.waveformForegroundNode.isLayerBacked = true
+ self.textNode = TextNode()
+ self.textNode.displaysAsynchronously = false
+ self.textNode.isUserInteractionEnabled = false
+
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
self.consumableContentNode = ASImageNode()
@@ -209,6 +230,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.playbackStatusDisposable.dispose()
self.fetchDisposable.dispose()
self.audioLevelEventsDisposable.dispose()
+ self.transcribeDisposable?.dispose()
}
override func didLoad() {
@@ -238,29 +260,29 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
@objc func progressPressed() {
if let resourceStatus = self.resourceStatus {
switch resourceStatus.mediaStatus {
- case let .fetchStatus(fetchStatus):
- if let context = self.context, let message = self.message, message.flags.isSending {
- let _ = context.account.postbox.transaction({ transaction -> Void in
- context.engine.messages.deleteMessages(transaction: transaction, ids: [message.id])
- }).start()
- } else {
- switch fetchStatus {
- case .Fetching:
- if let cancel = self.fetchControls.with({ return $0?.cancel }) {
- cancel()
- }
- case .Remote, .Paused:
- if let fetch = self.fetchControls.with({ return $0?.fetch }) {
- fetch(true)
- }
- case .Local:
- self.activateLocalContent()
+ case let .fetchStatus(fetchStatus):
+ if let context = self.context, let message = self.message, message.flags.isSending {
+ let _ = context.account.postbox.transaction({ transaction -> Void in
+ context.engine.messages.deleteMessages(transaction: transaction, ids: [message.id])
+ }).start()
+ } else {
+ switch fetchStatus {
+ case .Fetching:
+ if let cancel = self.fetchControls.with({ return $0?.cancel }) {
+ cancel()
}
+ case .Remote, .Paused:
+ if let fetch = self.fetchControls.with({ return $0?.fetch }) {
+ fetch(true)
+ }
+ case .Local:
+ self.activateLocalContent()
}
- case .playbackStatus:
- if let context = self.context, let message = self.message, let type = peerMessageMediaPlayerType(message) {
- context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type)
- }
+ }
+ case .playbackStatus:
+ if let context = self.context, let message = self.message, let type = peerMessageMediaPlayerType(message) {
+ context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type)
+ }
}
}
}
@@ -275,15 +297,98 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
+ private func transcribe() {
+ guard let context = self.context, let message = self.message else {
+ return
+ }
+ if self.transcribedText == nil {
+ if self.transcribeDisposable == nil {
+ self.audioTranscriptionState = .inProgress
+ self.requestUpdateLayout(true)
+
+ if !"".isEmpty {
+ let signal: Signal = context.account.postbox.transaction { transaction -> Message? in
+ return transaction.getMessage(message.id)
+ }
+ |> mapToSignal { message -> Signal in
+ guard let message = message else {
+ return .single(nil)
+ }
+ guard let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile else {
+ return .single(nil)
+ }
+ return context.account.postbox.mediaBox.resourceData(id: file.resource.id)
+ |> take(1)
+ |> mapToSignal { data -> Signal in
+ if !data.complete {
+ return .single(nil)
+ }
+ return .single(data.path)
+ }
+ }
+ |> mapToSignal { result -> Signal in
+ guard let result = result else {
+ return .single(nil)
+ }
+ return convertOpusToAAC(sourcePath: result, allocateTempFile: {
+ return TempBox.shared.tempFile(fileName: "audio.m4a").path
+ })
+ }
+ |> mapToSignal { result -> Signal in
+ guard let result = result else {
+ return .single(nil)
+ }
+ return transcribeAudio(path: result)
+ }
+
+ let _ = signal.start(next: { [weak self] result in
+ guard let strongSelf = self else {
+ return
+ }
+ strongSelf.transcribeDisposable = nil
+ strongSelf.audioTranscriptionState = .expanded
+ strongSelf.transcribedText = result
+ strongSelf.requestUpdateLayout(true)
+ })
+ } else {
+ self.transcribeDisposable = (context.engine.messages.transcribeAudio(messageId: message.id)
+ |> deliverOnMainQueue).start(next: { [weak self] result in
+ guard let strongSelf = self else {
+ return
+ }
+ strongSelf.transcribeDisposable = nil
+ strongSelf.audioTranscriptionState = .expanded
+ strongSelf.transcribedText = result
+ strongSelf.requestUpdateLayout(true)
+ })
+ }
+ }
+ } else {
+ switch self.audioTranscriptionState {
+ case .expanded:
+ self.audioTranscriptionState = .collapsed
+ self.requestUpdateLayout(true)
+ case .collapsed:
+ self.audioTranscriptionState = .expanded
+ self.requestUpdateLayout(true)
+ default:
+ break
+ }
+ }
+ }
+
func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) {
let currentFile = self.file
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
let descriptionAsyncLayout = TextNode.asyncLayout(self.descriptionNode)
let descriptionMeasuringAsyncLayout = TextNode.asyncLayout(self.descriptionMeasuringNode)
+ let textAsyncLayout = TextNode.asyncLayout(self.textNode)
let statusLayout = self.dateAndStatusNode.asyncLayout()
let currentMessage = self.message
+ let transcribedText = self.transcribedText
+ let audioTranscriptionState = self.audioTranscriptionState
return { arguments in
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
@@ -453,6 +558,17 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let (descriptionMeasuringLayout, descriptionMeasuringApply) = descriptionMeasuringAsyncLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(fileSizeString) / \(fileSizeString)", font: descriptionFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let descriptionMaxWidth = max(descriptionLayout.size.width, descriptionMeasuringLayout.size.width)
+ let textFont = arguments.presentationData.messageFont
+ let textString: NSAttributedString?
+ if let transcribedText = transcribedText, case .expanded = audioTranscriptionState {
+ textString = NSAttributedString(string: transcribedText, font: textFont, textColor: messageTheme.primaryTextColor)
+ } else {
+ textString = nil
+ }
+
+ let horizontalInset: CGFloat = (arguments.layoutConstants.bubble.edgeInset + arguments.layoutConstants.bubble.borderInset) * 2.0
+ let inlineTextConstrainedSize = CGSize(width: constrainedSize.width, height: constrainedSize.height)
+ let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: inlineTextConstrainedSize.width - horizontalInset, height: .greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let minVoiceWidth: CGFloat = 120.0
let maxVoiceWidth = constrainedSize.width
@@ -517,6 +633,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
reactionSettings = ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: displayReactionsInline, preferAdditionalInset: !displayReactionsInline)
}
+ let statusLayoutInput: ChatMessageDateAndStatusNode.LayoutInput
+ if let _ = textString {
+ statusLayoutInput = .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: reactionSettings)
+ } else {
+ statusLayoutInput = .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: reactionSettings)
+ }
+
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
context: arguments.context,
presentationData: arguments.presentationData,
@@ -524,7 +647,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
impressionCount: viewCount,
dateText: dateText,
type: statusType,
- layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: reactionSettings),
+ layoutInput: statusLayoutInput,
constrainedSize: constrainedSize,
availableReactions: arguments.associatedData.availableReactions,
reactions: dateReactionsAndPeers.reactions,
@@ -543,7 +666,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let descriptionAndStatusWidth = descriptionLayout.size.width
let calcDuration = max(minVoiceLength, min(maxVoiceLength, CGFloat(audioDuration)))
- minLayoutWidth = minVoiceWidth + (maxVoiceWidth - minVoiceWidth) * (calcDuration - minVoiceLength) / (maxVoiceLength - minVoiceLength)
+ minLayoutWidth = 30.0 + 8.0 + minVoiceWidth + (maxVoiceWidth - minVoiceWidth) * (calcDuration - minVoiceLength) / (maxVoiceLength - minVoiceLength)
minLayoutWidth = max(descriptionAndStatusWidth + 56, minLayoutWidth)
} else {
minLayoutWidth = max(titleLayout.size.width, descriptionMaxWidth) + 44.0 + 8.0
@@ -552,6 +675,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
minLayoutWidth = max(minLayoutWidth, statusSuggestedWidthAndContinue.0)
}
+
+ minLayoutWidth = max(minLayoutWidth, textLayout.size.width + horizontalInset)
let fileIconImage: UIImage?
if hasThumbnail {
@@ -591,6 +716,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height)
}
+ if textString != nil {
+ fittedLayoutSize.width = max(fittedLayoutSize.width + horizontalInset, textLayout.size.width)
+ fittedLayoutSize.height += textLayout.size.height + 5.0
+ }
+
var statusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> Void)?
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth)
@@ -645,8 +775,41 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} else {
statusReferenceFrame = progressFrame.offsetBy(dx: 0.0, dy: 8.0)
}
+
+ if textString == nil, strongSelf.textNode.supernode != nil, animation.isAnimated {
+ if let snapshotView = strongSelf.textNode.view.snapshotContentTree() {
+ snapshotView.frame = strongSelf.textNode.frame
+ strongSelf.view.insertSubview(snapshotView, aboveSubview: strongSelf.textNode.view)
+
+ snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
+ snapshotView?.removeFromSuperview()
+ })
+ }
+ }
+
+ let _ = textApply()
+ let textFrame = CGRect(origin: CGPoint(x: arguments.layoutConstants.text.bubbleInsets.left - arguments.layoutConstants.file.bubbleInsets.left, y: statusReferenceFrame.maxY + 1.0), size: textLayout.size)
+ strongSelf.textNode.frame = textFrame
+ if textString != nil {
+ if strongSelf.textNode.supernode == nil {
+ strongSelf.addSubnode(strongSelf.textNode)
+ if animation.isAnimated {
+ strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
+ }
+ }
+ } else {
+ if strongSelf.textNode.supernode != nil {
+ strongSelf.textNode.removeFromSupernode()
+ }
+ }
+
if let statusSizeAndApply = statusSizeAndApply {
- let statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0)
+ let statusFrame: CGRect
+ if textString != nil {
+ statusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width - 5.0 - statusSizeAndApply.0.width, y: textFrame.maxY + 4.0), size: statusSizeAndApply.0)
+ } else {
+ statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0)
+ }
if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.dateAndStatusNode.frame = statusFrame
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
@@ -671,7 +834,60 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
strongSelf.waveformScrubbingNode = waveformScrubbingNode
strongSelf.addSubnode(waveformScrubbingNode)
}
- strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: 57.0, y: 1.0), size: CGSize(width: boundingWidth - 60.0, height: 15.0))
+
+ let scrubbingFrame = CGRect(origin: CGPoint(x: 57.0, y: 1.0), size: CGSize(width: boundingWidth - 60.0 - 30.0 - 8.0, height: 15.0))
+
+ if case .inProgress = audioTranscriptionState {
+ if strongSelf.waveformShimmerNode == nil {
+ let waveformShimmerNode = ShimmerEffectNode()
+ strongSelf.waveformShimmerNode = waveformShimmerNode
+ strongSelf.addSubnode(waveformShimmerNode)
+
+ let waveformMaskNode = AudioWaveformNode()
+ strongSelf.waveformMaskNode = waveformMaskNode
+ waveformShimmerNode.view.mask = waveformMaskNode.view
+ }
+
+ if let audioWaveform = audioWaveform, let waveformShimmerNode = strongSelf.waveformShimmerNode, let waveformMaskNode = strongSelf.waveformMaskNode {
+ waveformShimmerNode.frame = scrubbingFrame
+ waveformShimmerNode.updateAbsoluteRect(scrubbingFrame, within: CGSize(width: scrubbingFrame.size.width + 60.0, height: scrubbingFrame.size.height + 4.0))
+
+ var shapes: [ShimmerEffectNode.Shape] = []
+ shapes.append(.rect(rect: CGRect(origin: CGPoint(), size: scrubbingFrame.size)))
+ waveformShimmerNode.update(
+ backgroundColor: .blue,
+ foregroundColor: messageTheme.mediaInactiveControlColor,
+ shimmeringColor: messageTheme.mediaActiveControlColor,
+ shapes: shapes,
+ horizontal: true,
+ effectSize: 60.0,
+ globalTimeOffset: false,
+ duration: 0.7,
+ size: scrubbingFrame.size
+ )
+
+ waveformMaskNode.frame = CGRect(origin: CGPoint(), size: scrubbingFrame.size)
+ waveformMaskNode.setup(color: .black, gravity: .bottom, waveform: audioWaveform)
+ }
+ } else {
+ if let waveformShimmerNode = strongSelf.waveformShimmerNode {
+ strongSelf.waveformShimmerNode = nil
+ if animation.isAnimated {
+ waveformShimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak waveformShimmerNode] _ in
+ waveformShimmerNode?.removeFromSupernode()
+ })
+ } else {
+ waveformShimmerNode.removeFromSupernode()
+ }
+ }
+ strongSelf.waveformMaskNode = nil
+ }
+
+ if let waveformScrubbingNode = strongSelf.waveformScrubbingNode {
+ waveformScrubbingNode.frame = scrubbingFrame
+ //animation.animator.updateFrame(layer: waveformScrubbingNode.layer, frame: scrubbingFrame, completion: nil)
+ //waveformScrubbingNode.update(size: scrubbingFrame.size, animator: animation.animator)
+ }
let waveformColor: UIColor
if arguments.incoming {
if consumableContentIcon != nil {
@@ -684,9 +900,40 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
strongSelf.waveformNode.setup(color: waveformColor, gravity: .bottom, waveform: audioWaveform)
strongSelf.waveformForegroundNode.setup(color: messageTheme.mediaActiveControlColor, gravity: .bottom, waveform: audioWaveform)
- } else if let waveformScrubbingNode = strongSelf.waveformScrubbingNode {
- strongSelf.waveformScrubbingNode = nil
- waveformScrubbingNode.removeFromSupernode()
+
+ let audioTranscriptionButton: ComponentHostView
+ if let current = strongSelf.audioTranscriptionButton {
+ audioTranscriptionButton = current
+ } else {
+ audioTranscriptionButton = ComponentHostView()
+ strongSelf.audioTranscriptionButton = audioTranscriptionButton
+ strongSelf.view.addSubview(audioTranscriptionButton)
+ }
+ let audioTranscriptionButtonSize = audioTranscriptionButton.update(
+ transition: animation.isAnimated ? .easeInOut(duration: 0.3) : .immediate,
+ component: AnyComponent(AudioTranscriptionButtonComponent(
+ theme: arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing,
+ transcriptionState: audioTranscriptionState,
+ pressed: {
+ guard let strongSelf = self else {
+ return
+ }
+ strongSelf.transcribe()
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: 30.0, height: 30.0)
+ )
+ animation.animator.updateFrame(layer: audioTranscriptionButton.layer, frame: CGRect(origin: CGPoint(x: boundingWidth - 30.0 + 3.0, y: -6.0), size: audioTranscriptionButtonSize), completion: nil)
+ } else {
+ if let waveformScrubbingNode = strongSelf.waveformScrubbingNode {
+ strongSelf.waveformScrubbingNode = nil
+ waveformScrubbingNode.removeFromSupernode()
+ }
+ if let audioTranscriptionButton = strongSelf.audioTranscriptionButton {
+ strongSelf.audioTranscriptionButton = nil
+ audioTranscriptionButton.removeFromSuperview()
+ }
}
if let iconFrame = iconFrame {
@@ -1213,6 +1460,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
}
+ if let audioTranscriptionButton = self.audioTranscriptionButton {
+ if let result = audioTranscriptionButton.hitTest(self.view.convert(point, to: self.audioTranscriptionButton), with: event) {
+ return result
+ }
+ }
return super.hitTest(point, with: event)
}
diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm
index e5a7102036..1c0cbe2633 100644
--- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm
+++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm
@@ -836,7 +836,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
+ (tgcalls::ProtocolVersion)protocolVersionFromLibraryVersion:(NSString *)version {
if ([version isEqualToString:@"2.7.7"]) {
return tgcalls::ProtocolVersion::V0;
- } else if ([version isEqualToString:@"3.0.0"]) {
+ } else if ([version isEqualToString:@"5.0.0"]) {
return tgcalls::ProtocolVersion::V1;
} else {
return tgcalls::ProtocolVersion::V0;
diff --git a/submodules/WallpaperBackgroundNode/BUILD b/submodules/WallpaperBackgroundNode/BUILD
index 40b4495105..ccd9dff870 100644
--- a/submodules/WallpaperBackgroundNode/BUILD
+++ b/submodules/WallpaperBackgroundNode/BUILD
@@ -1,4 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+load(
+ "@build_bazel_rules_apple//apple:resources.bzl",
+ "apple_resource_bundle",
+ "apple_resource_group",
+)
+load("//build-system/bazel-utils:plist_fragment.bzl",
+ "plist_fragment",
+)
+
+filegroup(
+ name = "WallpaperBackgroundNodeMetalResources",
+ srcs = glob([
+ "Resources/**/*.metal",
+ ]),
+ visibility = ["//visibility:public"],
+)
+
+plist_fragment(
+ name = "WallpaperBackgroundNodeBundleInfoPlist",
+ extension = "plist",
+ template =
+ """
+ CFBundleIdentifier
+ org.telegram.WallpaperBackgroundNode
+ CFBundleDevelopmentRegion
+ en
+ CFBundleName
+ WallpaperBackgroundNode
+ """
+)
+
+apple_resource_bundle(
+ name = "WallpaperBackgroundNodeBundle",
+ infoplists = [
+ ":WallpaperBackgroundNodeBundleInfoPlist",
+ ],
+ resources = [
+ ":WallpaperBackgroundNodeMetalResources",
+ ],
+)
swift_library(
name = "WallpaperBackgroundNode",
@@ -9,6 +49,9 @@ swift_library(
copts = [
"-warnings-as-errors",
],
+ data = [
+ ":WallpaperBackgroundNodeBundle",
+ ],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
diff --git a/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal b/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal
new file mode 100644
index 0000000000..e515393fbc
--- /dev/null
+++ b/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal
@@ -0,0 +1,35 @@
+#include
+using namespace metal;
+
+typedef struct {
+ packed_float2 position;
+} Vertex;
+
+typedef struct {
+ float4 position[[position]];
+} Varyings;
+
+vertex Varyings wallpaperVertex(constant Vertex *verticies[[buffer(0)]], unsigned int vid[[vertex_id]]) {
+ Varyings out;
+ constant Vertex &v = verticies[vid];
+ out.position = float4(float2(v.position), 0.0, 1.0);
+
+ return out;
+}
+
+fragment half4 wallpaperFragment1(Varyings in[[stage_in]]) {
+ float4 out = float4(0.0, 1.0, 0.0, 1.0);
+
+ return half4(out);
+}
+
+fragment half4 wallpaperFragment(Varyings in[[stage_in]], constant uint2 &resolution[[buffer(0)]], constant float &time[[buffer(1)]]) {
+ half4 p = half4(in.position);
+ p.y = -p.y;
+
+ p.y /= resolution.y;
+ p.y += tan(time + tan(p.x) + sin(.2 * p.x));
+ float4 out = float4(0.0, (0.3 + (p.y < 0.0 ? 0.0 : 1.0 - p.y * 3.0)) * 0.2, 0.0, 1.0);
+
+ return half4(out);
+}
diff --git a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift
new file mode 100644
index 0000000000..4c2c6a6f47
--- /dev/null
+++ b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift
@@ -0,0 +1,312 @@
+import Foundation
+import UIKit
+import AsyncDisplayKit
+import Display
+import GradientBackground
+import TelegramPresentationData
+import TelegramCore
+import AccountContext
+import SwiftSignalKit
+import WallpaperResources
+import FastBlur
+import Svg
+import GZip
+import AppBundle
+import AnimatedStickerNode
+import TelegramAnimatedStickerNode
+import HierarchyTrackingLayer
+import MetalKit
+import HierarchyTrackingLayer
+import simd
+
+private final class NullActionClass: NSObject, CAAction {
+ static let shared = NullActionClass()
+
+ @objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
+ }
+}
+
+@available(iOS 13.0, *)
+open class SimpleMetalLayer: CAMetalLayer {
+ override open func action(forKey event: String) -> CAAction? {
+ return nullAction
+ }
+
+ override public init() {
+ super.init()
+ }
+
+ override public init(layer: Any) {
+ super.init(layer: layer)
+ }
+
+ required public init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
+
+private func makePipelineState(device: MTLDevice, library: MTLLibrary, vertexProgram: String, fragmentProgram: String) -> MTLRenderPipelineState? {
+ guard let loadedVertexProgram = library.makeFunction(name: vertexProgram) else {
+ return nil
+ }
+ guard let loadedFragmentProgram = library.makeFunction(name: fragmentProgram) else {
+ return nil
+ }
+
+ let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
+ pipelineStateDescriptor.vertexFunction = loadedVertexProgram
+ pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram
+ pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
+ guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) else {
+ return nil
+ }
+
+ return pipelineState
+}
+
+@available(iOS 13.0, *)
+final class MetalWallpaperBackgroundNode: ASDisplayNode, WallpaperBackgroundNode {
+ private let device: MTLDevice
+ private let metalLayer: SimpleMetalLayer
+ private let commandQueue: MTLCommandQueue
+ private let renderPipelineState: MTLRenderPipelineState
+
+ private let hierarchyTrackingLayer = HierarchyTrackingLayer()
+
+ var isReady: Signal {
+ return .single(true)
+ }
+
+ var rotation: CGFloat = 0.0
+
+ private var animationPhase: Int = 0
+
+ private var animationThread: Thread?
+ private var displayLink: CADisplayLink?
+
+ override init() {
+ self.device = MTLCreateSystemDefaultDevice()!
+ self.metalLayer = SimpleMetalLayer()
+ self.metalLayer.maximumDrawableCount = 3
+ self.metalLayer.presentsWithTransaction = true
+ self.metalLayer.contentsScale = UIScreenScale
+ self.commandQueue = self.device.makeCommandQueue()!
+
+ let mainBundle = Bundle(for: MetalWallpaperBackgroundNode.self)
+
+ guard let path = mainBundle.path(forResource: "WallpaperBackgroundNodeBundle", ofType: "bundle") else {
+ preconditionFailure()
+ }
+ guard let bundle = Bundle(path: path) else {
+ preconditionFailure()
+ }
+ guard let defaultLibrary = try? self.device.makeDefaultLibrary(bundle: bundle) else {
+ preconditionFailure()
+ }
+
+ guard let renderPipelineState = makePipelineState(device: self.device, library: defaultLibrary, vertexProgram: "wallpaperVertex", fragmentProgram: "wallpaperFragment") else {
+ preconditionFailure()
+ }
+ self.renderPipelineState = renderPipelineState
+
+ super.init()
+
+ self.metalLayer.device = self.device
+ self.metalLayer.pixelFormat = .bgra8Unorm
+ self.metalLayer.framebufferOnly = true
+ self.metalLayer.allowsNextDrawableTimeout = true
+ self.metalLayer.isOpaque = true
+
+ self.layer.addSublayer(self.metalLayer)
+ self.layer.addSublayer(self.hierarchyTrackingLayer)
+
+ self.hierarchyTrackingLayer.opacity = 0.0
+ self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
+ self?.updateIsVisible(true)
+ }
+ self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
+ self?.updateIsVisible(false)
+ }
+ }
+
+ func update(wallpaper: TelegramWallpaper) {
+
+ }
+
+ func _internalUpdateIsSettingUpWallpaper() {
+
+ }
+
+ func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
+ if self.metalLayer.drawableSize != size {
+ self.metalLayer.drawableSize = size
+
+ transition.updateFrame(layer: self.metalLayer, frame: CGRect(origin: CGPoint(), size: size))
+
+ self.redraw()
+ }
+ }
+
+ private func updateIsVisible(_ isVisible: Bool) {
+ if isVisible {
+ if self.displayLink == nil {
+ final class DisplayLinkTarget: NSObject {
+ private let f: () -> Void
+
+ init(_ f: @escaping () -> Void) {
+ self.f = f
+ }
+
+ @objc func event() {
+ self.f()
+ }
+ }
+
+ let displayLink = CADisplayLink(target: DisplayLinkTarget { [weak self] in
+ guard let strongSelf = self else {
+ return
+ }
+ strongSelf.redraw()
+ }, selector: #selector(DisplayLinkTarget.event))
+ self.displayLink = displayLink
+ if #available(iOS 15.0, iOSApplicationExtension 15.0, *) {
+ if "".isEmpty {
+ displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 60.0, preferred: 60.0)
+ } else {
+ displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond))
+ }
+ }
+ displayLink.isPaused = false
+
+ if !"".isEmpty {
+ self.animationThread = Thread(block: {
+ displayLink.add(to: .current, forMode: .common)
+
+ while true {
+ if Thread.current.isCancelled {
+ break
+ }
+ RunLoop.current.run(until: .init(timeIntervalSinceNow: 1.0))
+ }
+ })
+ self.animationThread?.name = "MetalWallpaperBackgroundNode"
+ self.animationThread?.qualityOfService = .userInteractive
+ self.animationThread?.start()
+ } else {
+ displayLink.add(to: .current, forMode: .common)
+ }
+ }
+ } else {
+ if let displayLink = self.displayLink {
+ self.displayLink = nil
+
+ displayLink.invalidate()
+ }
+ if let animationThread = self.animationThread {
+ self.animationThread = nil
+ animationThread.cancel()
+ }
+ }
+ }
+
+ private var previousDrawTime: Double?
+
+ private func redraw() {
+ let timestamp = CACurrentMediaTime()
+ if let previousDrawTime = self.previousDrawTime {
+ let _ = previousDrawTime
+ //print("frame time \((timestamp - previousDrawTime) * 1000.0)")
+ }
+ self.previousDrawTime = timestamp
+
+ self.animationPhase += 1
+ let animationOffset = Float(self.animationPhase % 200) / 200.0
+ let _ = animationOffset
+
+ guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
+ return
+ }
+ guard let drawable = self.metalLayer.nextDrawable() else {
+ return
+ }
+
+ let drawTime = CACurrentMediaTime() - timestamp
+ if drawTime > 9.0 / 1000.0 {
+ print("get time \(drawTime * 1000.0)")
+ }
+
+ let renderPassDescriptor = MTLRenderPassDescriptor()
+ renderPassDescriptor.colorAttachments[0].texture = drawable.texture
+ renderPassDescriptor.colorAttachments[0].loadAction = .clear
+ renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(
+ red: 0.0,
+ green: 0.0,
+ blue: 0.0,
+ alpha: 1.0
+ )
+
+ guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
+ return
+ }
+
+ var vertices: [Float] = [
+ -1.0, -1.0,
+ 1.0, -1.0,
+ -1.0, 1.0,
+ 1.0, 1.0
+ ]
+
+ renderEncoder.setRenderPipelineState(self.renderPipelineState)
+
+ renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
+
+ var resolution = simd_uint2(UInt32(drawable.texture.width), UInt32(drawable.texture.height))
+ renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0)
+
+ var time = Float(timestamp) * 0.25
+ renderEncoder.setFragmentBytes(&time, length: 4, index: 1)
+
+ renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1)
+
+ renderEncoder.endEncoding()
+
+ if self.metalLayer.presentsWithTransaction {
+ if Thread.isMainThread {
+ commandBuffer.commit()
+ commandBuffer.waitUntilScheduled()
+ drawable.present()
+ } else {
+ CATransaction.begin()
+ commandBuffer.commit()
+ commandBuffer.waitUntilScheduled()
+ drawable.present()
+ CATransaction.commit()
+ }
+ } else {
+ commandBuffer.addScheduledHandler { _ in
+ drawable.present()
+ }
+ commandBuffer.commit()
+ }
+ }
+
+ func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) {
+
+ }
+
+ func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) {
+
+ }
+
+ func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool {
+ return false
+ }
+
+ func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? {
+ return nil
+ }
+
+ func makeDimmedNode() -> ASDisplayNode? {
+ return nil
+ }
+}
diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift
index 62f4fc6297..e9d345b38c 100644
--- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift
+++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift
@@ -1775,7 +1775,13 @@ private let sharedStorage = WallpaperBackgroundNodeMergedImpl.SharedStorage()
public func createWallpaperBackgroundNode(context: AccountContext, forChatDisplay: Bool, useSharedAnimationPhase: Bool = false, useExperimentalImplementation: Bool = false) -> WallpaperBackgroundNode {
if forChatDisplay && useExperimentalImplementation {
+ #if DEBUG
+ if #available(iOS 13.0, iOSApplicationExtension 13.0, *) {
+ return MetalWallpaperBackgroundNode()
+ }
+ #else
return WallpaperBackgroundNodeMergedImpl(context: context, storage: useSharedAnimationPhase ? sharedStorage : nil)
+ #endif
}
return WallpaperBackgroundNodeImpl(context: context, useSharedAnimationPhase: useSharedAnimationPhase)