GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,519 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramPresentationData
import ShimmerEffect
import EntityKeyboard
import AnimationCache
import MultiAnimationRenderer
import TextFormat
private let nativeItemSize = 36.0
private let minItemsPerRow = 8
private let verticalSpacing = 9.0
private let minSpacing = 9.0
private let containerInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0)
class ItemLayout {
let width: CGFloat
let itemsCount: Int
let itemsPerRow: Int
let visibleItemSize: CGFloat
let horizontalSpacing: CGFloat
let height: CGFloat
init(width: CGFloat, itemsCount: Int) {
self.width = width
self.itemsCount = itemsCount
let itemHorizontalSpace = width - containerInsets.left - containerInsets.right
self.itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + minSpacing) / (nativeItemSize + minSpacing)))
self.visibleItemSize = floor((itemHorizontalSpace - CGFloat(self.itemsPerRow - 1) * minSpacing) / CGFloat(self.itemsPerRow))
self.horizontalSpacing = floor((itemHorizontalSpace - visibleItemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1))
let numRowsInGroup = (itemsCount + (self.itemsPerRow - 1)) / self.itemsPerRow
self.height = CGFloat(numRowsInGroup) * visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * verticalSpacing
}
func frame(itemIndex: Int) -> CGRect {
let row = itemIndex / self.itemsPerRow
let column = itemIndex % self.itemsPerRow
return CGRect(
origin: CGPoint(
x: containerInsets.left + CGFloat(column) * (self.visibleItemSize + self.horizontalSpacing),
y: CGFloat(row) * (self.visibleItemSize + verticalSpacing)
),
size: CGSize(
width: self.visibleItemSize,
height: self.visibleItemSize
)
)
}
}
final class StickerPackEmojisItem: GridItem {
let context: AccountContext
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
let interaction: StickerPackPreviewInteraction
let info: StickerPackCollectionInfo
let items: [StickerPackItem]
let theme: PresentationTheme
let strings: PresentationStrings
let title: String?
let isInstalled: Bool?
let isEmpty: Bool
let section: GridSection? = nil
let fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, interaction: StickerPackPreviewInteraction, info: StickerPackCollectionInfo, items: [StickerPackItem], theme: PresentationTheme, strings: PresentationStrings, title: String?, isInstalled: Bool?, isEmpty: Bool) {
self.context = context
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.interaction = interaction
self.info = info
self.items = items
self.theme = theme
self.strings = strings
self.title = title
self.isInstalled = isInstalled
self.isEmpty = isEmpty
self.fillsRowWithDynamicHeight = { width in
let layout = ItemLayout(width: width, itemsCount: items.count)
return layout.height + (title != nil ? 61.0 : 0.0)
}
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPackEmojisItemNode()
return node
}
func update(node: GridItemNode) {
guard let _ = node as? StickerPackEmojisItemNode else {
assertionFailure()
return
}
}
}
private let textFont = Font.regular(20.0)
final class StickerPackEmojisItemNode: GridItemNode {
private var item: StickerPackEmojisItem?
private var itemLayout: ItemLayout?
private var shimmerHostView: PortalSourceView?
private var standaloneShimmerEffect: StandaloneShimmerEffect?
private var boundsChangeTrackerLayer = SimpleLayer()
private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:]
private var visibleItemPlaceholderViews: [EmojiKeyboardItemLayer.Key: EmojiPagerContentComponent.View.ItemPlaceholderView] = [:]
private let containerNode: ASDisplayNode
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let buttonNode: HighlightableButtonNode
override init() {
self.containerNode = ASDisplayNode()
self.titleNode = ImmediateTextNode()
self.subtitleNode = ImmediateTextNode()
self.buttonNode = HighlightableButtonNode(pointerStyle: nil)
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 14.0
super.init()
self.addSubnode(self.containerNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
guard let item = self.item else {
return
}
if item.isInstalled == true {
item.interaction.removeStickerPack(item.info)
} else {
item.interaction.addStickerPack(item.info, item.items)
}
}
override var isVisibleInGrid: Bool {
didSet {
}
}
override func didLoad() {
super.didLoad()
let shimmerHostView = PortalSourceView()
shimmerHostView.alpha = 0.0
shimmerHostView.frame = CGRect(origin: CGPoint(), size: self.size)
self.view.addSubview(shimmerHostView)
self.shimmerHostView = shimmerHostView
let standaloneShimmerEffect = StandaloneShimmerEffect()
self.standaloneShimmerEffect = standaloneShimmerEffect
if let item = self.item {
let shimmerBackgroundColor = item.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08)
let shimmerForegroundColor = item.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15)
standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor)
self.updateShimmerIfNeeded()
}
let boundsChangeTrackerLayer = SimpleLayer()
boundsChangeTrackerLayer.opacity = 0.0
self.layer.addSublayer(boundsChangeTrackerLayer)
boundsChangeTrackerLayer.didEnterHierarchy = { [weak self] in
self?.standaloneShimmerEffect?.updateLayer()
}
self.boundsChangeTrackerLayer = boundsChangeTrackerLayer
}
func targetItem(at point: CGPoint) -> (TelegramMediaFile, CALayer)? {
if let (item, _) = self.item(atPoint: point), let file = item.itemFile {
let itemId = EmojiKeyboardItemLayer.Key(
groupId: 0,
itemId: .animation(.file(file.fileId))
)
if let itemLayer = self.visibleItemLayers[itemId] {
return (file._parse(), itemLayer)
}
}
return nil
}
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let (item, _) = self.item(atPoint: location), let file = item.itemFile?._parse() {
if case .tap = gesture {
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let emojiAttribute = emojiAttribute {
self.item?.interaction.emojiSelected(text, emojiAttribute)
}
}
}
default:
break
}
}
private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (EmojiPagerContentComponent.Item, CGRect)? {
let localPoint = point
var closestItem: (key: EmojiKeyboardItemLayer.Key, distance: CGFloat)?
for (key, itemLayer) in self.visibleItemLayers {
if extendedHitRange {
let position = CGPoint(x: itemLayer.frame.midX, y: itemLayer.frame.midY)
let distance = CGPoint(x: localPoint.x - position.x, y: localPoint.y - position.y)
let distance2 = distance.x * distance.x + distance.y * distance.y
if distance2 > pow(max(itemLayer.bounds.width, itemLayer.bounds.height), 2.0) {
continue
}
if let closestItemValue = closestItem {
if closestItemValue.distance > distance2 {
closestItem = (key, distance2)
}
} else {
closestItem = (key, distance2)
}
} else {
if itemLayer.frame.contains(localPoint) {
return (itemLayer.item, itemLayer.frame)
}
}
}
if let key = closestItem?.key {
if let itemLayer = self.visibleItemLayers[key] {
return (itemLayer.item, itemLayer.frame)
}
}
return nil
}
private var size = CGSize()
override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
guard let item = item as? StickerPackEmojisItem else {
return
}
self.item = item
self.size = size
if let title = item.title {
let isInstalled = item.isInstalled ?? false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: item.theme.actionSheet.primaryTextColor, paragraphAlignment: .natural)
self.subtitleNode.attributedText = NSAttributedString(string: item.strings.EmojiPack_Emoji(Int32(item.items.count)), font: Font.regular(15.0), textColor: item.theme.actionSheet.secondaryTextColor, paragraphAlignment: .natural)
self.buttonNode.setAttributedTitle(NSAttributedString(string: isInstalled ? item.strings.EmojiPack_Added.uppercased() : item.strings.EmojiPack_Add.uppercased(), font: Font.semibold(15.0), textColor: isInstalled ? item.theme.list.itemCheckColors.fillColor : item.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center), for: .normal)
self.buttonNode.backgroundColor = isInstalled ? item.theme.list.itemCheckColors.fillColor.withAlphaComponent(0.08) : item.theme.list.itemCheckColors.fillColor
}
self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate)
let shimmerBackgroundColor = item.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08)
let shimmerForegroundColor = item.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15)
self.standaloneShimmerEffect?.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor)
self.setNeedsLayout()
}
private var visibleRect: CGRect?
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
var y: CGFloat
if absoluteRect.minY > 0.0 {
y = 0.0
} else {
y = absoluteRect.minY * -1.0
}
var rect = CGRect(origin: CGPoint(x: 0.0, y: y), size: CGSize(width: containerSize.width, height: containerSize.height))
rect.size.height += 96.0
self.visibleRect = rect
self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate)
}
func updateVisibleItems(attemptSynchronousLoads: Bool, transition: ContainedViewLayoutTransition) {
guard let item = self.item, !self.size.width.isZero, let visibleRect = self.visibleRect else {
return
}
let context = item.context
let animationCache = item.animationCache
let animationRenderer = item.animationRenderer
let theme = item.theme
let items = item.items
var validIds = Set<EmojiKeyboardItemLayer.Key>()
let itemLayout: ItemLayout
if let current = self.itemLayout, current.width == self.size.width && current.itemsCount == items.count {
itemLayout = current
} else {
itemLayout = ItemLayout(width: self.size.width, itemsCount: items.count)
self.itemLayout = itemLayout
}
self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: item.title != nil ? 61.0 : 0.0), size: CGSize(width: itemLayout.width, height: itemLayout.height))
for index in 0 ..< items.count {
var itemFrame = itemLayout.frame(itemIndex: index)
if !visibleRect.intersects(itemFrame) {
continue
}
let item = items[index]
let itemId = EmojiKeyboardItemLayer.Key(
groupId: 0,
itemId: .animation(.file(item.file.fileId))
)
let itemDimensions = item.file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
let itemNativeFitSize = itemDimensions.fitted(CGSize(width: nativeItemSize, height: nativeItemSize))
let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize))
validIds.insert(itemId)
itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0)
itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0)
itemFrame.size = itemVisibleFitSize
var updateItemLayerPlaceholder = false
var itemTransition = transition
let itemLayer: EmojiKeyboardItemLayer
if let current = self.visibleItemLayers[itemId] {
itemLayer = current
} else {
updateItemLayerPlaceholder = true
itemTransition = .immediate
let animationData = EntityKeyboardAnimationData(file: item.file)
itemLayer = EmojiKeyboardItemLayer(
item: EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
),
context: context,
attemptSynchronousLoad: attemptSynchronousLoads,
content: .animation(animationData),
cache: animationCache,
renderer: animationRenderer,
placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1),
blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5),
accentIconColor: theme.list.itemAccentColor,
pointSize: itemNativeFitSize,
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
guard let strongSelf = self else {
return
}
if displayPlaceholder {
if let itemLayer = strongSelf.visibleItemLayers[itemId] {
let placeholderView: EmojiPagerContentComponent.View.ItemPlaceholderView
if let current = strongSelf.visibleItemPlaceholderViews[itemId] {
placeholderView = current
} else {
var placeholderContent: EmojiPagerContentComponent.View.ItemPlaceholderView.Content?
if let immediateThumbnailData = item.file.immediateThumbnailData {
placeholderContent = .thumbnail(immediateThumbnailData)
}
placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView(
context: context,
dimensions: item.file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0),
content: placeholderContent,
shimmerView: nil,//strongSelf.shimmerHostView,
color: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08),
size: itemNativeFitSize
)
strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView
strongSelf.containerNode.view.insertSubview(placeholderView, at: 0)
}
placeholderView.frame = itemLayer.frame
placeholderView.update(size: placeholderView.bounds.size)
strongSelf.updateShimmerIfNeeded()
}
} else {
if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] {
strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId)
if duration > 0.0 {
placeholderView.layer.opacity = 0.0
placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in
guard let strongSelf = self else {
return
}
placeholderView?.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
})
} else {
placeholderView.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
}
}
}
}
)
self.containerNode.layer.addSublayer(itemLayer)
self.visibleItemLayers[itemId] = itemLayer
}
switch itemLayer.item.tintMode {
case .none:
break
case .accent:
itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor
case .primary:
itemLayer.layerTintColor = theme.list.itemPrimaryTextColor.cgColor
case let .custom(color):
itemLayer.layerTintColor = color.cgColor
}
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size)
itemTransition.updatePosition(layer: itemLayer, position: itemPosition)
itemTransition.updateBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
if let placeholderView = self.visibleItemPlaceholderViews[itemId] {
if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds {
itemTransition.updateFrame(view: placeholderView, frame: itemFrame)
placeholderView.update(size: itemFrame.size)
}
} else if updateItemLayerPlaceholder {
if itemLayer.displayPlaceholder {
itemLayer.onUpdateDisplayPlaceholder(true, 0.0)
}
}
itemLayer.isVisibleForAnimations = true
}
for id in self.visibleItemLayers.keys {
if !validIds.contains(id) {
self.visibleItemLayers[id]?.removeFromSuperlayer()
self.visibleItemLayers[id] = nil
}
}
for id in self.visibleItemPlaceholderViews.keys {
if !validIds.contains(id) {
self.visibleItemPlaceholderViews[id]?.removeFromSuperview()
self.visibleItemPlaceholderViews[id] = nil
}
}
}
private func updateShimmerIfNeeded() {
if self.visibleItemPlaceholderViews.isEmpty {
self.standaloneShimmerEffect?.layer = nil
} else {
self.standaloneShimmerEffect?.layer = self.shimmerHostView?.layer
}
}
override func layout() {
super.layout()
if let _ = self.item {
var buttonSize = self.buttonNode.calculateSizeThatFits(self.size)
buttonSize.width += 24.0
buttonSize.height = 28.0
let titleSize = self.titleNode.updateLayout(CGSize(width: self.size.width - 60.0, height: self.size.height))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: self.size.width - 60.0, height: self.size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 10.0), size: titleSize)
self.subtitleNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 33.0), size: subtitleSize)
self.buttonNode.frame = CGRect(origin: CGPoint(x: self.size.width - buttonSize.width - 16.0, y: 17.0), size: buttonSize)
}
self.shimmerHostView?.frame = CGRect(origin: CGPoint(), size: self.size)
self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate)
}
func transitionNode() -> ASDisplayNode? {
return self
}
}
@@ -0,0 +1,539 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramPresentationData
import ShimmerEffect
import StickerPeekUI
import TextFormat
final class StickerPackPreviewInteraction {
var previewedItem: StickerPreviewPeekItem?
var reorderingFileId: MediaId?
var playAnimatedStickers: Bool
let addStickerPack: (StickerPackCollectionInfo, [StickerPackItem]) -> Void
let removeStickerPack: (StickerPackCollectionInfo) -> Void
let emojiSelected: (String, ChatTextInputTextCustomEmojiAttribute) -> Void
let emojiLongPressed: (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void
let addPressed: () -> Void
init(playAnimatedStickers: Bool, addStickerPack: @escaping (StickerPackCollectionInfo, [StickerPackItem]) -> Void, removeStickerPack: @escaping (StickerPackCollectionInfo) -> Void, emojiSelected: @escaping (String, ChatTextInputTextCustomEmojiAttribute) -> Void, emojiLongPressed: @escaping (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void, addPressed: @escaping () -> Void) {
self.playAnimatedStickers = playAnimatedStickers
self.addStickerPack = addStickerPack
self.removeStickerPack = removeStickerPack
self.emojiSelected = emojiSelected
self.emojiLongPressed = emojiLongPressed
self.addPressed = addPressed
}
}
final class StickerPackPreviewGridItem: GridItem {
let context: AccountContext
let stickerItem: StickerPackItem?
let interaction: StickerPackPreviewInteraction
let theme: PresentationTheme
let isPremium: Bool
let isLocked: Bool
let isEmpty: Bool
let isEditable: Bool
let isEditing: Bool
let isAdd: Bool
let section: GridSection? = nil
init(context: AccountContext, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isPremium: Bool, isLocked: Bool, isEmpty: Bool, isEditable: Bool, isEditing: Bool, isAdd: Bool = false) {
self.context = context
self.stickerItem = stickerItem
self.interaction = interaction
self.theme = theme
self.isPremium = isPremium
self.isLocked = isLocked
self.isEmpty = isEmpty
self.isEditable = isEditable
self.isEditing = isEditing
self.isAdd = isAdd
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPackPreviewGridItemNode()
node.setup(context: self.context, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isLocked: self.isLocked, isPremium: self.isPremium, isEmpty: self.isEmpty, isEditable: self.isEditable, isEditing: self.isEditing, isAdd: self.isAdd)
return node
}
func update(node: GridItemNode) {
guard let node = node as? StickerPackPreviewGridItemNode else {
assertionFailure()
return
}
node.setup(context: self.context, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isLocked: self.isLocked, isPremium: self.isPremium, isEmpty: self.isEmpty, isEditable: self.isEditable, isEditing: self.isEditing, isAdd: self.isAdd)
}
}
private let textFont = Font.regular(20.0)
final class StickerPackPreviewGridItemNode: GridItemNode {
private var currentState: (AccountContext, StickerPackItem?, Bool, Bool)?
private var isLocked: Bool?
private var isPremium: Bool?
private var isEditable: Bool?
private var isEmpty: Bool?
private let containerNode: ASDisplayNode
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode
private var lockBackground: UIImageView?
private var lockIconNode: ASImageNode?
private var theme: PresentationTheme?
private var isEditing = false
private var averageColor: UIColor?
override var isVisibleInGrid: Bool {
didSet {
let visibility = self.isVisibleInGrid && (self.interaction?.playAnimatedStickers ?? true)
if visibility && self.setupTimestamp == nil {
self.setupTimestamp = CACurrentMediaTime()
}
if let animationNode = self.animationNode {
animationNode.visibility = visibility
}
}
}
private var currentIsPreviewing = false
private let stickerFetchedDisposable = MetaDisposable()
private let effectFetchedDisposable = MetaDisposable()
var interaction: StickerPackPreviewInteraction?
var stickerPackItem: StickerPackItem? {
return self.currentState?.1
}
var isAdd: Bool {
return self.currentState?.2 == true
}
override init() {
self.containerNode = ASDisplayNode()
self.imageNode = TransformImageNode()
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.placeholderNode)
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self, let image else {
return
}
if let stickerItem = strongSelf.currentState?.1 {
if stickerItem.file.isVideoSticker || stickerItem.file.isAnimatedSticker {
strongSelf.removePlaceholder(animated: !firstTime)
} else {
let current = CACurrentMediaTime()
if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 {
strongSelf.removePlaceholder(animated: true)
} else {
strongSelf.removePlaceholder(animated: false)
}
}
}
firstTime = false
if let self, self.isPremium == true || self.isEditable == true, let averageColor = getAverageColor(image: image) {
self.averageColor = averageColor
self.lockBackground?.tintColor = averageColor
self.lockBackground?.alpha = 1.0
}
}
}
deinit {
self.stickerFetchedDisposable.dispose()
self.effectFetchedDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
guard self.placeholderNode.alpha != 0 else {
return
}
if !animated {
self.placeholderNode.removeFromSupernode()
} else {
self.placeholderNode.alpha = 0.0
self.placeholderNode.allowsGroupOpacity = true
self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.placeholderNode.removeFromSupernode()
self?.placeholderNode.allowsGroupOpacity = false
})
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
@objc private func handleAddTap() {
self.interaction?.addPressed()
}
private var setupTimestamp: Double?
func setup(context: AccountContext, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isLocked: Bool, isPremium: Bool, isEmpty: Bool, isEditable: Bool, isEditing: Bool, isAdd: Bool) {
self.interaction = interaction
self.theme = theme
let isFirstTime = self.currentState == nil
if isAdd {
if !isFirstTime {
return
}
let color = theme.actionSheet.controlAccentColor
self.imageNode.setSignal(.single({ arguments in
let drawingContext = DrawingContext(size: arguments.imageSize, opaque: false)
let size = arguments.imageSize
drawingContext?.withContext({ context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
context.setFillColor(color.withMultipliedAlpha(0.1).cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0))
context.setFillColor(color.cgColor)
let plusSize = CGSize(width: 3.0, height: 21.0)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath)
context.fillPath()
UIGraphicsPopContext()
})
return drawingContext
}))
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleAddTap)))
self.currentState = (context, nil, true, false)
self.setNeedsLayout()
return
}
if interaction.reorderingFileId != nil {
self.isHidden = stickerItem?.file.fileId == interaction.reorderingFileId
} else {
self.isHidden = false
}
if self.currentState == nil || self.currentState!.0 !== context || self.currentState!.1 != stickerItem || self.isLocked != isLocked || self.isPremium != isPremium || self.isEmpty != isEmpty || self.isEditing != isEditing || self.isEditable != isEditable {
self.isLocked = isLocked
self.isPremium = isPremium
self.isEditable = isEditable
if isPremium || isEditing {
let lockBackground: UIImageView
let lockIconNode: ASImageNode
if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode {
lockBackground = currentBackground
lockIconNode = currentIcon
} else {
lockBackground = UIImageView()
lockBackground.alpha = self.averageColor != nil ? 1.0 : 0.0
lockBackground.tintColor = self.averageColor ?? .white
lockBackground.clipsToBounds = true
lockBackground.isUserInteractionEnabled = false
lockIconNode = ASImageNode()
lockIconNode.displaysAsynchronously = false
if isEditing {
lockIconNode.image = generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.addEllipse(in: CGRect(x: 5.5, y: 11.0, width: 3.0, height: 3.0))
context.fillPath()
context.addEllipse(in: CGRect(x: size.width / 2.0 - 1.5, y: 11.0, width: 3.0, height: 3.0))
context.fillPath()
context.addEllipse(in: CGRect(x: size.width - 3.0 - 5.5, y: 11.0, width: 3.0, height: 3.0))
context.fillPath()
})
} else {
lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
}
self.lockBackground = lockBackground
self.lockIconNode = lockIconNode
self.view.addSubview(lockBackground)
lockBackground.addSubview(lockIconNode.view)
if !isFirstTime {
lockBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
}
} else if let lockBackground = self.lockBackground {
self.lockBackground = nil
self.lockIconNode = nil
lockBackground.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
lockBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
lockBackground.removeFromSuperview()
})
}
if let stickerItem = stickerItem {
let visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
if visibility && self.setupTimestamp == nil {
self.setupTimestamp = CACurrentMediaTime()
}
let stickerItemFile = stickerItem.file._parse()
if stickerItem.file.isAnimatedSticker || stickerItem.file.isVideoSticker {
let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
if stickerItem.file.isVideoSticker {
self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: stickerItemFile, small: true, fetched: true))
} else {
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .other, file: stickerItemFile, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
}
if self.animationNode == nil {
let animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode = animationNode
self.containerNode.insertSubnode(animationNode, aboveSubnode: self.imageNode)
animationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
self?.imageNode.isHidden = true
let current = CACurrentMediaTime()
if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 {
if !strongSelf.placeholderNode.alpha.isZero {
strongSelf.removePlaceholder(animated: true)
}
} else {
strongSelf.removePlaceholder(animated: false)
}
}
}
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: context.account, resource: stickerItemFile.resource, isVideo: stickerItem.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached)
self.animationNode?.visibility = visibility
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: stickerPackFileReference(stickerItemFile), resource: stickerItemFile.resource).start())
if stickerItem.file.isPremiumSticker, let effect = stickerItemFile.videoThumbnails.first {
self.effectFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: stickerPackFileReference(stickerItemFile), resource: effect.resource).start())
}
} else {
if let animationNode = self.animationNode {
animationNode.visibility = false
self.animationNode = nil
animationNode.removeFromSupernode()
}
self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: stickerItemFile, small: true))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: stickerPackFileReference(stickerItemFile), resource: chatMessageStickerResource(file: stickerItemFile, small: true)).start())
}
} else {
if isEmpty {
if !self.placeholderNode.alpha.isZero {
self.placeholderNode.alpha = 0.0
self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
} else {
self.placeholderNode.alpha = 1.0
}
}
self.animationNode?.alpha = isLocked ? 0.5 : 1.0
self.imageNode.alpha = isLocked ? 0.5 : 1.0
self.currentState = (context, stickerItem, false, isEditing)
self.setNeedsLayout()
}
self.isEmpty = isEmpty
if self.isEditing != isEditing {
self.isEditing = isEditing
if self.isEditing {
self.startShaking()
} else {
self.containerNode.layer.removeAnimation(forKey: "shaking_position")
self.containerNode.layer.removeAnimation(forKey: "shaking_rotation")
}
}
}
private func startShaking() {
func degreesToRadians(_ x: CGFloat) -> CGFloat {
return .pi * x / 180.0
}
let duration: Double = 0.4
let displacement: CGFloat = 1.0
let degreesRotation: CGFloat = 2.0
let negativeDisplacement = -1.0 * displacement
let position = CAKeyframeAnimation.init(keyPath: "position")
position.beginTime = 0.8
position.duration = duration
position.values = [
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
]
position.calculationMode = .linear
position.isRemovedOnCompletion = false
position.repeatCount = Float.greatestFiniteMagnitude
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
position.isAdditive = true
let transform = CAKeyframeAnimation.init(keyPath: "transform")
transform.beginTime = 2.6
transform.duration = 0.3
transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
transform.values = [
degreesToRadians(-1.0 * degreesRotation),
degreesToRadians(degreesRotation),
degreesToRadians(-1.0 * degreesRotation)
]
transform.calculationMode = .linear
transform.isRemovedOnCompletion = false
transform.repeatCount = Float.greatestFiniteMagnitude
transform.isAdditive = true
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
self.containerNode.layer.add(position, forKey: "shaking_position")
self.containerNode.layer.add(transform, forKey: "shaking_rotation")
}
override func layout() {
super.layout()
let bounds = self.bounds
self.containerNode.frame = bounds
let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0)
var boundingSize = CGSize(width: boundsSide, height: boundsSide)
if let (_, item, isAdd, _) = self.currentState {
if isAdd {
let imageSize = CGSize(width: 512, height: 512).aspectFitted(boundingSize)
let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.frame = imageFrame
return
} else if let item = item, let dimensions = item.file.dimensions?.cgSize {
if item.file.isPremiumSticker {
boundingSize = CGSize(width: boundingSize.width * 1.1, height: boundingSize.width * 1.1)
}
let imageSize = dimensions.aspectFitted(boundingSize)
let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.frame = imageFrame
if let animationNode = self.animationNode {
animationNode.frame = imageFrame
animationNode.updateLayout(size: imageSize)
}
}
}
let imageFrame = self.imageNode.frame
let placeholderFrame = imageFrame
self.placeholderNode.frame = imageFrame
if let theme = self.theme, let (context, stickerItem, _, _) = self.currentState, let item = stickerItem {
self.placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size, enableEffect: context.sharedContext.energyUsageSettings.fullTranslucency)
}
if let lockBackground = self.lockBackground, let lockIconNode = self.lockIconNode {
let lockSize: CGSize
let lockBackgroundFrame: CGRect
if let (_, _, _, isEditing) = self.currentState, isEditing {
lockSize = CGSize(width: 24.0, height: 24.0)
lockBackgroundFrame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: lockSize)
} else {
lockSize = CGSize(width: 16.0, height: 16.0)
lockBackgroundFrame = CGRect(origin: CGPoint(x: bounds.width - lockSize.width - 1.0, y: bounds.height - lockSize.height - 1.0), size: lockSize)
}
if lockBackground.image == nil {
lockBackground.image = generateFilledCircleImage(diameter: lockSize.width, color: .white)?.withRenderingMode(.alwaysTemplate)
}
lockBackground.frame = lockBackgroundFrame
lockBackground.layer.cornerRadius = lockSize.width / 2.0
if #available(iOS 13.0, *) {
lockBackground.layer.cornerCurve = .circular
}
if let icon = lockIconNode.image {
let iconSize = CGSize(width: icon.size.width - 4.0, height: icon.size.height - 4.0)
lockIconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((lockBackgroundFrame.width - iconSize.width) / 2.0), y: floorToScreenPixels((lockBackgroundFrame.height - iconSize.height) / 2.0)), size: iconSize)
}
}
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
self.placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
}
func transitionNode() -> ASDisplayNode? {
return self
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
}
func updatePreviewing(animated: Bool) {
var isPreviewing = false
if let (_, maybeItem, isAdd, _) = self.currentState, let interaction = self.interaction, let item = maybeItem {
if isAdd {
return
}
isPreviewing = interaction.previewedItem == .pack(item.file._parse())
}
if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing
if isPreviewing {
self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0)
if animated {
self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4)
}
} else {
self.layer.sublayerTransform = CATransform3DIdentity
if animated {
self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5)
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
public final class StickerPreviewControllerPresentationArguments {
let transitionNode: (StickerPackItem) -> ASDisplayNode?
public init(transitionNode: @escaping (StickerPackItem) -> ASDisplayNode?) {
self.transitionNode = transitionNode
}
}
public final class StickerPreviewController: ViewController {
private var controllerNode: StickerPreviewControllerNode {
return self.displayNode as! StickerPreviewControllerNode
}
private var animatedIn = false
private let context: AccountContext
private var item: StickerPackItem
public init(context: AccountContext, item: StickerPackItem) {
self.context = context
self.item = item
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = StickerPreviewControllerNode(context: self.context)
self.controllerNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.cancel = { [weak self] in
self?.dismiss()
}
self.displayNodeDidLoad()
self.controllerNode.updateItem(self.item)
//self.ready.set(self.controllerNode.ready.get())
self.ready.set(.single(true))
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedIn {
self.animatedIn = true
self.controllerNode.animateIn(sourceNode: (self.presentationArguments as? StickerPreviewControllerPresentationArguments)?.transitionNode(self.item))
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(targetNode: (self.presentationArguments as? StickerPreviewControllerPresentationArguments)?.transitionNode(self.item), completion: completion)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
public func updateItem(_ item: StickerPackItem) {
self.item = item
self.controllerNode.updateItem(item)
}
}
@@ -0,0 +1,153 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import StickerResources
final class StickerPreviewControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let context: AccountContext
private let presentationData: PresentationData
private let dimNode: ASDisplayNode
private var textNode: ASTextNode
private var imageNode: TransformImageNode
private var containerLayout: (ContainerViewLayout, CGFloat)?
private var item: StickerPackItem?
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(context: AccountContext) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.6)
self.textNode = ASTextNode()
self.imageNode = TransformImageNode()
self.imageNode.addSubnode(self.textNode)
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.addSubnode(self.dimNode)
self.addSubnode(self.imageNode)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let boundingSize = CGSize(width: 180.0, height: 180.0)
if let item = self.item, let dimensitons = item.file.dimensions {
let textSpacing: CGFloat = 10.0
let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0))
let imageSize = dimensitons.cgSize.aspectFitted(boundingSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: (layout.size.height - imageSize.height - textSpacing - textSize.height) / 4.0), size: imageSize)
self.imageNode.frame = imageFrame
self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize)
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
func animateIn(sourceNode: ASDisplayNode?) {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let sourceNode = sourceNode {
let location = sourceNode.view.convert(CGPoint(x: sourceNode.bounds.midX, y: sourceNode.bounds.midY), to: self.view)
self.imageNode.layer.animateSpring(from: NSValue(cgPoint: location), to: NSValue(cgPoint: self.imageNode.layer.position), keyPath: "position", duration: 0.6, damping: 100.0)
self.imageNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0)
}
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
func animateOut(targetNode: ASDisplayNode?, completion: (() -> Void)? = nil) {
var dimCompleted = false
var itemCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if let strongSelf = self, dimCompleted && itemCompleted {
strongSelf.dismiss?()
}
completion?()
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
dimCompleted = true
internalCompletion()
})
if let targetNode = targetNode {
let location = targetNode.view.convert(CGPoint(x: targetNode.bounds.midX, y: targetNode.bounds.midY), to: self.view)
self.imageNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false)
self.imageNode.layer.animatePosition(from: self.imageNode.layer.position, to: location, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemCompleted = true
internalCompletion()
})
self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
} else {
self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in
itemCompleted = true
internalCompletion()
})
}
}
func updateItem(_ item: StickerPackItem) {
var animateIn = false
if let _ = self.item {
animateIn = true
let previousImageNode = self.imageNode
previousImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
previousImageNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.4 as NSNumber, keyPath: "transform.scale", duration: 0.4, damping: 88.0, removeOnCompletion: false, completion: { [weak previousImageNode] _ in
previousImageNode?.removeFromSupernode()
})
self.imageNode = TransformImageNode()
self.textNode = ASTextNode()
self.imageNode.addSubnode(self.textNode)
self.addSubnode(self.imageNode)
}
self.item = item
let itemFile = item.file._parse()
for case let .Sticker(text, _, _) in itemFile.attributes {
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black)
break
}
self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: itemFile, small: false, onlyFullSize: false))
if let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
if animateIn {
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
self.imageNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.7, damping: 88.0)
}
}
}