diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 8420e1a8eb..64eea5d434 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -12,7 +12,7 @@ public func freeMediaFileInteractiveFetched(account: Account, fileReference: Fil public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority) -> Signal { let file = fileReference.media let mediaReference = AnyMediaReference.standalone(media: fileReference.media) - return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int32.max) as Range), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil) + return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int64.max) as Range), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil) } public func freeMediaFileResourceInteractiveFetched(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal { @@ -37,7 +37,7 @@ public func messageMediaFileInteractiveFetched(context: AccountContext, message: return messageMediaFileInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), file: file, userInitiated: userInitiated, priority: .userInitiated) } -public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: IndexSet = IndexSet(integersIn: 0 ..< Int(Int32.max) as Range), userInitiated: Bool, priority: FetchManagerPriority) -> Signal { +public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: IndexSet = IndexSet(integersIn: 0 ..< Int(Int64.max) as Range), userInitiated: Bool, priority: FetchManagerPriority) -> Signal { let mediaReference = AnyMediaReference.message(message: messageReference, media: file) return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: ranges, statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: nil) } diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm index 75bfd3657a..df7b70e943 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -8,11 +8,29 @@ // #import +#import "ASTextKitContext.h" #import #import #import +@implementation ASCustomTextContainer + +- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect { + CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect]; + + /*if (result.origin.y < 10.0f) { + result.size.width -= 20.0f; + if (result.size.width < 0.0f) { + result.size.width = 0.0f; + } + }*/ + + return result; +} + +@end + @interface ASCustomLayoutManager : NSLayoutManager @end @@ -118,7 +136,8 @@ components.layoutManager = layoutManager; [components.textStorage addLayoutManager:components.layoutManager]; - components.textContainer = [[NSTextContainer alloc] initWithSize:textContainerSize]; + components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize]; + //components.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(textContainerSize.width - 60.0, 0.0, 60.0, 40.0)]]; components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. [components.layoutManager addTextContainer:components.textContainer]; diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.h b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h index 82a40b7d8d..5decde42f7 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitContext.h +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h @@ -50,4 +50,8 @@ AS_SUBCLASSING_RESTRICTED @end +@interface ASCustomTextContainer : NSTextContainer + +@end + #endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm index 42f4a1a45c..8e9c276fef 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm @@ -58,7 +58,7 @@ [_textStorage setAttributedString:attributedString]; } - _textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize]; + _textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize]; // We want the text laid out up to the very edges of the container. _textContainer.lineFragmentPadding = 0; _textContainer.lineBreakMode = lineBreakMode; diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index ba13d770d6..e1716f32a5 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -316,13 +316,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) - self.emojiViewProvider = { [weak self] emoji in + /*self.emojiViewProvider = { [weak self] emoji in guard let strongSelf = self, let file = strongSelf.context.animatedEmojiStickers[emoji]?.first?.file else { return UIView() } return EmojiTextAttachmentView(context: context, file: file) - } + }*/ self.updateSendButtonEnabled(isCaption || isAttachment, animated: false) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index a615715632..e817c7c61b 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -834,8 +834,12 @@ public final class ReactionNodePool { private var views: [ReactionButtonAsyncNode] = [] func putBack(view: ReactionButtonAsyncNode) { - view.reset() - self.views.append(view) + assert(view.superview == nil) + + if self.views.count < 64 { + view.reset() + self.views.append(view) + } } func take() -> Item { @@ -892,6 +896,12 @@ public final class ReactionButtonsAsyncLayoutContainer { public init() { } + deinit { + for (_, button) in self.buttons { + button.view.removeFromSuperview() + } + } + public func update( context: AccountContext, action: @escaping (String) -> Void, diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index 99091d9b16..6f7ca8889b 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -24,6 +24,7 @@ import UIKitRuntimeUtils } if let completion = self.completion { completion(flag) + self.completion = nil } } } @@ -83,6 +84,9 @@ public extension CALayer { animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor() animation.fillMode = .both } + if #available(iOS 15.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } return animation } else if timingFunction == kCAMediaTimingFunctionSpring { let animation = makeSpringAnimation(keyPath) @@ -108,6 +112,10 @@ public extension CALayer { animation.fillMode = .both } + if #available(iOS 15.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } + return animation } else { let k = Float(UIView.animationDurationFactor()) @@ -138,6 +146,10 @@ public extension CALayer { animation.fillMode = .both } + if #available(iOS 15.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } + return animation } } @@ -196,6 +208,10 @@ public extension CALayer { animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) } + if #available(iOS 15.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } + self.add(animation, forKey: keyPath) } @@ -225,6 +241,10 @@ public extension CALayer { animation.speed = speed * Float(animation.duration / duration) animation.isAdditive = additive + if #available(iOS 15.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } + return animation } @@ -257,6 +277,10 @@ public extension CALayer { animation.speed = speed * Float(animation.duration / duration) animation.isAdditive = additive + if #available(iOS 15.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } + self.add(animation, forKey: keyPath) } @@ -284,6 +308,10 @@ public extension CALayer { animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) } + if #available(iOS 15.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } + self.add(animation, forKey: key) } diff --git a/submodules/Display/Source/DisplayLinkAnimator.swift b/submodules/Display/Source/DisplayLinkAnimator.swift index 7f2023bd49..ff3239a4b4 100644 --- a/submodules/Display/Source/DisplayLinkAnimator.swift +++ b/submodules/Display/Source/DisplayLinkAnimator.swift @@ -1,14 +1,14 @@ import Foundation import UIKit -private final class DisplayLinkTarget: NSObject { +public final class DisplayLinkTarget: NSObject { private let f: () -> Void - init(_ f: @escaping () -> Void) { + public init(_ f: @escaping () -> Void) { self.f = f } - @objc func event() { + @objc public func event() { self.f() } } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 040c153dd1..ca4750161d 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -376,6 +376,30 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private let waitingForNodesDisposable = MetaDisposable() + private var auxiliaryDisplayLink: CADisplayLink? + private var isAuxiliaryDisplayLinkEnabled: Bool = false { + didSet { + /*if self.isAuxiliaryDisplayLinkEnabled != oldValue { + if self.isAuxiliaryDisplayLinkEnabled { + if self.auxiliaryDisplayLink == nil { + let displayLink = CADisplayLink(target: DisplayLinkTarget({ + }), selector: #selector(DisplayLinkTarget.event)) + if #available(iOS 15.0, *) { + displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } + displayLink.add(to: RunLoop.main, forMode: .common) + self.auxiliaryDisplayLink = displayLink + } + } else { + if let auxiliaryDisplayLink = self.auxiliaryDisplayLink { + self.auxiliaryDisplayLink = nil + auxiliaryDisplayLink.invalidate() + } + } + }*/ + } + } + /*override open var accessibilityElements: [Any]? { get { var accessibilityElements: [Any] = [] @@ -789,6 +813,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.isDeceleratingAfterTracking = true self.updateHeaderItemsFlashing(animated: true) self.resetScrollIndicatorFlashTimer(start: false) + + self.isAuxiliaryDisplayLinkEnabled = true } else { self.isDeceleratingAfterTracking = false self.resetHeaderItemsFlashTimer(start: true) @@ -797,6 +823,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.lastContentOffsetTimestamp = 0.0 self.didEndScrolling?(false) + self.isAuxiliaryDisplayLinkEnabled = false } self.endedInteractiveDragging(self.touchesPosition) } @@ -807,6 +834,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.resetHeaderItemsFlashTimer(start: true) self.updateHeaderItemsFlashing(animated: true) self.resetScrollIndicatorFlashTimer(start: true) + self.isAuxiliaryDisplayLinkEnabled = false if !scrollView.isTracking { self.didEndScrolling?(true) } @@ -3192,6 +3220,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture springAnimation.isRemovedOnCompletion = true springAnimation.isAdditive = true springAnimation.fillMode = CAMediaTimingFillMode.forwards + if #available(iOS 15.0, *) { + springAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } let k = Float(UIView.animationDurationFactor()) var speed: Float = 1.0 @@ -3223,6 +3254,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) basicAnimation.isRemovedOnCompletion = true basicAnimation.isAdditive = true + if #available(iOS 15.0, *) { + basicAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") reverseBasicAnimation.timingFunction = CAMediaTimingFunction(controlPoints: cp1x, cp1y, cp2x, cp2y) @@ -3231,6 +3265,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) reverseBasicAnimation.isRemovedOnCompletion = true reverseBasicAnimation.isAdditive = true + if #available(iOS 15.0, *) { + reverseBasicAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } animation = basicAnimation reverseAnimation = reverseBasicAnimation @@ -3245,6 +3282,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) basicAnimation.isRemovedOnCompletion = true basicAnimation.isAdditive = true + if #available(iOS 15.0, *) { + basicAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") reverseBasicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) @@ -3253,6 +3293,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) reverseBasicAnimation.isRemovedOnCompletion = true reverseBasicAnimation.isAdditive = true + if #available(iOS 15.0, *) { + reverseBasicAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } animation = basicAnimation reverseAnimation = reverseBasicAnimation @@ -3267,6 +3310,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) basicAnimation.isRemovedOnCompletion = true basicAnimation.isAdditive = true + if #available(iOS 15.0, *) { + basicAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") reverseBasicAnimation.timingFunction = ContainedViewLayoutTransitionCurve.slide.mediaTimingFunction @@ -3275,6 +3321,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) reverseBasicAnimation.isRemovedOnCompletion = true reverseBasicAnimation.isAdditive = true + if #available(iOS 15.0, *) { + reverseBasicAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: Float(UIScreen.main.maximumFramesPerSecond), maximum: Float(UIScreen.main.maximumFramesPerSecond), preferred: Float(UIScreen.main.maximumFramesPerSecond)) + } animation = basicAnimation reverseAnimation = reverseBasicAnimation diff --git a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift index 1ac340666c..9ab26c70e4 100644 --- a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift +++ b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift @@ -176,17 +176,6 @@ open class ManagedAnimationNode: ASDisplayNode { self.imageNode.displaysAsynchronously = false self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) - final class DisplayLinkTarget: NSObject { - private let f: () -> Void - - init(_ f: @escaping () -> Void) { - self.f = f - } - - @objc func event() { - self.f() - } - } var displayLinkUpdate: (() -> Void)? self.displayLink = CADisplayLink(target: DisplayLinkTarget { displayLinkUpdate?() diff --git a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift index ec091e971a..61c29e57a6 100644 --- a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift +++ b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift @@ -21,6 +21,38 @@ private func decodeStickerThumbnailData(_ data: Data) -> String { return string } +public func generateStickerPlaceholderImage(data: Data?, size: CGSize, imageSize: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor) -> UIImage? { + return generateImage(size, rotatedContext: { size, context in + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.setBlendMode(.copy) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + } else { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foregroundColor.cgColor) + } + + if let data = data { + var path = decodeStickerThumbnailData(data) + if !path.hasSuffix("z") { + path = "\(path)z" + } + let reader = PathDataReader(input: path) + let segments = reader.read() + + let scale = max(size.width, size.height) / max(imageSize.width, imageSize.height) + context.scaleBy(x: scale, y: scale) + renderPath(segments, context: context) + } else { + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0)) + UIGraphicsPushContext(context) + path.fill() + UIGraphicsPopContext() + } + }) +} + public class StickerShimmerEffectNode: ASDisplayNode { private var backdropNode: ASDisplayNode? private let backgroundNode: ASDisplayNode @@ -84,35 +116,7 @@ public class StickerShimmerEffectNode: ASDisplayNode { 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 - if let backgroundColor = backgroundColor { - context.setFillColor(backgroundColor.cgColor) - context.setBlendMode(.copy) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.clear.cgColor) - } else { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.black.cgColor) - } - - if let data = data { - var path = decodeStickerThumbnailData(data) - if !path.hasSuffix("z") { - path = "\(path)z" - } - let reader = PathDataReader(input: path) - let segments = reader.read() - - let scale = max(size.width, size.height) / max(imageSize.width, imageSize.height) - context.scaleBy(x: scale, y: scale) - renderPath(segments, context: context) - } else { - let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0)) - UIGraphicsPushContext(context) - path.fill() - UIGraphicsPopContext() - } - }) + let image = generateStickerPlaceholderImage(data: data, size: size, imageSize: imageSize, backgroundColor: backgroundColor, foregroundColor: .black) if backgroundColor == nil { self.foregroundNode.image = nil diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index c1a2d97a28..05fc1c813f 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -360,7 +360,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1678949555] = { return Api.InputWebDocument.parse_inputWebDocument($0) } dict[-1625153079] = { return Api.InputWebFileLocation.parse_inputWebFileGeoPointLocation($0) } dict[-1036396922] = { return Api.InputWebFileLocation.parse_inputWebFileLocation($0) } - dict[215516896] = { return Api.Invoice.parse_invoice($0) } + dict[-1197014651] = { return Api.Invoice.parse_invoice($0) } dict[-1059185703] = { return Api.JSONObjectValue.parse_jsonObjectValue($0) } dict[-146520221] = { return Api.JSONValue.parse_jsonArray($0) } dict[-952869270] = { return Api.JSONValue.parse_jsonBool($0) } @@ -808,6 +808,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1135492588] = { return Api.Update.parse_updateStickerSets($0) } dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) } dict[-2112423005] = { return Api.Update.parse_updateTheme($0) } + dict[-2006880112] = { return Api.Update.parse_updateTranscribeAudio($0) } dict[-1007549728] = { return Api.Update.parse_updateUserName($0) } dict[88680979] = { return Api.Update.parse_updateUserPhone($0) } dict[-232290676] = { return Api.Update.parse_updateUserPhoto($0) } @@ -987,7 +988,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[-1077051894] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) } + dict[-1821037486] = { 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) } diff --git a/submodules/TelegramApi/Sources/Api19.swift b/submodules/TelegramApi/Sources/Api19.swift index 75f69500fb..54b86471e6 100644 --- a/submodules/TelegramApi/Sources/Api19.swift +++ b/submodules/TelegramApi/Sources/Api19.swift @@ -599,6 +599,7 @@ public extension Api { case updateStickerSets case updateStickerSetsOrder(flags: Int32, order: [Int64]) case updateTheme(theme: Api.Theme) + case updateTranscribeAudio(flags: Int32, transcriptionId: Int64, text: String) case updateUserName(userId: Int64, firstName: String, lastName: String, username: String) case updateUserPhone(userId: Int64, phone: String) case updateUserPhoto(userId: Int64, date: Int32, photo: Api.UserProfilePhoto, previous: Api.Bool) @@ -1423,6 +1424,14 @@ public extension Api { } theme.serialize(buffer, true) break + case .updateTranscribeAudio(let flags, let transcriptionId, let text): + if boxed { + buffer.appendInt32(-2006880112) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(transcriptionId, buffer: buffer, boxed: false) + serializeString(text, buffer: buffer, boxed: false) + break case .updateUserName(let userId, let firstName, let lastName, let username): if boxed { buffer.appendInt32(-1007549728) @@ -1667,6 +1676,8 @@ public extension Api { return ("updateStickerSetsOrder", [("flags", String(describing: flags)), ("order", String(describing: order))]) case .updateTheme(let theme): return ("updateTheme", [("theme", String(describing: theme))]) + case .updateTranscribeAudio(let flags, let transcriptionId, let text): + return ("updateTranscribeAudio", [("flags", String(describing: flags)), ("transcriptionId", String(describing: transcriptionId)), ("text", String(describing: text))]) case .updateUserName(let userId, let firstName, let lastName, let username): return ("updateUserName", [("userId", String(describing: userId)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("username", String(describing: username))]) case .updateUserPhone(let userId, let phone): @@ -3325,6 +3336,23 @@ public extension Api { return nil } } + public static func parse_updateTranscribeAudio(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateTranscribeAudio(flags: _1!, transcriptionId: _2!, text: _3!) + } + else { + return nil + } + } public static func parse_updateUserName(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 8c14d1edfe..9dadf48c72 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -490,14 +490,15 @@ public extension Api.messages { } public extension Api.messages { enum TranscribedAudio: TypeConstructorDescription { - case transcribedAudio(transcriptionId: Int64, text: String) + case transcribedAudio(flags: Int32, transcriptionId: Int64, text: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .transcribedAudio(let transcriptionId, let text): + case .transcribedAudio(let flags, let transcriptionId, let text): if boxed { - buffer.appendInt32(-1077051894) + buffer.appendInt32(-1821037486) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(transcriptionId, buffer: buffer, boxed: false) serializeString(text, buffer: buffer, boxed: false) break @@ -506,20 +507,23 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .transcribedAudio(let transcriptionId, let text): - return ("transcribedAudio", [("transcriptionId", String(describing: transcriptionId)), ("text", String(describing: text))]) + case .transcribedAudio(let flags, let transcriptionId, let text): + return ("transcribedAudio", [("flags", String(describing: flags)), ("transcriptionId", String(describing: transcriptionId)), ("text", String(describing: text))]) } } public static func parse_transcribedAudio(_ reader: BufferReader) -> TranscribedAudio? { - var _1: Int64? - _1 = reader.readInt64() - var _2: String? - _2 = parseString(reader) + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + _3 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.TranscribedAudio.transcribedAudio(transcriptionId: _1!, text: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.TranscribedAudio.transcribedAudio(flags: _1!, transcriptionId: _2!, text: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 98e4086e03..076af6e651 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -6226,6 +6226,21 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func assignPlayMarketTransaction(purchaseToken: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1336560365) + serializeString(purchaseToken, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.assignPlayMarketTransaction", parameters: [("purchaseToken", String(describing: purchaseToken))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.payments { static func clearSavedInfo(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6319,6 +6334,23 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func requestRecurrentPayment(userId: Int64, recurrentInitCharge: String, invoiceMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1329030023) + serializeInt64(userId, buffer: buffer, boxed: false) + serializeString(recurrentInitCharge, buffer: buffer, boxed: false) + invoiceMedia.serialize(buffer, true) + return (FunctionDescription(name: "payments.requestRecurrentPayment", parameters: [("userId", String(describing: userId)), ("recurrentInitCharge", String(describing: recurrentInitCharge)), ("invoiceMedia", String(describing: invoiceMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.payments { static func sendPaymentForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index e465a3133e..049c138639 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -898,13 +898,13 @@ public extension Api { } public extension Api { enum Invoice: TypeConstructorDescription { - case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?) + case invoice(flags: Int32, currency: String, prices: [Api.LabeledPrice], maxTipAmount: Int64?, suggestedTipAmounts: [Int64]?, recurrentTermsUrl: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts): + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let recurrentTermsUrl): if boxed { - buffer.appendInt32(215516896) + buffer.appendInt32(-1197014651) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -919,14 +919,15 @@ public extension Api { for item in suggestedTipAmounts! { serializeInt64(item, buffer: buffer, boxed: false) }} + if Int(flags) & Int(1 << 8) != 0 {serializeString(recurrentTermsUrl!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts): - return ("invoice", [("flags", String(describing: flags)), ("currency", String(describing: currency)), ("prices", String(describing: prices)), ("maxTipAmount", String(describing: maxTipAmount)), ("suggestedTipAmounts", String(describing: suggestedTipAmounts))]) + case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let recurrentTermsUrl): + return ("invoice", [("flags", String(describing: flags)), ("currency", String(describing: currency)), ("prices", String(describing: prices)), ("maxTipAmount", String(describing: maxTipAmount)), ("suggestedTipAmounts", String(describing: suggestedTipAmounts)), ("recurrentTermsUrl", String(describing: recurrentTermsUrl))]) } } @@ -945,13 +946,16 @@ public extension Api { if Int(_1!) & Int(1 << 8) != 0 {if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } + var _6: String? + if Int(_1!) & Int(1 << 8) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil let _c5 = (Int(_1!) & Int(1 << 8) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5) + let _c6 = (Int(_1!) & Int(1 << 8) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.Invoice.invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5, recurrentTermsUrl: _6) } else { return nil diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 114a9d19cf..cfc92c58db 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1101,6 +1101,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo updatedState.updateMedia(webpage.webpageId, media: webpage) } } + /*case let .updateTranscribeAudio(flags, transcriptionId, text): + break*/ case let .updateNotifySettings(apiPeer, apiNotificationSettings): switch apiPeer { case let .notifyPeer(peer): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AudioTranscriptionMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AudioTranscriptionMessageAttribute.swift index 47e1b00d16..0ca15c1388 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AudioTranscriptionMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AudioTranscriptionMessageAttribute.swift @@ -3,24 +3,28 @@ import Postbox public class AudioTranscriptionMessageAttribute: MessageAttribute, Equatable { public let id: Int64 public let text: String + public let isPending: Bool public var associatedPeerIds: [PeerId] { return [] } - public init(id: Int64, text: String) { + public init(id: Int64, text: String, isPending: Bool) { self.id = id self.text = text + self.isPending = isPending } required public init(decoder: PostboxDecoder) { self.id = decoder.decodeInt64ForKey("id", orElse: 0) self.text = decoder.decodeStringForKey("text", orElse: "") + self.isPending = decoder.decodeBoolForKey("isPending", orElse: false) } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.id, forKey: "id") encoder.encodeString(self.text, forKey: "text") + encoder.encodeBool(self.isPending, forKey: "isPending") } public static func ==(lhs: AudioTranscriptionMessageAttribute, rhs: AudioTranscriptionMessageAttribute) -> Bool { @@ -30,6 +34,9 @@ public class AudioTranscriptionMessageAttribute: MessageAttribute, Equatable { if lhs.text != rhs.text { return false } + if lhs.isPending != rhs.isPending { + return false + } return true } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift index a71eb42664..c7084859e1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift @@ -64,12 +64,14 @@ func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: Me return postbox.transaction { transaction -> EngineAudioTranscriptionResult in switch result { - case let .transcribedAudio(transcriptionId, text): + case let .transcribedAudio(flags, transcriptionId, text): transaction.updateMessage(messageId, update: { currentMessage in let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) var attributes = currentMessage.attributes.filter { !($0 is AudioTranscriptionMessageAttribute) } - attributes.append(AudioTranscriptionMessageAttribute(id: transcriptionId, text: text)) + let isPending = (flags & (1 << 0)) != 0 + + attributes.append(AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending)) return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 16b181d959..3f71144f59 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -124,7 +124,7 @@ public enum BotPaymentFormRequestError { extension BotPaymentInvoice { init(apiInvoice: Api.Invoice) { switch apiInvoice { - case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts): + case let .invoice(flags, currency, prices, maxTipAmount, suggestedTipAmounts, _): var fields = BotPaymentInvoiceFields() if (flags & (1 << 1)) != 0 { fields.insert(.name) diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 77d0498976..819d57826a 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -43,6 +43,7 @@ public protocol AnimationCacheItemWriter: AnyObject { public protocol AnimationCache: AnyObject { func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal + func getSynchronously(sourceId: String) -> AnimationCacheItem? } private func md5Hash(_ string: String) -> String { @@ -375,6 +376,18 @@ public final class AnimationCacheImpl: AnimationCache { } } } + + func getSynchronously(sourceId: String) -> AnimationCacheItem? { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + + if FileManager.default.fileExists(atPath: itemPath) { + return loadItem(path: itemPath) + } else { + return nil + } + } } private let queue: Queue @@ -403,4 +416,10 @@ public final class AnimationCacheImpl: AnimationCache { } |> runOn(self.queue) } + + public func getSynchronously(sourceId: String) -> AnimationCacheItem? { + return self.impl.syncWith { impl -> AnimationCacheItem? in + return impl.getSynchronously(sourceId: sourceId) + } + } } diff --git a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift index bf66270433..b131136b8a 100644 --- a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift +++ b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift @@ -8,7 +8,6 @@ import LottieAnimationComponent public final class AudioTranscriptionButtonComponent: Component { public enum TranscriptionState { - case possible case inProgress case expanded case collapsed @@ -130,8 +129,6 @@ public final class AudioTranscriptionButtonComponent: Component { let animationName: String switch component.transcriptionState { - case .possible: - animationName = "voiceToText" case .inProgress: animationName = "voiceToText" case .collapsed: diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD index 571d0edd31..ff1faa52e0 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD @@ -19,6 +19,10 @@ swift_library( "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/YuvConversion:YuvConversion", "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 37ae264449..700adef954 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -9,103 +9,79 @@ import AccountContext import YuvConversion import TelegramCore import Postbox +import AnimationCache +import LottieAnimationCache +import MultiAnimationRenderer +import ShimmerEffect -private final class InlineStickerItemLayer: SimpleLayer { - static let queue = Queue() +public final class InlineStickerItemLayer: MultiAnimationRenderTarget { + public static let queue = Queue() + + public struct Key: Hashable { + public var id: MediaId + public var index: Int + + public init(id: MediaId, index: Int) { + self.id = id + self.index = index + } + } private let file: TelegramMediaFile - private let source: AnimatedStickerNodeSource - private var frameSource: QueueLocalObject? private var disposable: Disposable? private var fetchDisposable: Disposable? private var isInHierarchyValue: Bool = false - var isVisibleForAnimations: Bool = false { + public var isVisibleForAnimations: Bool = false { didSet { - self.updatePlayback() + if self.isVisibleForAnimations != oldValue { + self.updatePlayback() + } } } private var displayLink: ConstantDisplayLinkAnimator? - init(context: AccountContext, file: TelegramMediaFile) { - self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { self.file = file super.init() - let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) - let width = Int(24 * UIScreenScale) - let height = Int(24 * UIScreenScale) - - let directDataPath = Atomic(value: nil) - let _ = (self.source.directDataPath(attemptSynchronously: true) |> take(1)).start(next: { result in - let _ = directDataPath.swap(result) - }) - - if let directDataPath = directDataPath.with({ $0 }), let directData = try? Data(contentsOf: URL(fileURLWithPath: directDataPath), options: .alwaysMapped) { - let syncFrameSource = AnimatedStickerDirectFrameSource(queue: .mainQueue(), data: directData, width: width, height: height, cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)! - - if let animationFrame = syncFrameSource.takeFrame(draw: true) { - var image: UIImage? - - autoreleasepool { - image = generateImagePixel(CGSize(width: CGFloat(animationFrame.width), height: CGFloat(animationFrame.height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in - var data = animationFrame.data - data.withUnsafeMutableBytes { bytes -> Void in - guard let baseAddress = bytes.baseAddress else { - return - } - switch animationFrame.type { - case .argb: - memcpy(pixelData, baseAddress.assumingMemoryBound(to: UInt8.self), bytes.count) - case .yuva: - if animationFrame.bytesPerRow <= 0 || animationFrame.height <= 0 || animationFrame.width <= 0 || animationFrame.bytesPerRow * animationFrame.height > bytes.count { - assert(false) - return - } - decodeYUVAToRGBA(baseAddress.assumingMemoryBound(to: UInt8.self), pixelData, Int32(animationFrame.width), Int32(animationFrame.height), Int32(contextBytesPerRow)) - default: - break - } - } - }) - } - - if let image = image { + if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation) { + let size = CGSize(width: 24.0, height: 24.0) + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { self.contents = image.cgImage } } } - self.disposable = (self.source.directDataPath(attemptSynchronously: false) - |> filter { $0 != nil } - |> take(1) - |> deliverOn(InlineStickerItemLayer.queue)).start(next: { [weak self] path in - guard let directData = try? Data(contentsOf: URL(fileURLWithPath: path!), options: [.mappedRead]) else { - return - } - Queue.mainQueue().async { - guard let strongSelf = self else { + self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, fetch: { writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { return } - strongSelf.frameSource = QueueLocalObject(queue: InlineStickerItemLayer.queue, generate: { - return AnimatedStickerDirectFrameSource(queue: InlineStickerItemLayer.queue, data: directData, width: width, height: height, cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)! - }) - strongSelf.updatePlayback() + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + let scale = min(2.0, UIScreenScale) + cacheLottieAnimation(data: data, width: Int(24 * scale), height: Int(24 * scale), writer: writer) + }) + + let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() } }) - - self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() } - override init(layer: Any) { - guard let layer = layer as? InlineStickerItemLayer else { - preconditionFailure() - } - self.source = layer.source - self.file = layer.file - - super.init(layer: layer) + override public init(layer: Any) { + preconditionFailure() } required public init?(coder: NSCoder) { @@ -117,7 +93,7 @@ private final class InlineStickerItemLayer: SimpleLayer { self.fetchDisposable?.dispose() } - override func action(forKey event: String) -> CAAction? { + override public func action(forKey event: String) -> CAAction? { if event == kCAOnOrderIn { self.isInHierarchyValue = true } else if event == kCAOnOrderOut { @@ -128,72 +104,17 @@ private final class InlineStickerItemLayer: SimpleLayer { } private func updatePlayback() { - let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations && self.frameSource != nil - if shouldBePlaying != (self.displayLink != nil) { - if shouldBePlaying { - self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in - self?.loadNextFrame() - }) - self.displayLink?.isPaused = false - } else { - self.displayLink?.invalidate() - self.displayLink = nil - } - } - } - - private var didRequestFrame = false - - private func loadNextFrame() { - guard let frameSource = self.frameSource else { - return - } - self.didRequestFrame = true - frameSource.with { [weak self] impl in - if let animationFrame = impl.takeFrame(draw: true) { - var image: UIImage? - - autoreleasepool { - image = generateImagePixel(CGSize(width: CGFloat(animationFrame.width), height: CGFloat(animationFrame.height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in - var data = animationFrame.data - data.withUnsafeMutableBytes { bytes -> Void in - guard let baseAddress = bytes.baseAddress else { - return - } - switch animationFrame.type { - case .argb: - memcpy(pixelData, baseAddress.assumingMemoryBound(to: UInt8.self), bytes.count) - case .yuva: - if animationFrame.bytesPerRow <= 0 || animationFrame.height <= 0 || animationFrame.width <= 0 || animationFrame.bytesPerRow * animationFrame.height > bytes.count { - assert(false) - return - } - decodeYUVAToRGBA(baseAddress.assumingMemoryBound(to: UInt8.self), pixelData, Int32(animationFrame.width), Int32(animationFrame.height), Int32(contextBytesPerRow)) - default: - break - } - } - }) - } - - if let image = image { - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - strongSelf.contents = image.cgImage - } - } - } - } + let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations + + self.shouldBeAnimating = shouldBePlaying } } public final class EmojiTextAttachmentView: UIView { private let contentLayer: InlineStickerItemLayer - public init(context: AccountContext, file: TelegramMediaFile) { - self.contentLayer = InlineStickerItemLayer(context: context, file: file) + public init(context: AccountContext, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) { + self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor) super.init(frame: CGRect()) diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index e7b404962e..97fb1cee2c 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -6,6 +6,7 @@ import AnimationCache public protocol MultiAnimationRenderer: AnyObject { func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable + func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool } open class MultiAnimationRenderTarget: SimpleLayer { @@ -159,6 +160,19 @@ private final class ItemAnimationContext { self.displayLink?.invalidate() } + func updateAddedTarget(target: MultiAnimationRenderTarget) { + if let item = self.item, let currentFrameGroup = self.currentFrameGroup { + let currentFrame = self.frameIndex % item.numFrames + + if let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { + target.contents = currentFrameGroup.image.cgImage + target.contentsRect = contentsRect + } + } + + self.updateIsPlaying() + } + func updateIsPlaying() { var isPlaying = true if self.item == nil { @@ -268,6 +282,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } let index = itemContext.targets.add(Weak(target)) + itemContext.updateAddedTarget(target: target) let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in Queue.mainQueue().async { @@ -303,6 +318,20 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool { + if let item = cache.getSynchronously(sourceId: itemId) { + guard let frameGroup = FrameGroup(item: item, baseFrameIndex: 0, count: 1, skip: 1) else { + return false + } + + target.contents = frameGroup.image.cgImage + + return true + } else { + return false + } + } + private func updateIsPlaying() { var isPlaying = false for (_, itemContext) in self.itemContexts { @@ -380,6 +409,23 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } + public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool { + let groupContext: GroupContext + if let current = self.groupContexts[groupId] { + groupContext = current + } else { + groupContext = GroupContext(stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContexts[groupId] = groupContext + } + + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId) + } + private func updateIsPlaying() { var isPlaying = false for (_, groupContext) in self.groupContexts { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1ea74ebed1..92e944b37a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -972,7 +972,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = combineLatest(queue: .mainQueue(), strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)), - contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), + contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction, messageNode: node as? ChatMessageItemView), strongSelf.context.engine.stickers.availableReactions(), peerAllowedReactions(context: strongSelf.context, peerId: topMessage.id.peerId), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 96c4c3bffe..3969092771 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2473,7 +2473,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var attributes: [MessageAttribute] = [] attributes.append(ForwardOptionsMessageAttribute(hideNames: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideNames == true, hideCaptions: self.chatPresentationInterfaceState.interfaceState.forwardOptionsState?.hideCaptions == true)) - for id in forwardMessageIds { + for id in forwardMessageIds.sorted() { messages.append(.forward(source: id, grouping: .auto, attributes: attributes, correlationId: nil)) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 9d51e3ff66..2e95f80d98 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -294,14 +294,16 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } } - if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && chatPresentationInterfaceState.interfaceState.forwardMessageIds == nil { - if chatPresentationInterfaceState.hasScheduledMessages { + let isTextEmpty = chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 + + if chatPresentationInterfaceState.interfaceState.forwardMessageIds == nil { + if isTextEmpty && chatPresentationInterfaceState.hasScheduledMessages { accessoryItems.append(.scheduledMessages) } var stickersEnabled = true if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel { - if case .broadcast = peer.info, canSendMessagesToPeer(peer) { + if isTextEmpty, case .broadcast = peer.info, canSendMessagesToPeer(peer) { accessoryItems.append(.silentPost(chatPresentationInterfaceState.interfaceState.silentPosting)) } if peer.hasBannedPermission(.banSendStickers) != nil { @@ -312,11 +314,17 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte stickersEnabled = false } } - if chatPresentationInterfaceState.hasBots && chatPresentationInterfaceState.hasBotCommands { + if isTextEmpty && chatPresentationInterfaceState.hasBots && chatPresentationInterfaceState.hasBotCommands { accessoryItems.append(.commands) } + #if DEBUG accessoryItems.append(.stickers(stickersEnabled)) - if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { + #else + if isTextEmpty { + accessoryItems.append(.stickers(stickersEnabled)) + } + #endif + if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { accessoryItems.append(.inputButtons) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 9b82a2f2ec..7a4a221538 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -374,10 +374,15 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState, return updated } -func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil) -> Signal { +func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil, messageNode: ChatMessageItemView? = nil) -> Signal { guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { return .single(ContextController.Items(content: .list([]))) } + + var hasExpandedAudioTranscription = false + if let messageNode = messageNode as? ChatMessageBubbleItemNode { + hasExpandedAudioTranscription = messageNode.hasExpandedAudioTranscription() + } if messages.count == 1, let _ = messages[0].adAttribute { let message = messages[0] @@ -704,7 +709,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } var hasRateTranscription = false - if let audioTranscription = audioTranscription { + if hasExpandedAudioTranscription, let audioTranscription = audioTranscription { hasRateTranscription = true actions.insert(.custom(ChatRateTranscriptionContextItem(context: context, message: message, action: { [weak context] value in guard let context = context else { @@ -812,7 +817,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } for attribute in message.attributes { - if let attribute = attribute as? AudioTranscriptionMessageAttribute { + if hasExpandedAudioTranscription, let attribute = attribute as? AudioTranscriptionMessageAttribute { if !messageText.isEmpty { messageText.append("\n") } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 8cd4ae2e57..f6afb54426 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -4026,4 +4026,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } return nil } + + func hasExpandedAudioTranscription() -> Bool { + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageFileBubbleContentNode { + return contentNode.interactiveFileNode.hasExpandedAudioTranscription + } + } + return false + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index d3cbce7279..be2fc20662 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -839,8 +839,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { for node in reactionButtons.removedNodes { if animation.isAnimated { node.view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in - node?.view.removeFromSuperview() + node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + node.view.removeFromSuperview() }) } else { node.view.removeFromSuperview() diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 89b0ba633e..05e542a239 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -30,6 +30,17 @@ private struct FetchControls { let cancel: () -> Void } +private func transcribedText(message: Message) -> EngineAudioTranscriptionResult? { + for attribute in message.attributes { + if let attribute = attribute as? AudioTranscriptionMessageAttribute { + if !attribute.text.isEmpty || !attribute.isPending { + return .success(EngineAudioTranscriptionResult.Success(id: attribute.id, text: attribute.text)) + } + } + } + return nil +} + final class ChatMessageInteractiveFileNode: ASDisplayNode { final class Arguments { let context: AccountContext @@ -174,9 +185,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { private var streamingCacheStatusFrame: CGRect? private var fileIconImage: UIImage? - private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .possible - private var transcribedText: EngineAudioTranscriptionResult? + private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed private var transcribeDisposable: Disposable? + var hasExpandedAudioTranscription: Bool { + if case .expanded = audioTranscriptionState { + return true + } else { + return false + } + } override init() { self.titleNode = TextNode() @@ -306,17 +323,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return } - if self.transcribedText == nil { - for attribute in message.attributes { - if let attribute = attribute as? AudioTranscriptionMessageAttribute { - self.transcribedText = .success(EngineAudioTranscriptionResult.Success(id: attribute.id, text: attribute.text)) - self.audioTranscriptionState = .collapsed - break - } - } - } - - if self.transcribedText == nil { + if transcribedText(message: message) == nil { if self.transcribeDisposable == nil { self.audioTranscriptionState = .inProgress self.requestUpdateLayout(true) @@ -361,7 +368,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return } strongSelf.transcribeDisposable = nil - if let result = result { + /*if let result = result { strongSelf.transcribedText = .success(EngineAudioTranscriptionResult.Success(id: 0, text: result)) } else { strongSelf.transcribedText = .error @@ -371,7 +378,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } else { strongSelf.audioTranscriptionState = .collapsed } - strongSelf.requestUpdateLayout(true) + strongSelf.requestUpdateLayout(true)*/ }) } else { self.transcribeDisposable = (context.engine.messages.transcribeAudio(messageId: message.id) @@ -380,9 +387,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return } strongSelf.transcribeDisposable = nil - strongSelf.audioTranscriptionState = .expanded + /*strongSelf.audioTranscriptionState = .expanded strongSelf.transcribedText = result - strongSelf.requestUpdateLayout(true) + strongSelf.requestUpdateLayout(true)*/ }) } } @@ -410,7 +417,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let statusLayout = self.dateAndStatusNode.asyncLayout() let currentMessage = self.message - let transcribedText = self.transcribedText let audioTranscriptionState = self.audioTranscriptionState return { arguments in @@ -585,7 +591,22 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let descriptionMaxWidth = max(descriptionLayout.size.width, descriptionMeasuringLayout.size.width) let textFont = arguments.presentationData.messageFont let textString: NSAttributedString? - if let transcribedText = transcribedText, case .expanded = audioTranscriptionState { + var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState? + + let transcribedText = transcribedText(message: arguments.message) + + switch audioTranscriptionState { + case .inProgress: + if transcribedText != nil { + updatedAudioTranscriptionState = .expanded + } + default: + break + } + + let effectiveAudioTranscriptionState = updatedAudioTranscriptionState ?? audioTranscriptionState + + if let transcribedText = transcribedText, case .expanded = effectiveAudioTranscriptionState { switch transcribedText { case let .success(success): textString = NSAttributedString(string: success.text, font: textFont, textColor: messageTheme.primaryTextColor) @@ -794,6 +815,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.descriptionNode.frame = descriptionFrame strongSelf.descriptionMeasuringNode.frame = CGRect(origin: CGPoint(), size: descriptionMeasuringLayout.size) + if let updatedAudioTranscriptionState = updatedAudioTranscriptionState { + strongSelf.audioTranscriptionState = updatedAudioTranscriptionState + } + if let consumableContentIcon = consumableContentIcon { if strongSelf.consumableContentNode.supernode == nil { strongSelf.addSubnode(strongSelf.consumableContentNode) @@ -970,7 +995,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } var isTranscriptionInProgress = false - if case .inProgress = audioTranscriptionState { + if case .inProgress = effectiveAudioTranscriptionState { isTranscriptionInProgress = true } @@ -1009,7 +1034,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { 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, + transcriptionState: effectiveAudioTranscriptionState, pressed: { guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index c23a0ec318..69430dcc02 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -71,6 +71,10 @@ final class MessageReactionButtonsNode: ASDisplayNode { super.init() } + deinit { + + } + func update() { } @@ -365,8 +369,8 @@ final class MessageReactionButtonsNode: ASDisplayNode { for node in reactionButtons.removedNodes { if animation.isAnimated { node.view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in - node?.view.removeFromSuperview() + node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + node.view.removeFromSuperview() }) } else { node.view.removeFromSuperview() diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index c2a30e2735..d41b57d92a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -18,6 +18,7 @@ import YuvConversion import AnimationCache import LottieAnimationCache import MultiAnimationRenderer +import EmojiTextAttachmentView private final class CachedChatMessageText { let text: String @@ -64,167 +65,6 @@ private final class InlineStickerItem: Hashable { } } -private final class InlineStickerItemLayer: MultiAnimationRenderTarget { - static let queue = Queue() - - struct Key: Hashable { - var id: MediaId - var index: Int - } - - private let file: TelegramMediaFile - //private var frameSource: QueueLocalObject? - private var disposable: Disposable? - private var fetchDisposable: Disposable? - - private var isInHierarchyValue: Bool = false - var isVisibleForAnimations: Bool = false { - didSet { - if self.isVisibleForAnimations != oldValue { - self.updatePlayback() - } - } - } - private var displayLink: ConstantDisplayLinkAnimator? - - init(context: AccountContext, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer) { - self.file = file - - super.init() - - self.disposable = renderer.add(groupId: "inlineEmoji", target: self, cache: cache, itemId: file.resource.id.stringRepresentation, fetch: { writer in - let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - - let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in - guard let result = result else { - return - } - - guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { - writer.finish() - return - } - let scale = min(2.0, UIScreenScale) - cacheLottieAnimation(data: data, width: Int(24 * scale), height: Int(24 * scale), writer: writer) - }) - - let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() - } - }) - - /*let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) - - self.disposable = (self.source.directDataPath(attemptSynchronously: false) - |> filter { $0 != nil } - |> take(1) - |> deliverOn(InlineStickerItemLayer.queue)).start(next: { [weak self] path in - guard let directData = try? Data(contentsOf: URL(fileURLWithPath: path!), options: [.mappedRead]) else { - return - } - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - strongSelf.frameSource = QueueLocalObject(queue: InlineStickerItemLayer.queue, generate: { - return AnimatedStickerDirectFrameSource(queue: InlineStickerItemLayer.queue, data: directData, width: Int(24 * UIScreenScale), height: Int(24 * UIScreenScale), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)! - }) - strongSelf.updatePlayback() - } - }) - - self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()*/ - } - - override init(layer: Any) { - preconditionFailure() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.disposable?.dispose() - self.fetchDisposable?.dispose() - } - - override func action(forKey event: String) -> CAAction? { - if event == kCAOnOrderIn { - self.isInHierarchyValue = true - } else if event == kCAOnOrderOut { - self.isInHierarchyValue = false - } - self.updatePlayback() - return nullAction - } - - private func updatePlayback() { - let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations - - self.shouldBeAnimating = shouldBePlaying - - /*if shouldBePlaying != (self.displayLink != nil) { - if shouldBePlaying { - self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in - self?.loadNextFrame() - }) - self.displayLink?.isPaused = false - } else { - self.displayLink?.invalidate() - self.displayLink = nil - } - }*/ - } - - /*private func loadNextFrame() { - guard let frameSource = self.frameSource else { - return - } - self.didRequestFrame = true - frameSource.with { [weak self] impl in - if let animationFrame = impl.takeFrame(draw: true) { - var image: UIImage? - - autoreleasepool { - image = generateImagePixel(CGSize(width: CGFloat(animationFrame.width), height: CGFloat(animationFrame.height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in - var data = animationFrame.data - data.withUnsafeMutableBytes { bytes -> Void in - guard let baseAddress = bytes.baseAddress else { - return - } - switch animationFrame.type { - case .argb: - memcpy(pixelData, baseAddress.assumingMemoryBound(to: UInt8.self), bytes.count) - case .yuva: - if animationFrame.bytesPerRow <= 0 || animationFrame.height <= 0 || animationFrame.width <= 0 || animationFrame.bytesPerRow * animationFrame.height > bytes.count { - assert(false) - return - } - decodeYUVAToRGBA(baseAddress.assumingMemoryBound(to: UInt8.self), pixelData, Int32(animationFrame.width), Int32(animationFrame.height), Int32(contextBytesPerRow)) - default: - break - } - } - }) - } - - if let image = image { - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - strongSelf.contents = image.cgImage - } - } - } - } - }*/ -} - class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private var spoilerTextNode: TextNode? @@ -723,7 +563,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.textAccessibilityOverlayNode.frame = textFrame strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout - strongSelf.updateInlineStickers(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, textLayout: textLayout) + strongSelf.updateInlineStickers(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, textLayout: textLayout, placeholderColor: messageTheme.mediaPlaceholderColor) if let statusSizeAndApply = statusSizeAndApply { animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil) @@ -754,7 +594,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?) { + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { var nextIndexById: [MediaId: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] @@ -775,7 +615,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let current = self.inlineStickerItemLayers[id] { itemLayer = current } else { - itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file, cache: cache, renderer: renderer) + itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) itemLayer.isVisibleForAnimations = self.isVisibleForAnimations diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 4e4fbe4a86..a35e0bedd8 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -667,12 +667,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputBackgroundNode.isUserInteractionEnabled = true self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) - self.emojiViewProvider = { [weak self] emoji in - guard let strongSelf = self, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else { - return UIView() + if let presentationContext = presentationContext { + self.emojiViewProvider = { [weak self, weak presentationContext] emoji in + guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else { + return UIView() + } + + return EmojiTextAttachmentView(context: context, file: file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12)) } - - return EmojiTextAttachmentView(context: context, file: file) } } @@ -1715,7 +1717,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.slowmodePlaceholderNode?.isHidden = true } - var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight) + var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: minimalHeight - textFieldInsets.bottom - minimalInputHeight) for (_, button) in self.accessoryItemButtons.reversed() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) button.updateLayout(size: buttonSize) diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index ce7c1a9681..df1438bfce 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -90,6 +90,23 @@ static bool notyfyingShiftState = false; @end +@interface CADisplayLink (FrameRateRangeOverride) + +- (void)_65087dc8_setPreferredFrameRateRange:(CAFrameRateRange)range API_AVAILABLE(ios(15.0)); + +@end + +@implementation CADisplayLink (FrameRateRangeOverride) + +- (void)_65087dc8_setPreferredFrameRateRange:(CAFrameRateRange)range API_AVAILABLE(ios(15.0)) { + float maxFps = [UIScreen mainScreen].maximumFramesPerSecond; + range = CAFrameRateRangeMake(maxFps, maxFps, maxFps); + + [self _65087dc8_setPreferredFrameRateRange:range]; +} + +@end + @implementation UIViewController (Navigation) + (void)load @@ -105,6 +122,10 @@ static bool notyfyingShiftState = false; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentingViewController) newSelector:@selector(_65087dc8_presentingViewController)]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentViewController:animated:completion:) newSelector:@selector(_65087dc8_presentViewController:animated:completion:)]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(setNeedsStatusBarAppearanceUpdate) newSelector:@selector(_65087dc8_setNeedsStatusBarAppearanceUpdate)]; + + if (@available(iOS 15.0, *)) { + [RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)]; + } }); } diff --git a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift index 4c2c6a6f47..e127f45cec 100644 --- a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift @@ -150,18 +150,6 @@ final class MetalWallpaperBackgroundNode: ASDisplayNode, WallpaperBackgroundNode 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