From 9963c7794ac5c5bfd529a60ae428d88c6242a828 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 10 May 2022 17:51:56 +0400 Subject: [PATCH] Inline animation --- .../Sources/AnimatedStickerFrameSource.swift | 16 +- submodules/Display/Source/TextNode.swift | 93 ++++++- .../ChatMessageTextBubbleContentNode.swift | 231 +++++++++++++++++- 3 files changed, 320 insertions(+), 20 deletions(-) diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift index f512243712..e8f295c764 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift @@ -562,23 +562,23 @@ private final class AnimatedStickerDirectFrameSourceCache { } -final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource { +public final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource { private let queue: Queue private let data: Data private let width: Int private let height: Int private let cache: AnimatedStickerDirectFrameSourceCache? private let bytesPerRow: Int - let frameCount: Int - let frameRate: Int + public let frameCount: Int + public let frameRate: Int fileprivate var currentFrame: Int private let animation: LottieInstance - var frameIndex: Int { + public var frameIndex: Int { return self.currentFrame % self.frameCount } - init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, useMetalCache: Bool = false, fitzModifier: EmojiFitzModifier?) { + public init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, useMetalCache: Bool = false, fitzModifier: EmojiFitzModifier?) { self.queue = queue self.data = data self.width = width @@ -604,7 +604,7 @@ final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource { assert(self.queue.isCurrent()) } - func takeFrame(draw: Bool) -> AnimatedStickerFrame? { + public func takeFrame(draw: Bool) -> AnimatedStickerFrame? { let frameIndex = self.currentFrame % self.frameCount self.currentFrame += 1 if draw { @@ -630,11 +630,11 @@ final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource { } } - func skipToEnd() { + public func skipToEnd() { self.currentFrame = self.frameCount - 1 } - func skipToFrameIndex(_ index: Int) { + public func skipToFrameIndex(_ index: Int) { self.currentFrame = index } } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 64f1cb3858..fed1774a06 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -25,6 +25,19 @@ private final class TextNodeSpoiler { } } + +private final class TextNodeEmbeddedItem { + let range: NSRange + let frame: CGRect + let item: AnyHashable + + init(range: NSRange, frame: CGRect, item: AnyHashable) { + self.range = range + self.frame = frame + self.item = item + } +} + public struct TextRangeRectEdge: Equatable { public var x: CGFloat public var y: CGFloat @@ -45,8 +58,9 @@ private final class TextNodeLine { let strikethroughs: [TextNodeStrikethrough] let spoilers: [TextNodeSpoiler] let spoilerWords: [TextNodeSpoiler] + let embeddedItems: [TextNodeEmbeddedItem] - init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler]) { + init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem]) { self.line = line self.frame = frame self.range = range @@ -54,6 +68,7 @@ private final class TextNodeLine { self.strikethroughs = strikethroughs self.spoilers = spoilers self.spoilerWords = spoilerWords + self.embeddedItems = embeddedItems } } @@ -153,6 +168,31 @@ public final class TextNodeLayoutArguments { } public final class TextNodeLayout: NSObject { + public final class EmbeddedItem: Equatable { + public let range: NSRange + public let rect: CGRect + public let value: AnyHashable + + public init(range: NSRange, rect: CGRect, value: AnyHashable) { + self.range = range + self.rect = rect + self.value = value + } + + public static func ==(lhs: EmbeddedItem, rhs: EmbeddedItem) -> Bool { + if lhs.range != rhs.range { + return false + } + if lhs.rect != rhs.rect { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + } + public let attributedString: NSAttributedString? fileprivate let maximumNumberOfLines: Int fileprivate let truncationType: CTLineTruncationType @@ -163,7 +203,7 @@ public final class TextNodeLayout: NSObject { fileprivate let verticalAlignment: TextVerticalAlignment fileprivate let lineSpacing: CGFloat fileprivate let cutout: TextNodeCutout? - fileprivate let insets: UIEdgeInsets + public let insets: UIEdgeInsets public let size: CGSize public let rawTextSize: CGSize public let truncated: Bool @@ -177,6 +217,7 @@ public final class TextNodeLayout: NSObject { public let hasRTL: Bool public let spoilers: [(NSRange, CGRect)] public let spoilerWords: [(NSRange, CGRect)] + public let embeddedItems: [TextNodeLayout.EmbeddedItem] fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) { self.attributedString = attributedString @@ -203,6 +244,7 @@ public final class TextNodeLayout: NSObject { var hasRTL = false var spoilers: [(NSRange, CGRect)] = [] var spoilerWords: [(NSRange, CGRect)] = [] + var embeddedItems: [TextNodeLayout.EmbeddedItem] = [] for line in lines { if line.isRTL { hasRTL = true @@ -218,10 +260,14 @@ public final class TextNodeLayout: NSObject { spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) }) spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) }) + for embeddedItem in line.embeddedItems { + embeddedItems.append(TextNodeLayout.EmbeddedItem(range: embeddedItem.range, rect: embeddedItem.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY), value: embeddedItem.item)) + } } self.hasRTL = hasRTL self.spoilers = spoilers self.spoilerWords = spoilerWords + self.embeddedItems = embeddedItems } public func areLinesEqual(to other: TextNodeLayout) -> Bool { @@ -971,6 +1017,7 @@ public class TextNode: ASDisplayNode { var strikethroughs: [TextNodeStrikethrough] = [] var spoilers: [TextNodeSpoiler] = [] var spoilerWords: [TextNodeSpoiler] = [] + var embeddedItems: [TextNodeEmbeddedItem] = [] var lineConstrainedWidth = constrainedSize.width var lineConstrainedWidthDelta: CGFloat = 0.0 @@ -1028,6 +1075,24 @@ public class TextNode: ASDisplayNode { spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) } + func addEmbeddedItem(item: AnyHashable, line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) + } + var isLastLine = false if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { isLastLine = true @@ -1116,6 +1181,12 @@ public class TextNode: ASDisplayNode { } addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1123,7 +1194,6 @@ public class TextNode: ASDisplayNode { strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight))) } else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle { headIndent = paragraphStyle.headIndent - } } } @@ -1146,7 +1216,7 @@ public class TextNode: ASDisplayNode { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems)) break } else { if lineCharacterCount > 0 { @@ -1198,6 +1268,12 @@ public class TextNode: ASDisplayNode { } addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) + } else if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { + var ascent: CGFloat = 0.0 + var descent: CGFloat = 0.0 + CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil) + + addEmbeddedItem(item: embeddedItem, line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1226,7 +1302,7 @@ public class TextNode: ASDisplayNode { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: embeddedItems)) } else { if !lines.isEmpty { layoutSize.height += fontLineSpacing @@ -1289,9 +1365,6 @@ public class TextNode: ASDisplayNode { var clearRects: [CGRect] = [] if let layout = parameters as? TextNodeLayout { - if (layout.attributedString?.string ?? "").hasPrefix("Д") { - print() - } if !isRasterizing || layout.backgroundColor != nil { context.setBlendMode(.copy) context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor) @@ -1803,7 +1876,7 @@ open class TextView: UIView { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [])) break } else { if lineCharacterCount > 0 { @@ -1883,7 +1956,7 @@ open class TextView: UIView { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords, embeddedItems: [])) } else { if !lines.isEmpty { layoutSize.height += fontLineSpacing diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index a24ae12be0..0998f6617b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -9,6 +9,12 @@ import UrlEscaping import TelegramUniversalVideoContent import TextSelectionNode import InvisibleInkDustNode +import Emoji +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import SwiftSignalKit +import AccountContext +import YuvConversion private final class CachedChatMessageText { let text: String @@ -36,6 +42,154 @@ private final class CachedChatMessageText { } } +private final class InlineStickerItem: Hashable { + let file: TelegramMediaFile + + init(file: TelegramMediaFile) { + self.file = file + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.file.fileId) + } + + static func ==(lhs: InlineStickerItem, rhs: InlineStickerItem) -> Bool { + if lhs.file.fileId != rhs.file.fileId { + return false + } + return true + } +} + +private final class InlineStickerItemLayer: SimpleLayer { + static let queue = Queue() + + struct Key: Hashable { + var id: MediaId + var index: Int + } + + private let file: TelegramMediaFile + private let source: AnimatedStickerNodeSource + private var frameSource: QueueLocalObject? + private var disposable: Disposable? + + private var isInHierarchy: Bool = false + private var displayLink: ConstantDisplayLinkAnimator? + + init(context: AccountContext, file: TelegramMediaFile) { + self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + self.file = file + + super.init() + + let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + + self.disposable = (self.source.directDataPath() + |> 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() + } + }) + } + + override init(layer: Any) { + guard let layer = layer as? InlineStickerItemLayer else { + preconditionFailure() + } + self.source = layer.source + self.file = layer.file + + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + } + + override func action(forKey event: String) -> CAAction? { + if event == kCAOnOrderIn { + self.isInHierarchy = true + } else if event == kCAOnOrderOut { + self.isInHierarchy = false + } + self.updatePlayback() + return nullAction + } + + private func updatePlayback() { + let shouldBePlaying = self.isInHierarchy && 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 func loadNextFrame() { + guard let frameSource = self.frameSource else { + return + } + 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? @@ -47,6 +201,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private var textSelectionNode: TextSelectionNode? private var textHighlightingNodes: [LinkHighlightingNode] = [] + private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] private var cachedChatMessageText: CachedChatMessageText? @@ -179,7 +334,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let rawText: String - let attributedText: NSAttributedString + var attributedText: NSAttributedString var messageEntities: [MessageTextEntity]? var mediaDuration: Double? = nil @@ -281,6 +436,34 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor) } + #if DEBUG + let updatedString = NSMutableAttributedString(attributedString: attributedText) + while true { + var hadUpdates = false + updatedString.string.enumerateSubstrings(in: updatedString.string.startIndex ..< updatedString.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in + if let substring = substring { + let emoji = substring.basicEmoji.0 + + var emojiFile: TelegramMediaFile? + emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file + if emojiFile == nil { + emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file + } + + if let emojiFile = emojiFile { + updatedString.replaceCharacters(in: NSRange(substringRange, in: updatedString.string), with: NSAttributedString(string: "[..]", attributes: [NSAttributedString.Key("Attribute__EmbeddedItem"): InlineStickerItem(file: emojiFile), NSAttributedString.Key.foregroundColor: UIColor.clear.cgColor])) + hadUpdates = true + stop = true + } + } + } + if !hadUpdates { + break + } + } + attributedText = updatedString + #endif + let cutout: TextNodeCutout? = nil let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0) @@ -386,7 +569,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let _ = textApply() animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil) - //strongSelf.textNode.frame = textFrame if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { let spoilerTextNode = spoilerTextApply() @@ -434,6 +616,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.textAccessibilityOverlayNode.frame = textFrame strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout + strongSelf.updateInlineStickers(context: item.context, textLayout: textLayout) + 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) if strongSelf.statusNode.supernode == nil { @@ -463,6 +647,49 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + private func updateInlineStickers(context: AccountContext, textLayout: TextNodeLayout?) { + var nextIndexById: [MediaId: Int] = [:] + var validIds: [InlineStickerItemLayer.Key] = [] + + if let textLayout = textLayout { + for item in textLayout.embeddedItems { + if let stickerItem = item.value as? InlineStickerItem { + let index: Int + if let currentNext = nextIndexById[stickerItem.file.fileId] { + index = currentNext + } else { + index = 0 + } + nextIndexById[stickerItem.file.fileId] = index + 1 + let id = InlineStickerItemLayer.Key(id: stickerItem.file.fileId, index: index) + validIds.append(id) + + let itemLayer: InlineStickerItemLayer + if let current = self.inlineStickerItemLayers[id] { + itemLayer = current + } else { + itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file) + self.inlineStickerItemLayers[id] = itemLayer + self.textNode.layer.addSublayer(itemLayer) + } + + itemLayer.frame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 1.0).center, size: CGSize()).insetBy(dx: -12.0, dy: -12.0) + } + } + } + + var removeKeys: [InlineStickerItemLayer.Key] = [] + for (key, itemLayer) in self.inlineStickerItemLayers { + if !validIds.contains(key) { + removeKeys.append(key) + itemLayer.removeFromSuperlayer() + } + } + for key in removeKeys { + self.inlineStickerItemLayers.removeValue(forKey: key) + } + } + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)