mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Inline animation
This commit is contained in:
parent
72923655e4
commit
9963c7794a
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<AnimatedStickerDirectFrameSource>?
|
||||
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user