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() 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 } }