import Foundation import UIKit import Display import AsyncDisplayKit import AnimatedStickerNode import TelegramAnimatedStickerNode import SwiftSignalKit import AccountContext import YuvConversion import TelegramCore import Postbox import AnimationCache import LottieAnimationCache import VideoAnimationCache import MultiAnimationRenderer import ShimmerEffect import TextFormat import TelegramUIPreferences import GenerateStickerPlaceholderImage import UIKitRuntimeUtils import ComponentFlow public func generateTopicIcon(title: String, backgroundColors: [UIColor], strokeColors: [UIColor], size: CGSize) -> UIImage? { let realSize = size return generateImage(realSize, rotatedContext: { realSize, context in context.clear(CGRect(origin: .zero, size: realSize)) context.saveGState() let size = CGSize(width: 32.0, height: 32.0) let scale: CGFloat = realSize.width / size.width context.scaleBy(x: scale, y: scale) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.translateBy(x: -14.0 - UIScreenPixel, y: -14.0 - UIScreenPixel) let _ = try? drawSvgPath(context, path: "M24.1835,4.71703 C21.7304,2.42169 18.2984,0.995605 14.5,0.995605 C7.04416,0.995605 1.0,6.49029 1.0,13.2683 C1.0,17.1341 2.80572,20.3028 5.87839,22.5523 C6.27132,22.84 6.63324,24.4385 5.75738,25.7811 C5.39922,26.3301 5.00492,26.7573 4.70138,27.0861 C4.26262,27.5614 4.01347,27.8313 4.33716,27.967 C4.67478,28.1086 6.66968,28.1787 8.10952,27.3712 C9.23649,26.7392 9.91903,26.1087 10.3787,25.6842 C10.7588,25.3331 10.9864,25.1228 11.187,25.1688 C11.9059,25.3337 12.6478,25.4461 13.4075,25.5015 C13.4178,25.5022 13.4282,25.503 13.4386,25.5037 C13.7888,25.5284 14.1428,25.5411 14.5,25.5411 C21.9558,25.5411 28.0,20.0464 28.0,13.2683 C28.0,9.94336 26.5455,6.92722 24.1835,4.71703 ") context.closePath() context.clip() let colorsArray = backgroundColors.map { $0.cgColor } as NSArray var locations: [CGFloat] = [0.0, 1.0] let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) context.resetClip() let _ = try? drawSvgPath(context, path: "M24.1835,4.71703 C21.7304,2.42169 18.2984,0.995605 14.5,0.995605 C7.04416,0.995605 1.0,6.49029 1.0,13.2683 C1.0,17.1341 2.80572,20.3028 5.87839,22.5523 C6.27132,22.84 6.63324,24.4385 5.75738,25.7811 C5.39922,26.3301 5.00492,26.7573 4.70138,27.0861 C4.26262,27.5614 4.01347,27.8313 4.33716,27.967 C4.67478,28.1086 6.66968,28.1787 8.10952,27.3712 C9.23649,26.7392 9.91903,26.1087 10.3787,25.6842 C10.7588,25.3331 10.9864,25.1228 11.187,25.1688 C11.9059,25.3337 12.6478,25.4461 13.4075,25.5015 C13.4178,25.5022 13.4282,25.503 13.4386,25.5037 C13.7888,25.5284 14.1428,25.5411 14.5,25.5411 C21.9558,25.5411 28.0,20.0464 28.0,13.2683 C28.0,9.94336 26.5455,6.92722 24.1835,4.71703 ") context.closePath() if let path = context.path { let strokePath = path.copy(strokingWithWidth: 1.0 + UIScreenPixel, lineCap: .round, lineJoin: .round, miterLimit: 0.0) context.beginPath() context.addPath(strokePath) context.clip() let colorsArray = strokeColors.map { $0.cgColor } as NSArray var locations: [CGFloat] = [0.0, 1.0] let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) } context.restoreGState() let fontSize = round(15.0 * scale) let attributedString = NSAttributedString(string: title, attributes: [NSAttributedString.Key.font: Font.with(size: fontSize, design: .round, weight: .bold), NSAttributedString.Key.foregroundColor: UIColor.white]) let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, [.useOpticalBounds]) let lineOffset = CGPoint(x: 1.0 - UIScreenPixel, y: floorToScreenPixels(realSize.height * 0.05)) let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (realSize.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (realSize.height - lineBounds.size.height) / 2.0) + lineOffset.y) context.translateBy(x: realSize.width / 2.0, y: realSize.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -realSize.width / 2.0, y: -realSize.height / 2.0) context.translateBy(x: lineOrigin.x, y: lineOrigin.y) CTLineDraw(line, context) context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) }) } public enum AnimationCacheAnimationType { case still case lottie case video } public extension AnimationCacheAnimationType { init(file: TelegramMediaFile) { if file.isVideoSticker || file.isVideoEmoji { self = .video } else if file.isAnimatedSticker { self = .lottie } else { self = .still } } } public func animationCacheFetchFile(context: AccountContext, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable { return animationCacheFetchFile( postbox: context.account.postbox, userLocation: userLocation, userContentType: userContentType, resource: resource, type: type, keyframeOnly: keyframeOnly, customColor: customColor ) } public func animationCacheFetchFile(postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable { return { options in let source = AnimatedStickerResourceSource(postbox: postbox, resource: resource.resource, fitzModifier: nil, isVideo: false) let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in guard let result = result else { return } switch type { case .video: cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .lottie: guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { options.writer.finish() return } cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .still: cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: customColor) } }) let fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resource).start() return ActionDisposable { dataDisposable.dispose() fetchDisposable.dispose() } } } public func animationCacheLoadLocalFile(name: String, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable { return { options in let source = AnimatedStickerNodeLocalFileSource(name: name) let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in guard let result = result else { return } switch type { case .video: cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .lottie: guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { options.writer.finish() return } cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .still: cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: customColor) } }) return ActionDisposable { dataDisposable.dispose() } } } private func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: Bool, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? { return generateImage(bounds, rotatedContext: { contextSize, context in let bounds = CGRect(origin: CGPoint(), size: contextSize) context.clear(bounds) let circleBounds = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - size.width) / 2.0), y: floorToScreenPixels((bounds.height - size.height) / 2.0)), size: size) context.addEllipse(in: circleBounds) context.clip() if let secondColor = nameColor.secondary { var firstColor = nameColor.main var secondColor = secondColor if isDark, nameColor.tertiary == nil { firstColor = secondColor secondColor = nameColor.main } context.setFillColor(secondColor.cgColor) context.fill(circleBounds) if let thirdColor = nameColor.tertiary { context.move(to: CGPoint(x: contextSize.width, y: 0.0)) context.addLine(to: CGPoint(x: contextSize.width, y: contextSize.height)) context.addLine(to: CGPoint(x: 0.0, y: contextSize.height)) context.closePath() context.setFillColor(firstColor.cgColor) context.fillPath() context.setFillColor(thirdColor.cgColor) context.translateBy(x: contextSize.width / 2.0, y: contextSize.height / 2.0) context.rotate(by: .pi / 4.0) let rectSide = size.width / 40.0 * 18.0 let rectCornerRadius = round(size.width / 40.0 * 4.0) let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: -rectSide / 2.0, y: -rectSide / 2.0), size: CGSize(width: rectSide, height: rectSide)), cornerRadius: rectCornerRadius) context.addPath(path.cgPath) context.fillPath() } else { context.move(to: .zero) context.addLine(to: CGPoint(x: contextSize.width, y: 0.0)) context.addLine(to: CGPoint(x: 0.0, y: contextSize.height)) context.closePath() context.setFillColor(firstColor.cgColor) context.fillPath() } } else { context.setFillColor(nameColor.main.cgColor) context.fill(circleBounds) } }) } public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private final class Arguments { let context: InlineStickerItemLayer.Context let userLocation: MediaResourceUserLocation let emoji: ChatTextInputTextCustomEmojiAttribute let cache: AnimationCache let renderer: MultiAnimationRenderer let unique: Bool let placeholderColor: UIColor let loopCount: Int? let pointSize: CGSize let pixelSize: CGSize init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, loopCount: Int?, pointSize: CGSize, pixelSize: CGSize) { self.context = context self.userLocation = userLocation self.emoji = emoji self.cache = cache self.renderer = renderer self.unique = unique self.placeholderColor = placeholderColor self.loopCount = loopCount self.pointSize = pointSize self.pixelSize = pixelSize } } public enum Context: Equatable { public final class Custom: Equatable { public let postbox: Postbox public let energyUsageSettings: () -> EnergyUsageSettings public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> public init(postbox: Postbox, energyUsageSettings: @escaping () -> EnergyUsageSettings, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>) { self.postbox = postbox self.energyUsageSettings = energyUsageSettings self.resolveInlineStickers = resolveInlineStickers } public static func ==(lhs: Custom, rhs: Custom) -> Bool { if lhs.postbox !== rhs.postbox { return false } return true } } case account(AccountContext) case custom(Custom) var postbox: Postbox { switch self { case let .account(account): return account.account.postbox case let .custom(custom): return custom.postbox } } var energyUsageSettings: EnergyUsageSettings { switch self { case let .account(account): return account.sharedContext.energyUsageSettings case let .custom(custom): return custom.energyUsageSettings() } } func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { switch self { case let .account(account): return account.engine.stickers.resolveInlineStickers(fileIds: fileIds) case let .custom(custom): return custom.resolveInlineStickers(fileIds) } } public static func ==(lhs: Context, rhs: Context) -> Bool { switch lhs { case let .account(lhsContext): if case let .account(rhsContext) = rhs, lhsContext === rhsContext { return true } else { return false } case let .custom(custom): if case .custom(custom) = rhs { return true } else { return false } } } } public static let queue = Queue() public struct Key: Hashable { public var id: Int64 public var index: Int public init(id: Int64, index: Int) { self.id = id self.index = index } } private let arguments: Arguments? private var isDisplayingPlaceholder: Bool = false private var didProcessTintColor: Bool = false public private(set) var file: TelegramMediaFile? private var localAnimationName: String? private var infoDisposable: Disposable? private var disposable: Disposable? private var fetchDisposable: Disposable? private var loadDisposable: Disposable? private var _contentTintColor: UIColor? public var contentTintColor: UIColor? { get { return self._contentTintColor } set(value) { if self._contentTintColor != value { self._contentTintColor = value self.updateTintColor() } } } private var _dynamicColor: UIColor? public var dynamicColor: UIColor? { get { return self._dynamicColor } set(value) { if self._dynamicColor != value { self._dynamicColor = value self.updateTintColor() } } } private var currentLoopCount: Int = 0 private var isInHierarchyValue: Bool = false public var isVisibleForAnimations: Bool = false { didSet { if self.isVisibleForAnimations != oldValue { self.updatePlayback() } } } public weak var mirrorLayer: CALayer? { didSet { if let mirrorLayer = self.mirrorLayer { mirrorLayer.contents = self.contents var customColor = self.contentTintColor if let file = self.file { if file.isCustomTemplateEmoji { customColor = self.dynamicColor } } if customColor != nil { if self.layerTintColor == nil { setLayerContentsMaskMode(mirrorLayer, true) } } else { if self.layerTintColor != nil { setLayerContentsMaskMode(mirrorLayer, false) } } if let customColor { ComponentTransition.immediate.setTintColor(layer: mirrorLayer, color: customColor) } else { self.layerTintColor = nil } } } } override public var contents: Any? { didSet { if let mirrorLayer = self.mirrorLayer { mirrorLayer.contents = self.contents } } } public convenience init(context: AccountContext, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) { self.init( context: .account(context), userLocation: userLocation, attemptSynchronousLoad: attemptSynchronousLoad, emoji: emoji, file: file, cache: cache, renderer: renderer, unique: unique, placeholderColor: placeholderColor, pointSize: pointSize, dynamicColor: dynamicColor, loopCount: loopCount ) } public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) { let scale = min(2.0, UIScreenScale) self.arguments = Arguments( context: context, userLocation: userLocation, emoji: emoji, cache: cache, renderer: renderer, unique: unique, placeholderColor: placeholderColor, loopCount: loopCount, pointSize: pointSize, pixelSize: CGSize(width: pointSize.width * scale, height: pointSize.height * scale) ) self._dynamicColor = dynamicColor super.init() if let custom = emoji.custom { switch custom { case let .topic(id, info): self.updateTopicInfo(topicInfo: (id, info)) case let .nameColors(colors): self.updateNameColors(colors: colors) case let .stars(tinted): self.updateStars(tinted: tinted) if tinted { self.updateTintColor() } case .ton: self.updateTon() case let .animation(name): self.updateLocalAnimation(name: name, attemptSynchronousLoad: attemptSynchronousLoad) } } else if let file = file { self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) } else { self.infoDisposable = (context.resolveInlineStickers(fileIds: [emoji.fileId]) |> deliverOnMainQueue).start(next: { [weak self] files in guard let strongSelf = self else { return } if let file = files[emoji.fileId] { strongSelf.updateFile(file: file, attemptSynchronousLoad: false) } }) } } override public init(layer: Any) { self.arguments = nil super.init(layer: layer) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.loadDisposable?.dispose() self.infoDisposable?.dispose() self.disposable?.dispose() self.fetchDisposable?.dispose() } override public func action(forKey event: String) -> CAAction? { if event == kCAOnOrderIn { self.isInHierarchyValue = true self.updatePlayback() } else if event == kCAOnOrderOut { self.isInHierarchyValue = false self.updatePlayback() } return nullAction } public func updateTintColor(contentTintColor: UIColor?, dynamicColor: UIColor?, transition: ComponentTransition) { self._contentTintColor = contentTintColor self._dynamicColor = dynamicColor if !self.isDisplayingPlaceholder { var customColor = self.contentTintColor if let file = self.file { if file.isCustomTemplateEmoji { customColor = self.dynamicColor } } if customColor != nil { if self.layerTintColor == nil { setLayerContentsMaskMode(self, true) } } else { if self.layerTintColor != nil { setLayerContentsMaskMode(self, false) } } if let customColor { transition.setTintColor(layer: self, color: customColor) } else { self.layerTintColor = nil } } else { if self.layerTintColor != nil { setLayerContentsMaskMode(self, false) } self.layerTintColor = nil } } private func updateTintColor() { if !self.isDisplayingPlaceholder { var customColor = self.contentTintColor if let file = self.file { if file.isCustomTemplateEmoji { customColor = self.dynamicColor } } else if let emoji = self.arguments?.emoji, let custom = emoji.custom, case .stars = custom { customColor = self.dynamicColor } if customColor != nil { if self.layerTintColor == nil { setLayerContentsMaskMode(self, true) } } else { if self.layerTintColor != nil { setLayerContentsMaskMode(self, false) } } self.layerTintColor = customColor?.cgColor } else { if self.layerTintColor != nil { setLayerContentsMaskMode(self, false) } self.layerTintColor = nil } } private func updatePlayback() { guard let arguments = self.arguments else { return } var shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations if shouldBePlaying, let loopCount = arguments.loopCount, self.currentLoopCount >= loopCount { shouldBePlaying = false } if self.shouldBeAnimating != shouldBePlaying { self.shouldBeAnimating = shouldBePlaying if !shouldBePlaying { self.currentLoopCount = 0 } } } private func updateTopicInfo(topicInfo: (Int64, EngineMessageHistoryThread.Info)) { if topicInfo.0 == 1 { let image = generateImage(CGSize(width: 18.0, height: 18.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) if let cgImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/GeneralTopicIcon"), color: .white)?.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size)) } }) self.contents = image?.cgImage } else { func generateTopicColors(_ color: Int32) -> ([UInt32], [UInt32]) { return ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]) } let topicColors: [Int32: ([UInt32], [UInt32])] = [ 0x6FB9F0: ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), 0xFFD67E: ([0xFFD67E, 0xFC8601], [0xDA9400, 0xFA5F00]), 0xCB86DB: ([0xCB86DB, 0x9338AF], [0x812E98, 0x6F2B87]), 0x8EEE98: ([0x8EEE98, 0x02B504], [0x02A01B, 0x009716]), 0xFF93B2: ([0xFF93B2, 0xE23264], [0xFC447A, 0xC80C46]), 0xFB6F5F: ([0xFB6F5F, 0xD72615], [0xDC1908, 0xB61506]) ] let colors = topicColors[topicInfo.1.iconColor] ?? generateTopicColors(topicInfo.1.iconColor) if let image = generateTopicIcon(title: String(topicInfo.1.title.prefix(1)), backgroundColors: colors.0.map(UIColor.init(rgb:)), strokeColors: colors.1.map(UIColor.init(rgb:)), size: CGSize(width: 32.0, height: 32.0)) { self.contents = image.cgImage } } } private func updateNameColors(colors: [UInt32]) { if colors.isEmpty { return } let main = UIColor(rgb: colors[0]) var secondary: UIColor? if colors.count >= 2 { secondary = UIColor(rgb: colors[1]) } var tertiary: UIColor? if colors.count >= 3 { tertiary = UIColor(rgb: colors[2]) } let mappedColor = PeerNameColors.Colors(main: main, secondary: secondary, tertiary: tertiary) let image = generateImage(CGSize(width: 18.0, height: 18.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) if let cgImage = generatePeerNameColorImage(nameColor: mappedColor, isDark: false, bounds: CGSize(width: 18.0, height: 18.0), size: CGSize(width: 16.0, height: 16.0))?.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size)) } }) self.contents = image?.cgImage } private func updateStars(tinted: Bool) { self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage } private func updateTon() { self.contents = tonImage?.cgImage } private func updateLocalAnimation(name: String, attemptSynchronousLoad: Bool) { guard let arguments = self.arguments else { return } self.localAnimationName = name if attemptSynchronousLoad { if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) { } self.loadAnimation() } else { self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: true, customColor: nil), completion: { [weak self] result, isFinal in guard let strongSelf = self else { return } strongSelf.loadAnimation() }) } } private func loadLocalAnimation() { guard let arguments = self.arguments else { return } guard let name = self.localAnimationName else { return } let keyframeOnly = arguments.pixelSize.width >= 120.0 self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil)) } private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { guard let arguments = self.arguments else { return } if self.file?.fileId == file.fileId { return } self.file = file if attemptSynchronousLoad { if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) { if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: arguments.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: arguments.placeholderColor) { self.contents = image.cgImage self.isDisplayingPlaceholder = true self.updateTintColor() } } else { self.updateTintColor() } self.loadAnimation() } else { let isTemplate = file.isCustomTemplateEmoji let pointSize = arguments.pointSize let placeholderColor = arguments.placeholderColor let isThumbnailCancelled = Atomic(value: false) self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { MultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { guard let strongSelf = self, !isThumbnailCancelled.with({ $0 }) else { return } if let image = image { strongSelf.contents = image.cgImage strongSelf.isDisplayingPlaceholder = true strongSelf.updateTintColor() } if isFinal { strongSelf.loadAnimation() } } } } else { guard let strongSelf = self else { return } let _ = isThumbnailCancelled.swap(true) strongSelf.loadAnimation() } }) } } private func loadAnimation() { guard let arguments = self.arguments else { return } guard let file = self.file else { return } let isTemplate = file.isCustomTemplateEmoji let context = arguments.context if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji { let keyframeOnly = arguments.pixelSize.width >= 120.0 self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil)) } else { self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in guard result.complete else { return } cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil) }) let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: context.postbox, userLocation: arguments.userLocation, fileReference: .customEmoji(media: file), resource: file.resource).start() return ActionDisposable { dataDisposable.dispose() fetchDisposable.dispose() } }) } } override public func updateDisplayPlaceholder(displayPlaceholder: Bool) { if self.isDisplayingPlaceholder == displayPlaceholder { return } self.isDisplayingPlaceholder = displayPlaceholder self.updateTintColor() } override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) { guard let arguments = self.arguments else { return } if self.isDisplayingPlaceholder { self.isDisplayingPlaceholder = false self.updateTintColor() if let current = self.contents { let previousLayer = SimpleLayer() previousLayer.contents = current previousLayer.frame = self.frame self.superlayer?.insertSublayer(previousLayer, below: self) previousLayer.opacity = 0.0 previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in previousLayer?.removeFromSuperlayer() }) self.contents = contents self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) } else { self.contents = contents self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } else { if !self.didProcessTintColor { //self.didProcessTintColor = true self.updateTintColor() } self.contents = contents } if didLoop { self.currentLoopCount += 1 if let loopCount = arguments.loopCount, self.currentLoopCount >= loopCount { self.updatePlayback() } } } } public final class EmojiTextAttachmentView: UIView { public let contentLayer: InlineStickerItemLayer public var isActive: Bool = true { didSet { if self.isActive != oldValue { self.contentLayer.isVisibleForAnimations = self.isActive } } } public convenience init(context: AccountContext, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { self.init( context: .account(context), userLocation: userLocation, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: pointSize ) } public init(context: InlineStickerItemLayer.Context, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { self.contentLayer = InlineStickerItemLayer(context: context, userLocation: userLocation, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: pointSize) super.init(frame: CGRect()) self.layer.addSublayer(self.contentLayer) self.contentLayer.isVisibleForAnimations = context.energyUsageSettings.loopEmoji } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func updateTextColor(_ textColor: UIColor) { self.contentLayer.dynamicColor = textColor } override public func layoutSubviews() { super.layoutSubviews() self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height)) } } public final class CustomEmojiContainerView: UIView { private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? public private(set) var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] public init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { self.emojiViewProvider = emojiViewProvider super.init(frame: CGRect()) } required init(coder: NSCoder) { preconditionFailure() } public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute, CGFloat)]) { var nextIndexById: [Int64: Int] = [:] var validKeys = Set() for (rect, emoji, fontSize) in emojiRects { let index: Int if let nextIndex = nextIndexById[emoji.fileId] { index = nextIndex } else { index = 0 } nextIndexById[emoji.fileId] = index + 1 let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index) let view: UIView if let current = self.emojiLayers[key] { view = current } else if let newView = self.emojiViewProvider(emoji) { view = newView self.addSubview(newView) self.emojiLayers[key] = view } else { continue } if let view = view as? EmojiTextAttachmentView { view.updateTextColor(textColor) } let itemSize: CGFloat = floor(24.0 * fontSize / 17.0) let size = CGSize(width: itemSize, height: itemSize) view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0) + 1.0), size: size) validKeys.insert(key) } var removeKeys: [InlineStickerItemLayer.Key] = [] for (key, view) in self.emojiLayers { if !validKeys.contains(key) { removeKeys.append(key) view.removeFromSuperview() } } for key in removeKeys { self.emojiLayers.removeValue(forKey: key) } } } private let tintedStarImage: UIImage? = { generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) if let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) } })?.withRenderingMode(.alwaysTemplate) }() private let starImage: UIImage? = { generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 1.0, dy: 1.0), byTiling: false) } })?.withRenderingMode(.alwaysTemplate) }() private let tonImage: UIImage? = { generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: UIColor(rgb: 0x007aff)), let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) } })?.withRenderingMode(.alwaysTemplate) }()