From 574583381f6f5f00a3345ef240c8525f17fa442d Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 7 May 2022 21:22:58 +0400 Subject: [PATCH 1/3] Temp --- .../WhiteFilledIcon@2x.png | Bin 2367 -> 9992 bytes .../WhiteFilledIcon@3x.png | Bin 4196 -> 12364 bytes .../Sources/Items/ItemListCheckboxItem.swift | 4 +- .../Sources/OngoingCallContext.swift | 2 +- .../Sources/OngoingCallThreadLocalContext.mm | 2 +- submodules/WallpaperBackgroundNode/BUILD | 43 +++ .../WallpaperBackgroundShaders.metal | 24 ++ .../MetalWallpaperBackgroundNode.swift | 301 ++++++++++++++++++ .../Sources/WallpaperBackgroundNode.swift | 6 + 9 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal create mode 100644 submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift diff --git a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png index 221eb7e7ee5de3324255638dabb8eb2ffa48699b..2e52591bc340a1829f046c727300d601616e6b43 100644 GIT binary patch literal 9992 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 BQ&0c^ delta 2357 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 DIMaxm 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/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 45423635a7..67bf0508e3 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -784,7 +784,7 @@ public final class OngoingCallContext { var allowP2P = allowP2P if debugUseLegacyVersionForReflectors { useModernImplementation = true - version = "4.1.2" + version = "10.0.0" allowP2P = false } else { useModernImplementation = version != OngoingCallThreadLocalContext.version() diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 58ed57d11e..186f16661c 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -831,7 +831,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..e0f3204bb5 --- /dev/null +++ b/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal @@ -0,0 +1,24 @@ +#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 wallpaperFragment(Varyings in[[stage_in]]) { + float4 out = float4(0.0, 1.0, 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..43b2cb28fb --- /dev/null +++ b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift @@ -0,0 +1,301 @@ +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 + +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, *) { + 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, animationOffset * 1.0, + 1.0, animationOffset * 1.0 + ] + + renderEncoder.setRenderPipelineState(self.renderPipelineState) + + renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0) + + 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) From 5ad827599bcbc529332539c6fdbb9173a2cdbb44 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 9 May 2022 20:01:32 +0400 Subject: [PATCH 2/3] Shader --- .../Resources/WallpaperBackgroundShaders.metal | 13 ++++++++++++- .../Sources/MetalWallpaperBackgroundNode.swift | 11 +++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal b/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal index e0f3204bb5..e515393fbc 100644 --- a/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal +++ b/submodules/WallpaperBackgroundNode/Resources/WallpaperBackgroundShaders.metal @@ -17,8 +17,19 @@ vertex Varyings wallpaperVertex(constant Vertex *verticies[[buffer(0)]], unsigne return out; } -fragment half4 wallpaperFragment(Varyings in[[stage_in]]) { +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 index 43b2cb28fb..5a86915eb3 100644 --- a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift @@ -17,6 +17,7 @@ import TelegramAnimatedStickerNode import HierarchyTrackingLayer import MetalKit import HierarchyTrackingLayer +import simd private final class NullActionClass: NSObject, CAAction { static let shared = NullActionClass() @@ -247,13 +248,19 @@ final class MetalWallpaperBackgroundNode: ASDisplayNode, WallpaperBackgroundNode var vertices: [Float] = [ -1.0, -1.0, 1.0, -1.0, - -1.0, animationOffset * 1.0, - 1.0, animationOffset * 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) From 6b3c11a6fdf66ccd1a57bac3350529ea690c41a5 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 14 May 2022 00:08:44 +0400 Subject: [PATCH 3/3] Audio transcription --- Telegram/BUILD | 2 + .../Sources/ChatListController.swift | 4 +- .../Sources/BundleIconComponent.swift | 2 +- .../Sources/LottieAnimationComponent.swift | 53 ++- submodules/Display/Source/SimpleLayer.swift | 16 + submodules/Media/ConvertOpusToAAC/BUILD | 20 ++ .../Sources/ConvertOpusToAAC.swift | 69 ++++ .../Media/LocalAudioTranscription/BUILD | 18 + .../Sources/LocalAudioTranscription.swift | 73 +++++ .../Sources/FFMpegAudioFrameDecoder.swift | 34 +- .../Sources/MediaPlayerScrubbingNode.swift | 23 +- .../Sources/SoftwareVideoSource.swift | 13 + .../Sources/MTBindKeyMessageService.m | 3 +- .../ShimmerEffect/Sources/ShimmerEffect.swift | 29 +- .../Sources/StickerShimmerEffectNode.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 5 +- submodules/TelegramApi/Sources/Api10.swift | 30 +- submodules/TelegramApi/Sources/Api25.swift | 128 ++------ submodules/TelegramApi/Sources/Api26.swift | 92 ++++++ submodules/TelegramApi/Sources/Api27.swift | 22 +- .../Components/MediaStreamComponent.swift | 3 +- .../ApiUtils/TelegramMediaAction.swift | 4 +- .../SyncCore_TelegramMediaAction.swift | 11 +- .../TelegramEngine/Messages/AdMessages.swift | 8 +- .../Messages/TelegramEngineMessages.swift | 4 + .../TelegramEngine/Messages/Translate.swift | 25 ++ .../Payments/BotPaymentForm.swift | 18 +- .../Sources/ServiceMessageStrings.swift | 2 +- submodules/TelegramUI/BUILD | 4 + .../AudioTranscriptionButtonComponent/BUILD | 22 ++ .../AudioTranscriptionButtonComponent.swift | 188 +++++++++++ .../Components/AudioWaveformComponent/BUILD | 20 ++ .../Sources/AudioWaveformComponent.swift | 63 ++++ .../Resources/Animations/textToVoice.json | 1 + .../Resources/Animations/voiceToText.json | 1 + .../ChatMessageAttachedContentNode.swift | 1 + .../ChatMessageFileBubbleContentNode.swift | 1 + .../ChatMessageInteractiveFileNode.swift | 308 ++++++++++++++++-- .../MetalWallpaperBackgroundNode.swift | 6 +- 39 files changed, 1139 insertions(+), 189 deletions(-) create mode 100644 submodules/Media/ConvertOpusToAAC/BUILD create mode 100644 submodules/Media/ConvertOpusToAAC/Sources/ConvertOpusToAAC.swift create mode 100644 submodules/Media/LocalAudioTranscription/BUILD create mode 100644 submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift create mode 100644 submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift create mode 100644 submodules/TelegramUI/Components/AudioWaveformComponent/BUILD create mode 100644 submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift create mode 100644 submodules/TelegramUI/Resources/Animations/textToVoice.json create mode 100644 submodules/TelegramUI/Resources/Animations/voiceToText.json 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/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index ad293b8e21..f4651eb569 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/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/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 4945e00a2e..7b282e7966 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -414,7 +414,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1281329567] = { return Api.MessageAction.parse_messageActionGroupCallScheduled($0) } dict[-1615153660] = { return Api.MessageAction.parse_messageActionHistoryClear($0) } dict[1345295095] = { return Api.MessageAction.parse_messageActionInviteToGroupCall($0) } - dict[1080663248] = { return Api.MessageAction.parse_messageActionPaymentSent($0) } + dict[-1776926890] = { return Api.MessageAction.parse_messageActionPaymentSent($0) } dict[-1892568281] = { return Api.MessageAction.parse_messageActionPaymentSentMe($0) } dict[-2132731265] = { return Api.MessageAction.parse_messageActionPhoneCall($0) } dict[-1799538451] = { return Api.MessageAction.parse_messageActionPinMessage($0) } @@ -985,6 +985,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) } dict[816245886] = { return Api.messages.Stickers.parse_stickers($0) } dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) } + dict[-1442723025] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) } dict[1741309751] = { return Api.messages.TranslatedText.parse_translateNoResult($0) } dict[-1575684144] = { return Api.messages.TranslatedText.parse_translateResultText($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } @@ -1739,6 +1740,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.Stickers: _1.serialize(buffer, boxed) + case let _1 as Api.messages.TranscribedAudio: + _1.serialize(buffer, boxed) case let _1 as Api.messages.TranslatedText: _1.serialize(buffer, boxed) case let _1 as Api.messages.VotesList: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 0a7fe0a125..e99bde0f5d 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1009,7 +1009,7 @@ public extension Api { case messageActionGroupCallScheduled(call: Api.InputGroupCall, scheduleDate: Int32) case messageActionHistoryClear case messageActionInviteToGroupCall(call: Api.InputGroupCall, users: [Int64]) - case messageActionPaymentSent(currency: String, totalAmount: Int64) + case messageActionPaymentSent(flags: Int32, currency: String, totalAmount: Int64, invoiceSlug: String?) case messageActionPaymentSentMe(flags: Int32, currency: String, totalAmount: Int64, payload: Buffer, info: Api.PaymentRequestedInfo?, shippingOptionId: String?, charge: Api.PaymentCharge) case messageActionPhoneCall(flags: Int32, callId: Int64, reason: Api.PhoneCallDiscardReason?, duration: Int32?) case messageActionPinMessage @@ -1170,12 +1170,14 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) } break - case .messageActionPaymentSent(let currency, let totalAmount): + case .messageActionPaymentSent(let flags, let currency, let totalAmount, let invoiceSlug): if boxed { - buffer.appendInt32(1080663248) + buffer.appendInt32(-1776926890) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) serializeInt64(totalAmount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(invoiceSlug!, buffer: buffer, boxed: false)} break case .messageActionPaymentSentMe(let flags, let currency, let totalAmount, let payload, let info, let shippingOptionId, let charge): if boxed { @@ -1303,8 +1305,8 @@ public extension Api { return ("messageActionHistoryClear", []) case .messageActionInviteToGroupCall(let call, let users): return ("messageActionInviteToGroupCall", [("call", String(describing: call)), ("users", String(describing: users))]) - case .messageActionPaymentSent(let currency, let totalAmount): - return ("messageActionPaymentSent", [("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount))]) + case .messageActionPaymentSent(let flags, let currency, let totalAmount, let invoiceSlug): + return ("messageActionPaymentSent", [("flags", String(describing: flags)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount)), ("invoiceSlug", String(describing: invoiceSlug))]) case .messageActionPaymentSentMe(let flags, let currency, let totalAmount, let payload, let info, let shippingOptionId, let charge): return ("messageActionPaymentSentMe", [("flags", String(describing: flags)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount)), ("payload", String(describing: payload)), ("info", String(describing: info)), ("shippingOptionId", String(describing: shippingOptionId)), ("charge", String(describing: charge))]) case .messageActionPhoneCall(let flags, let callId, let reason, let duration): @@ -1565,14 +1567,20 @@ public extension Api { } } public static func parse_messageActionPaymentSent(_ reader: BufferReader) -> MessageAction? { - var _1: String? - _1 = parseString(reader) - var _2: Int64? - _2 = reader.readInt64() + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MessageAction.messageActionPaymentSent(currency: _1!, totalAmount: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MessageAction.messageActionPaymentSent(flags: _1!, currency: _2!, totalAmount: _3!, invoiceSlug: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 0635fa8ff7..2acb37d32a 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -488,6 +488,42 @@ public extension Api.messages { } } +public extension Api.messages { + enum TranscribedAudio: TypeConstructorDescription { + case transcribedAudio(text: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .transcribedAudio(let text): + if boxed { + buffer.appendInt32(-1442723025) + } + serializeString(text, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .transcribedAudio(let text): + return ("transcribedAudio", [("text", String(describing: text))]) + } + } + + public static func parse_transcribedAudio(_ reader: BufferReader) -> TranscribedAudio? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.messages.TranscribedAudio.transcribedAudio(text: _1!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum TranslatedText: TypeConstructorDescription { case translateNoResult @@ -1464,95 +1500,3 @@ public extension Api.photos { } } -public extension Api.photos { - enum Photos: TypeConstructorDescription { - case photos(photos: [Api.Photo], users: [Api.User]) - case photosSlice(count: Int32, photos: [Api.Photo], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photos(let photos, let users): - if boxed { - buffer.appendInt32(-1916114267) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(photos.count)) - for item in photos { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .photosSlice(let count, let photos, let users): - if boxed { - buffer.appendInt32(352657236) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(photos.count)) - for item in photos { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .photos(let photos, let users): - return ("photos", [("photos", String(describing: photos)), ("users", String(describing: users))]) - case .photosSlice(let count, let photos, let users): - return ("photosSlice", [("count", String(describing: count)), ("photos", String(describing: photos)), ("users", String(describing: users))]) - } - } - - public static func parse_photos(_ reader: BufferReader) -> Photos? { - var _1: [Api.Photo]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.photos.Photos.photos(photos: _1!, users: _2!) - } - else { - return nil - } - } - public static func parse_photosSlice(_ reader: BufferReader) -> Photos? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Photo]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.photos.Photos.photosSlice(count: _1!, photos: _2!, users: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index ad5ef9bca8..58fa0773fb 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1,3 +1,95 @@ +public extension Api.photos { + enum Photos: TypeConstructorDescription { + case photos(photos: [Api.Photo], users: [Api.User]) + case photosSlice(count: Int32, photos: [Api.Photo], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photos(let photos, let users): + if boxed { + buffer.appendInt32(-1916114267) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(photos.count)) + for item in photos { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .photosSlice(let count, let photos, let users): + if boxed { + buffer.appendInt32(352657236) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(photos.count)) + for item in photos { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .photos(let photos, let users): + return ("photos", [("photos", String(describing: photos)), ("users", String(describing: users))]) + case .photosSlice(let count, let photos, let users): + return ("photosSlice", [("count", String(describing: count)), ("photos", String(describing: photos)), ("users", String(describing: users))]) + } + } + + public static func parse_photos(_ reader: BufferReader) -> Photos? { + var _1: [Api.Photo]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.photos.Photos.photos(photos: _1!, users: _2!) + } + else { + return nil + } + } + public static func parse_photosSlice(_ reader: BufferReader) -> Photos? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Photo]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Photo.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.photos.Photos.photosSlice(count: _1!, photos: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.stats { enum BroadcastStats: TypeConstructorDescription { case broadcastStats(period: Api.StatsDateRangeDays, followers: Api.StatsAbsValueAndPrev, viewsPerPost: Api.StatsAbsValueAndPrev, sharesPerPost: Api.StatsAbsValueAndPrev, enabledNotifications: Api.StatsPercentValue, growthGraph: Api.StatsGraph, followersGraph: Api.StatsGraph, muteGraph: Api.StatsGraph, topHoursGraph: Api.StatsGraph, interactionsGraph: Api.StatsGraph, ivInteractionsGraph: Api.StatsGraph, viewsBySourceGraph: Api.StatsGraph, newFollowersBySourceGraph: Api.StatsGraph, languagesGraph: Api.StatsGraph, recentMessageInteractions: [Api.MessageInteractionCounters]) diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 09fd916287..d811b8f4ca 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -6024,6 +6024,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func transcribeAudio(peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(647928393) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.transcribeAudio", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.TranscribedAudio? in + let reader = BufferReader(buffer) + var result: Api.messages.TranscribedAudio? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.TranscribedAudio + } + return result + }) + } +} public extension Api.functions.messages { static func translateText(flags: Int32, peer: Api.InputPeer?, msgId: Int32?, text: String?, fromLang: String?, toLang: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7222,11 +7238,11 @@ public extension Api.functions.upload { } } public extension Api.functions.upload { - static func getFileHashes(location: Api.InputFileLocation, offset: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.FileHash]>) { + static func getFileHashes(location: Api.InputFileLocation, offset: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.FileHash]>) { let buffer = Buffer() - buffer.appendInt32(-956147407) + buffer.appendInt32(-1856595926) location.serialize(buffer, true) - serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt64(offset, buffer: buffer, boxed: false) return (FunctionDescription(name: "upload.getFileHashes", parameters: [("location", String(describing: location)), ("offset", String(describing: offset))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.FileHash]? in let reader = BufferReader(buffer) var result: [Api.FileHash]? 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 dd34b5c563..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 a50599ac01..29de9beb96 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -270,6 +270,10 @@ swift_library( "//submodules/PremiumUI:PremiumUI", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", "//submodules/Utils/RangeSet:RangeSet", + "//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/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift index 5a86915eb3..4c2c6a6f47 100644 --- a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift @@ -170,7 +170,11 @@ final class MetalWallpaperBackgroundNode: ASDisplayNode, WallpaperBackgroundNode }, selector: #selector(DisplayLinkTarget.event)) self.displayLink = displayLink if #available(iOS 15.0, iOSApplicationExtension 15.0, *) { - displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + 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