mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Animation and other improvements
This commit is contained in:
parent
0d38a9bd08
commit
bacd88a1ff
@ -12,7 +12,7 @@ public func freeMediaFileInteractiveFetched(account: Account, fileReference: Fil
|
||||
public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority) -> Signal<Void, NoError> {
|
||||
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<Int>), 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<Int>), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil)
|
||||
}
|
||||
|
||||
public func freeMediaFileResourceInteractiveFetched(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal<FetchResourceSourceType, FetchResourceError> {
|
||||
@ -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<Int>), userInitiated: Bool, priority: FetchManagerPriority) -> Signal<Void, NoError> {
|
||||
public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: IndexSet = IndexSet(integersIn: 0 ..< Int(Int64.max) as Range<Int>), userInitiated: Bool, priority: FetchManagerPriority) -> Signal<Void, NoError> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -8,11 +8,29 @@
|
||||
//
|
||||
|
||||
#import <AsyncDisplayKit/ASTextKitComponents.h>
|
||||
#import "ASTextKitContext.h"
|
||||
#import <AsyncDisplayKit/ASAssert.h>
|
||||
#import <AsyncDisplayKit/ASMainThreadDeallocation.h>
|
||||
|
||||
#import <tgmath.h>
|
||||
|
||||
@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];
|
||||
|
||||
|
||||
@ -50,4 +50,8 @@ AS_SUBCLASSING_RESTRICTED
|
||||
|
||||
@end
|
||||
|
||||
@interface ASCustomTextContainer : NSTextContainer
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -6226,6 +6226,21 @@ public extension Api.functions.payments {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.payments {
|
||||
static func assignPlayMarketTransaction(purchaseToken: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
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<Api.Bool>) {
|
||||
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<Api.Updates>) {
|
||||
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<Api.payments.PaymentResult>) {
|
||||
let buffer = Buffer()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -43,6 +43,7 @@ public protocol AnimationCacheItemWriter: AnyObject {
|
||||
|
||||
public protocol AnimationCache: AnyObject {
|
||||
func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal<AnimationCacheItem?, NoError>
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<AnimatedStickerDirectFrameSource>?
|
||||
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<String?>(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())
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ContextController.Items, NoError> {
|
||||
func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil, messageNode: ChatMessageItemView? = nil) -> Signal<ContextController.Items, NoError> {
|
||||
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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<AnimatedStickerDirectFrameSource>?
|
||||
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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:)];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user