Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "FeaturedStickersScreen",
module_name = "FeaturedStickersScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/MergeLists:MergeLists",
"//submodules/StickerPeekUI:StickerPeekUI",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/UndoUI:UndoUI",
"//submodules/ContextUI:ContextUI",
"//submodules/PremiumUI:PremiumUI",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/StickerResources:StickerResources",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TextFormat",
"//submodules/ListSectionHeaderNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,19 @@
import Foundation
import AsyncDisplayKit
import Display
import ChatPresentationInterfaceState
import TelegramPresentationData
open class ChatMediaInputPane: ASDisplayNode {
var inputNodeInteraction: ChatMediaInputNodeInteraction?
var collectionListPanelOffset: CGFloat = 0.0
var isEmpty: Bool {
return false
}
open func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
}
open func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
}
}
@@ -0,0 +1,490 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import OverlayStatusController
import AccountContext
import PresentationDataUtils
import UndoUI
import StickerResources
public final class TrendingPaneInteraction {
public let installPack: (ItemCollectionInfo) -> Void
public let openPack: (ItemCollectionInfo) -> Void
public let getItemIsPreviewed: (StickerPackItem) -> Bool
public let openSearch: () -> Void
public let itemContext = StickerPaneSearchGlobalItemContext()
public init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) {
self.installPack = installPack
self.openPack = openPack
self.getItemIsPreviewed = getItemIsPreviewed
self.openSearch = openSearch
}
}
public final class TrendingPanePackEntry: Identifiable, Comparable {
public let index: Int
public let info: StickerPackCollectionInfo.Accessor
public let theme: PresentationTheme
public let strings: PresentationStrings
public let topItems: [StickerPackItem]
public let installed: Bool
public let unread: Bool
public let topSeparator: Bool
public init(index: Int, info: StickerPackCollectionInfo.Accessor, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) {
self.index = index
self.info = info
self.theme = theme
self.strings = strings
self.topItems = topItems
self.installed = installed
self.unread = unread
self.topSeparator = topSeparator
}
public var stableId: ItemCollectionId {
return self.info.id
}
public static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.info != rhs.info {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.topItems != rhs.topItems {
return false
}
if lhs.installed != rhs.installed {
return false
}
if lhs.unread != rhs.unread {
return false
}
if lhs.topSeparator != rhs.topSeparator {
return false
}
return true
}
public static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
return lhs.index < rhs.index
}
public func item(context: AccountContext, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem {
let info = self.info
return StickerPaneSearchGlobalItem(context: context, theme: self.theme, strings: self.strings, listAppearance: false, info: self.info, topItems: self.topItems, topSeparator: self.topSeparator, regularInsets: false, installed: self.installed, unread: self.unread, open: {
interaction.openPack(info._parse())
}, install: {
interaction.installPack(info._parse())
}, getItemIsPreviewed: { item in
return interaction.getItemIsPreviewed(item)
}, itemContext: interaction.itemContext)
}
}
private enum TrendingPaneEntryId: Hashable {
case search
case pack(ItemCollectionId)
}
private enum TrendingPaneEntry: Identifiable, Comparable {
case search(theme: PresentationTheme, strings: PresentationStrings)
case pack(TrendingPanePackEntry)
var stableId: TrendingPaneEntryId {
switch self {
case .search:
return .search
case let .pack(pack):
return .pack(pack.stableId)
}
}
static func ==(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool {
switch lhs {
case let .search(lhsTheme, lhsStrings):
if case let .search(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
return true
} else {
return false
}
case let .pack(pack):
if case .pack(pack) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: TrendingPaneEntry, rhs: TrendingPaneEntry) -> Bool {
switch lhs {
case .search:
return false
case let .pack(lhsPack):
switch rhs {
case .search:
return false
case let .pack(rhsPack):
return lhsPack < rhsPack
}
}
}
func item(context: AccountContext, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem {
switch self {
case let .search(theme, strings):
return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: {
interaction.openSearch()
})
case let .pack(pack):
return pack.item(context: context, interaction: interaction, grid: grid)
}
}
}
private struct TrendingPaneTransition {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let initial: Bool
}
private func preparedTransition(from fromEntries: [TrendingPaneEntry], to toEntries: [TrendingPaneEntry], context: AccountContext, interaction: TrendingPaneInteraction, initial: Bool) -> TrendingPaneTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, grid: false), previousIndex: $0.2) }
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, grid: false)) }
return TrendingPaneTransition(deletions: deletions, insertions: insertions, updates: updates, initial: initial)
}
private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], installedPacks: Set<ItemCollectionId>, theme: PresentationTheme, strings: PresentationStrings, isPane: Bool) -> [TrendingPaneEntry] {
var result: [TrendingPaneEntry] = []
var index = 0
if isPane {
result.append(.search(theme: theme, strings: strings))
}
for item in trendingEntries {
if !installedPacks.contains(item.info.id) {
result.append(.pack(TrendingPanePackEntry(index: index, info: item.info, theme: theme, strings: strings, topItems: item.topItems, installed: installedPacks.contains(item.info.id), unread: item.unread, topSeparator: index != 0)))
index += 1
}
}
return result
}
public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
public final class Interaction {
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
let presentController: (ViewController, Any?) -> Void
let getNavigationController: () -> NavigationController?
public init(
sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool,
presentController: @escaping (ViewController, Any?) -> Void,
getNavigationController: @escaping () -> NavigationController?
) {
self.sendSticker = sendSticker
self.presentController = presentController
self.getNavigationController = getNavigationController
}
}
private let context: AccountContext
private let forceTheme: PresentationTheme?
private let interaction: ChatMediaInputTrendingPane.Interaction
private let getItemIsPreviewed: (StickerPackItem) -> Bool
private let isPane: Bool
public let gridNode: GridNode
private var enqueuedTransitions: [TrendingPaneTransition] = []
private var validLayout: (CGSize, CGFloat)?
private var disposable: Disposable?
private var isActivated = false
private let _ready = Promise<Void>()
private var didSetReady = false
public var ready: Signal<Void, NoError> {
return self._ready.get()
}
public var scrollingInitiated: (() -> Void)?
public var stickerActionTitle: String?
private let installDisposable = MetaDisposable()
public init(context: AccountContext, forceTheme: PresentationTheme?, interaction: ChatMediaInputTrendingPane.Interaction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) {
self.context = context
self.forceTheme = forceTheme
self.interaction = interaction
self.getItemIsPreviewed = getItemIsPreviewed
self.isPane = isPane
self.gridNode = GridNode()
super.init()
self.addSubnode(self.gridNode)
self.gridNode.scrollingInitiated = { [weak self] in
self?.scrollingInitiated?()
}
}
deinit {
self.disposable?.dispose()
self.installDisposable.dispose()
}
public func activate() {
if self.isActivated {
return
}
self.isActivated = true
let interaction = TrendingPaneInteraction(installPack: { [weak self] info in
if let strongSelf = self, let info = info as? StickerPackCollectionInfo {
let context = strongSelf.context
var installSignal = context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
let parsedInfo = info._parse()
return preloadedStickerPackThumbnail(account: context.account, info: info, items: items)
|> filter { $0 }
|> ignoreValues
|> then(
context.engine.stickers.addStickerPackInteractively(info: parsedInfo, items: items)
|> ignoreValues
)
|> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
}
|> then(.single((parsedInfo, items)))
}
case .fetching:
break
case .none:
break
}
return .complete()
}
|> deliverOnMainQueue
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme = self?.forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.interaction.presentController(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(1.0, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
installSignal = installSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
self?.installDisposable.set(nil)
}
strongSelf.installDisposable.set(installSignal.start(next: { info, items in
guard let strongSelf = self else {
return
}
var animateInAsReplacement = false
if let navigationController = strongSelf.interaction.getNavigationController() {
for controller in navigationController.overlayControllers {
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitActionAndReplacementAnimation()
animateInAsReplacement = true
}
}
}
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme = strongSelf.forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
strongSelf.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in
return true
}))
}))
}
}, openPack: { [weak self] info in
if let strongSelf = self, let info = info as? StickerPackCollectionInfo {
strongSelf.view.window?.endEditing(true)
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
var updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?
if let forceTheme = strongSelf.forceTheme {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: forceTheme)
updatedPresentationData = (presentationData, .single(presentationData))
}
let controller = strongSelf.context.sharedContext.makeStickerPackScreen(
context: strongSelf.context,
updatedPresentationData: updatedPresentationData,
mainStickerPack: packReference,
stickerPacks: [packReference],
loadedStickerPacks: [],
actionTitle: strongSelf.stickerActionTitle,
isEditing: false,
expandIfNeeded: false,
parentNavigationController: strongSelf.interaction.getNavigationController(),
sendSticker: { fileReference, sourceNode, sourceRect in
if let strongSelf = self {
return strongSelf.interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
} else {
return false
}
},
actionPerformed: nil
)
strongSelf.interaction.presentController(controller, nil)
}
}, getItemIsPreviewed: self.getItemIsPreviewed,
openSearch: { [weak self] in
self?.inputNodeInteraction?.toggleSearch(true, .trending, "")
})
interaction.itemContext.canPlayMedia = true
let isPane = self.isPane
let previousEntries = Atomic<[TrendingPaneEntry]?>(value: nil)
let context = self.context
let forceTheme = self.forceTheme
self.disposable = (combineLatest(context.account.viewTracker.featuredStickerPacks(), context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]), context.sharedContext.presentationData)
|> map { trendingEntries, view, presentationData -> TrendingPaneTransition in
var presentationData = presentationData
if let forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
var installedPacks = Set<ItemCollectionId>()
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView {
if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
for entry in packsEntries {
installedPacks.insert(entry.id)
}
}
}
let entries = trendingPaneEntries(trendingEntries: trendingEntries, installedPacks: installedPacks, theme: presentationData.theme, strings: presentationData.strings, isPane: isPane)
let previous = previousEntries.swap(entries)
return preparedTransition(from: previous ?? [], to: entries, context: context, interaction: interaction, initial: previous == nil)
}
|> deliverOnMainQueue).start(next: { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.enqueueTransition(transition)
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(Void()))
}
}).strict()
}
public override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
let hadValidLayout = self.validLayout != nil
self.validLayout = (size, bottomInset)
let itemSize: CGSize
if case .tablet = deviceMetrics.type, size.width > 480.0 {
itemSize = CGSize(width: floor(size.width / 2.0), height: 128.0)
} else {
itemSize = CGSize(width: size.width, height: 128.0)
}
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: isVisible ? 300.0 : 0.0, type: .fixed(itemSize: itemSize, fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
transition.updateFrame(node: self.gridNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func enqueueTransition(_ transition: TrendingPaneTransition) {
self.enqueuedTransitions.append(transition)
if self.validLayout != nil {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
public override func willEnterHierarchy() {
super.willEnterHierarchy()
self.activate()
}
private func dequeueTransition() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
let itemTransition: ContainedViewLayoutTransition = .immediate
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: transition.initial), completion: { _ in })
}
}
public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
let localPoint = self.view.convert(point, to: self.gridNode.view)
var resultNode: StickerPaneSearchGlobalItemNode?
self.gridNode.forEachItemNode { itemNode in
if itemNode.frame.contains(localPoint), let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
resultNode = itemNode
}
}
if let resultNode = resultNode {
return resultNode.itemAt(point: self.gridNode.view.convert(localPoint, to: resultNode.view))
}
return nil
}
public func updatePreviewing(animated: Bool) {
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
itemNode.updatePreviewing(animated: animated)
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,198 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ShimmerEffect
private let titleFont = Font.bold(16.0)
private let statusFont = Font.regular(15.0)
private let buttonFont = Font.medium(13.0)
final class TrendingTopItemNode: ASDisplayNode {
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode?
public private(set) var file: TelegramMediaFile? = nil
public private(set) var theme: PresentationTheme?
private var listAppearance = false
private var itemSize: CGSize?
private let loadDisposable = MetaDisposable()
var currentIsPreviewing = false
var visibility: Bool = false {
didSet {
if oldValue != self.visibility {
self.animationNode?.visibility = self.visibility
}
}
}
override init() {
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode?.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.imageNode)
if let placeholderNode = self.placeholderNode {
self.addSubnode(placeholderNode)
}
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
}
firstTime = false
}
}
deinit {
self.loadDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
if !animated {
placeholderNode.removeFromSupernode()
} else {
placeholderNode.allowsGroupOpacity = true
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
placeholderNode?.removeFromSupernode()
placeholderNode?.allowsGroupOpacity = false
})
}
}
}
private var absoluteLocation: (CGRect, CGSize)?
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteLocation = (rect, containerSize)
if let placeholderNode = placeholderNode {
placeholderNode.updateAbsoluteRect(rect, within: containerSize)
}
}
func update(theme: PresentationTheme, listAppearance: Bool) {
self.theme = theme
self.listAppearance = listAppearance
let backgroundColor: UIColor?
let foregroundColor: UIColor
let shimmeringColor: UIColor
if listAppearance {
backgroundColor = nil
foregroundColor = theme.list.itemPlainSeparatorColor.blitOver(theme.list.plainBackgroundColor, alpha: 0.3)
shimmeringColor = theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)
} else {
let color = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
backgroundColor = color
foregroundColor = theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(color, alpha: 0.15)
shimmeringColor = theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3)
}
if let placeholderNode = self.placeholderNode, let file = self.file {
placeholderNode.update(backgroundColor: backgroundColor, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, data: file.immediateThumbnailData, size: self.itemSize ?? CGSize(width: 75.0, height: 75.0), enableEffect: true)
}
}
func setup(account: Account, item: StickerPackItem, itemSize: CGSize, synchronousLoads: Bool) {
let file = item.file._parse()
self.file = file
self.itemSize = itemSize
if item.file.isAnimatedSticker || item.file.isVideoSticker {
let animationNode: AnimatedStickerNode
if let currentAnimationNode = self.animationNode {
animationNode = currentAnimationNode
} else {
animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.transform = self.imageNode.transform
animationNode.visibility = self.visibility
self.animationNode = animationNode
if let placeholderNode = self.placeholderNode {
self.insertSubnode(animationNode, belowSubnode: placeholderNode)
} else {
self.addSubnode(animationNode)
}
}
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
if item.file.isVideoSticker {
self.imageNode.setSignal(chatMessageSticker(postbox: account.postbox, userLocation: .other, file: file, small: false, synchronousLoad: synchronousLoads))
} else {
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, userLocation: .other, file: file, small: false, size: fittedDimensions, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
}
animationNode.started = { [weak self] in
self?.imageNode.alpha = 0.0
}
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached)
self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
} else {
self.imageNode.setSignal(chatMessageSticker(account: account, userLocation: .other, file: file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
if let currentAnimationNode = self.animationNode {
self.animationNode = nil
currentAnimationNode.removeFromSupernode()
}
self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: true)).start())
}
}
func updatePreviewing(animated: Bool, isPreviewing: Bool) {
if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing
if isPreviewing {
if animated {
self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "transform.scale", duration: 0.4, removeOnCompletion: false)
}
} else {
self.layer.removeAnimation(forKey: "transform.scale")
if animated {
self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
}
}
}
}
override func layout() {
super.layout()
if let dimensions = self.file?.dimensions, let itemSize = self.itemSize {
let imageSize = dimensions.cgSize.aspectFitted(itemSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
}
self.imageNode.frame = self.bounds
self.animationNode?.updateLayout(size: self.bounds.size)
let size = self.bounds.size
let boundingSize = size
if let placeholderNode = self.placeholderNode {
let placeholderFrame = CGRect(origin: CGPoint(x: floor((size.width - boundingSize.width) / 2.0), y: floor((size.height - boundingSize.height) / 2.0)), size: boundingSize)
placeholderNode.frame = placeholderFrame
if let theme = self.theme {
self.update(theme: theme, listAppearance: self.listAppearance)
}
}
}
}
@@ -0,0 +1,130 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe")
private func generateLoupeIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: templateLoupeIcon, color: color)
}
public enum PaneSearchBarType {
case stickers
case gifs
}
public final class PaneSearchBarPlaceholderItem: GridItem {
public let theme: PresentationTheme
public let strings: PresentationStrings
public let type: PaneSearchBarType
public let activate: () -> Void
public let section: GridSection? = nil
public let fillsRowWithHeight: (CGFloat, Bool)? = (56.0, true)
public init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.type = type
self.activate = activate
}
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = PaneSearchBarPlaceholderNode()
node.activate = self.activate
node.setup(theme: self.theme, strings: self.strings, type: self.type)
return node
}
public func update(node: GridItemNode) {
guard let node = node as? PaneSearchBarPlaceholderNode else {
assertionFailure()
return
}
node.activate = self.activate
node.setup(theme: self.theme, strings: self.strings, type: self.type)
}
}
public final class PaneSearchBarPlaceholderNode: GridItemNode {
private var currentState: (PresentationTheme, PresentationStrings, PaneSearchBarType)?
public var activate: (() -> Void)?
public let backgroundNode: ASImageNode
public let labelNode: ImmediateTextNode
public let iconNode: ASImageNode
public override init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.isUserInteractionEnabled = false
self.labelNode = ImmediateTextNode()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
super.init()
self.isAccessibilityElement = true
self.accessibilityTraits = .searchField
self.addSubnode(self.backgroundNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.iconNode)
}
public override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
public func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) {
if self.currentState?.0 !== theme || self.currentState?.1 !== strings || self.currentState?.2 != type {
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor)
self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor)
let placeholder: String
switch type {
case .stickers:
placeholder = strings.Stickers_Search
case .gifs:
placeholder = strings.Gif_Search
}
self.labelNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
self.accessibilityLabel = placeholder
self.currentState = (theme, strings, type)
}
}
public override func layout() {
super.layout()
let bounds = self.bounds
let backgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 12.0), size: CGSize(width: bounds.width - 8.0 * 2.0, height: 36.0))
self.backgroundNode.frame = backgroundFrame
let textSize = self.labelNode.updateLayout(bounds.size)
let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - textSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.height - textSize.height) / 2.0)), size: textSize)
self.labelNode.frame = textFrame
if let iconImage = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - iconImage.size.width - 6.0, y: floorToScreenPixels(textFrame.midY - iconImage.size.height / 2.0)), size: iconImage.size)
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.activate?()
}
}
}
@@ -0,0 +1,557 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import ListSectionHeaderNode
import AccountContext
import StickerResources
final class StickerPaneSearchGlobalSection: GridSection {
let title: String?
let theme: PresentationTheme
var height: CGFloat {
if let _ = self.title {
return 28.0
} else {
return 0.0
}
}
var hashValue: Int {
if let _ = self.title {
return 1
} else {
return 0
}
}
init(title: String?, theme: PresentationTheme) {
self.title = title
self.theme = theme
}
func isEqual(to: GridSection) -> Bool {
if let to = to as? StickerPaneSearchGlobalSection {
return to.hashValue == self.hashValue
} else {
return false
}
}
func node() -> ASDisplayNode {
return StickerPaneSearchGlobalSectionNode(theme: self.theme, title: self.title ?? "")
}
}
private final class StickerPaneSearchGlobalSectionNode: ASDisplayNode {
private let node: ListSectionHeaderNode
init(theme: PresentationTheme, title: String) {
self.node = ListSectionHeaderNode(theme: theme)
super.init()
if !title.isEmpty {
self.node.title = title
self.addSubnode(self.node)
}
}
override func layout() {
super.layout()
self.node.frame = self.bounds
self.node.updateLayout(size: self.bounds.size, leftInset: 0.0, rightInset: 0.0)
}
}
public final class StickerPaneSearchGlobalItemContext {
public var canPlayMedia: Bool
public init(canPlayMedia: Bool = false) {
self.canPlayMedia = canPlayMedia
}
}
public final class StickerPaneSearchGlobalItem: GridItem {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let listAppearance: Bool
public let fillsRow: Bool
public let info: StickerPackCollectionInfo.Accessor
public let topItems: [StickerPackItem]
public let topSeparator: Bool
public let regularInsets: Bool
public let installed: Bool
public let installing: Bool
public let unread: Bool
public let open: () -> Void
public let install: () -> Void
public let getItemIsPreviewed: (StickerPackItem) -> Bool
public let itemContext: StickerPaneSearchGlobalItemContext
public let section: GridSection?
public var fillsRowWithHeight: (CGFloat, Bool)? {
var additionalHeight: CGFloat = 0.0
if self.regularInsets {
additionalHeight = 12.0 + 12.0
} else {
additionalHeight += 12.0
if self.topSeparator {
additionalHeight += 12.0
}
}
return (128.0 + additionalHeight, self.fillsRow)
}
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo.Accessor, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) {
self.context = context
self.theme = theme
self.strings = strings
self.listAppearance = listAppearance
self.fillsRow = fillsRow
self.info = info
self.topItems = topItems
self.topSeparator = topSeparator
self.regularInsets = regularInsets
self.installed = installed
self.installing = installing
self.unread = unread
self.open = open
self.install = install
self.getItemIsPreviewed = getItemIsPreviewed
self.itemContext = itemContext
self.section = StickerPaneSearchGlobalSection(title: sectionTitle, theme: theme)
}
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPaneSearchGlobalItemNode()
node.setup(item: self)
return node
}
public func update(node: GridItemNode) {
guard let node = node as? StickerPaneSearchGlobalItemNode else {
assertionFailure()
return
}
node.setup(item: self)
}
}
private let titleFont = Font.bold(16.0)
private let statusFont = Font.regular(15.0)
private let buttonFont = Font.semibold(13.0)
public class StickerPaneSearchGlobalItemNode: GridItemNode {
private let titleNode: TextNode
private let descriptionNode: TextNode
private let unreadNode: ASImageNode
private let installTextNode: TextNode
private let installBackgroundNode: ASImageNode
private let installButtonNode: HighlightTrackingButtonNode
private let uninstallTextNode: TextNode
private let uninstallBackgroundNode: ASImageNode
private let uninstallButtonNode: HighlightTrackingButtonNode
private var itemNodes: [TrendingTopItemNode]
private let topSeparatorNode: ASDisplayNode
private var highlightNode: ASDisplayNode?
public var item: StickerPaneSearchGlobalItem?
private var appliedItem: StickerPaneSearchGlobalItem?
private let preloadDisposable = MetaDisposable()
private let preloadedStickerPackThumbnailDisposable = MetaDisposable()
private var preloadedThumbnail = false
private var canPlay = false
private var canPlayMedia: Bool = false {
didSet {
if self.canPlayMedia != oldValue {
self.updatePlayback()
}
}
}
public override var isVisibleInGrid: Bool {
didSet {
if oldValue != self.isVisibleInGrid {
self.updatePlayback()
}
}
}
private func updatePlayback() {
let canPlay = self.canPlayMedia && self.isVisibleInGrid
if canPlay != self.canPlay {
self.canPlay = canPlay
for node in self.itemNodes {
node.visibility = self.canPlay
}
if let item = self.item, self.isVisibleInGrid, !self.preloadedThumbnail {
self.preloadedThumbnail = true
self.preloadedStickerPackThumbnailDisposable.set(preloadedStickerPackThumbnail(account: item.context.account, info: item.info, items: item.topItems).start())
}
}
}
public override init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.descriptionNode = TextNode()
self.descriptionNode.isUserInteractionEnabled = false
self.descriptionNode.contentMode = .left
self.descriptionNode.contentsScale = UIScreen.main.scale
self.unreadNode = ASImageNode()
self.unreadNode.isLayerBacked = true
self.unreadNode.displayWithoutProcessing = true
self.unreadNode.displaysAsynchronously = false
self.installTextNode = TextNode()
self.installTextNode.isUserInteractionEnabled = false
self.installTextNode.contentMode = .left
self.installTextNode.contentsScale = UIScreen.main.scale
self.installBackgroundNode = ASImageNode()
self.installBackgroundNode.isLayerBacked = true
self.installBackgroundNode.displayWithoutProcessing = true
self.installBackgroundNode.displaysAsynchronously = false
self.installButtonNode = HighlightTrackingButtonNode()
self.uninstallTextNode = TextNode()
self.uninstallTextNode.isUserInteractionEnabled = false
self.uninstallTextNode.contentMode = .left
self.uninstallTextNode.contentsScale = UIScreen.main.scale
self.uninstallBackgroundNode = ASImageNode()
self.uninstallBackgroundNode.isLayerBacked = true
self.uninstallBackgroundNode.displayWithoutProcessing = true
self.uninstallBackgroundNode.displaysAsynchronously = false
self.uninstallButtonNode = HighlightTrackingButtonNode()
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.itemNodes = []
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.descriptionNode)
self.addSubnode(self.unreadNode)
self.addSubnode(self.installBackgroundNode)
self.addSubnode(self.installTextNode)
self.addSubnode(self.installButtonNode)
self.addSubnode(self.uninstallBackgroundNode)
self.addSubnode(self.uninstallTextNode)
self.addSubnode(self.uninstallButtonNode)
self.addSubnode(self.topSeparatorNode)
self.installButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.installBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.installBackgroundNode.alpha = 0.4
strongSelf.installTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.installTextNode.alpha = 0.4
} else {
strongSelf.installBackgroundNode.alpha = 1.0
strongSelf.installBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.installTextNode.alpha = 1.0
strongSelf.installTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.installButtonNode.addTarget(self, action: #selector(self.installPressed), forControlEvents: .touchUpInside)
self.uninstallButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.uninstallBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.uninstallBackgroundNode.alpha = 0.4
strongSelf.uninstallTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.uninstallTextNode.alpha = 0.4
} else {
strongSelf.uninstallBackgroundNode.alpha = 1.0
strongSelf.uninstallBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.uninstallTextNode.alpha = 1.0
strongSelf.uninstallTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.uninstallButtonNode.addTarget(self, action: #selector(self.installPressed), forControlEvents: .touchUpInside)
}
deinit {
self.preloadDisposable.dispose()
self.preloadedStickerPackThumbnailDisposable.dispose()
}
public override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
private var absoluteLocation: (CGRect, CGSize)?
public override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteLocation = (rect, containerSize)
for node in self.itemNodes {
let nodeRect = CGRect(origin: CGPoint(x: rect.minX + node.frame.minX, y: rect.minY + node.frame.minY), size: node.frame.size)
node.updateAbsoluteRect(nodeRect, within: containerSize)
}
}
public func setup(item: StickerPaneSearchGlobalItem) {
if item.topItems.count < Int(item.info.count) && item.topItems.count < 5 && self.item?.info.id != item.info.id {
self.preloadDisposable.set(preloadedFeaturedStickerSet(network: item.context.account.network, postbox: item.context.account.postbox, id: item.info.id).start())
}
self.item = item
self.setNeedsLayout()
self.updatePreviewing(animated: false)
}
public func updateCanPlayMedia() {
guard let item = self.item else {
return
}
self.canPlayMedia = item.itemContext.canPlayMedia && item.context.sharedContext.energyUsageSettings.loopStickers
}
public func highlight() {
guard self.highlightNode == nil else {
return
}
let highlightNode = ASDisplayNode()
highlightNode.frame = self.bounds
if let theme = self.item?.theme {
highlightNode.backgroundColor = theme.list.itemCheckColors.fillColor.withAlphaComponent(0.08)
}
self.highlightNode = highlightNode
self.insertSubnode(highlightNode, at: 0)
Queue.mainQueue().after(1.5) {
self.highlightNode = nil
highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak highlightNode] _ in
highlightNode?.removeFromSupernode()
})
}
}
public override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
guard let item = self.item else {
return
}
let params = ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: size.height)
let topSeparatorOffset: CGFloat
var topOffset: CGFloat = 0.0
if item.regularInsets {
topOffset = 12.0
topSeparatorOffset = -UIScreenPixel
} else {
topSeparatorOffset = 16.0
topOffset += 12.0
if item.topSeparator {
topOffset += 12.0
}
}
self.topSeparatorNode.isHidden = !item.topSeparator
self.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 16.0, y: topSeparatorOffset), size: CGSize(width: params.width - 16.0 * 2.0, height: UIScreenPixel))
if item.listAppearance {
self.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
} else {
self.topSeparatorNode.backgroundColor = item.theme.chat.inputMediaPanel.stickersSectionTextColor.withAlphaComponent(0.3)
}
let makeInstallLayout = TextNode.asyncLayout(self.installTextNode)
let makeUninstallLayout = TextNode.asyncLayout(self.uninstallTextNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
let currentItem = self.appliedItem
self.appliedItem = item
var updateButtonBackgroundImage: UIImage?
var updateUninstallButtonBackgroundImage: UIImage?
if currentItem?.theme !== item.theme {
updateUninstallButtonBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddedPackButtonImage(item.theme)
updateButtonBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.theme)
}
let unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.theme)
let leftInset: CGFloat = 14.0
let rightInset: CGFloat = 16.0
let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Install, font: buttonFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (uninstallLayout, uninstallApply) = makeUninstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Installed, font: buttonFont, textColor: item.theme.list.itemCheckColors.fillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.info.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - max(installLayout.size.width, uninstallLayout.size.width), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.StickerPack_StickerCount(item.info.count), font: statusFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let strongSelf = self
let _ = installApply()
let _ = uninstallApply()
let _ = titleApply()
let _ = descriptionApply()
if let updateButtonBackgroundImage = updateButtonBackgroundImage {
strongSelf.installBackgroundNode.image = updateButtonBackgroundImage
}
if let updateUninstallButtonBackgroundImage = updateUninstallButtonBackgroundImage {
strongSelf.uninstallBackgroundNode.image = updateUninstallButtonBackgroundImage
}
let installWidth: CGFloat = installLayout.size.width + 32.0
let buttonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - installWidth, y: 4.0 + topOffset), size: CGSize(width: installWidth, height: 28.0))
strongSelf.installBackgroundNode.frame = buttonFrame
strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size)
strongSelf.installButtonNode.frame = buttonFrame
let uninstallWidth: CGFloat = uninstallLayout.size.width + 32.0
let uninstallButtonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - uninstallWidth, y: 4.0 + topOffset), size: CGSize(width: uninstallWidth, height: 28.0))
strongSelf.uninstallBackgroundNode.frame = uninstallButtonFrame
strongSelf.uninstallTextNode.frame = CGRect(origin: CGPoint(x: uninstallButtonFrame.minX + floor((uninstallButtonFrame.width - uninstallLayout.size.width) / 2.0), y: uninstallButtonFrame.minY + floor((uninstallButtonFrame.height - uninstallLayout.size.height) / 2.0) + 1.0), size: uninstallLayout.size)
strongSelf.uninstallButtonNode.frame = uninstallButtonFrame
strongSelf.installButtonNode.isHidden = item.installed
strongSelf.installBackgroundNode.isHidden = item.installed
strongSelf.installTextNode.isHidden = item.installed
strongSelf.uninstallButtonNode.isHidden = !item.installed
strongSelf.uninstallBackgroundNode.isHidden = !item.installed
strongSelf.uninstallTextNode.isHidden = !item.installed
let titleFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 2.0 + topOffset), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 23.0 + topOffset), size: descriptionLayout.size)
if item.unread {
strongSelf.unreadNode.isHidden = false
} else {
strongSelf.unreadNode.isHidden = true
}
if let image = unreadImage {
strongSelf.unreadNode.image = image
strongSelf.unreadNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: titleFrame.minY + 7.0), size: image.size)
}
let sideInset: CGFloat = 2.0
let availableWidth = params.width - params.leftInset - params.rightInset - sideInset * 2.0
var itemSide: CGFloat = floor(availableWidth / 5.0)
itemSide = min(itemSide, 75.0)
let itemSize = CGSize(width: itemSide, height: itemSide)
var offset = sideInset
let itemSpacing = (max(0, availableWidth - 5.0 * itemSide - sideInset * 2.0)) / 4.0
var topItems = item.topItems
if topItems.count > 5 {
topItems.removeSubrange(5 ..< topItems.count)
}
for i in 0 ..< topItems.count {
let file = topItems[i].file
let node: TrendingTopItemNode
if i < strongSelf.itemNodes.count {
node = strongSelf.itemNodes[i]
} else {
node = TrendingTopItemNode()
node.visibility = strongSelf.canPlay
strongSelf.itemNodes.append(node)
strongSelf.addSubnode(node)
}
if file.fileId != node.file?.fileId {
node.setup(account: item.context.account, item: topItems[i], itemSize: itemSize, synchronousLoads: synchronousLoads)
}
if item.theme !== node.theme {
node.update(theme: item.theme, listAppearance: item.listAppearance)
}
if let dimensions = file.dimensions {
let imageSize = dimensions.cgSize.aspectFitted(itemSize)
node.frame = CGRect(origin: CGPoint(x: offset, y: 48.0 + topOffset), size: imageSize)
offset += itemSize.width + itemSpacing
}
if let (rect, size) = strongSelf.absoluteLocation {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
if topItems.count < strongSelf.itemNodes.count {
for i in (topItems.count ..< strongSelf.itemNodes.count).reversed() {
strongSelf.itemNodes[i].removeFromSupernode()
strongSelf.itemNodes.remove(at: i)
}
}
self.canPlayMedia = item.itemContext.canPlayMedia && item.context.sharedContext.energyUsageSettings.loopStickers
}
@objc func installPressed() {
if let item = self.item {
item.install()
}
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let item = self.item {
item.open()
}
}
}
public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
guard let item = self.item else {
return nil
}
var index = 0
for itemNode in self.itemNodes {
if itemNode.frame.contains(point), index < item.topItems.count {
return (itemNode, item.topItems[index])
}
index += 1
}
return nil
}
public func updatePreviewing(animated: Bool) {
guard let item = self.item else {
return
}
var index = 0
for itemNode in self.itemNodes {
if index < item.topItems.count {
let isPreviewing = item.getItemIsPreviewed(item.topItems[index])
itemNode.updatePreviewing(animated: animated, isPreviewing: isPreviewing)
}
index += 1
}
}
}
@@ -0,0 +1,256 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import TelegramPresentationData
import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ChatPresentationInterfaceState
import EmojiTextAttachmentView
import TextFormat
final class StickerPaneSearchStickerSection: GridSection {
let code: String
let theme: PresentationTheme
let height: CGFloat = 26.0
var hashValue: Int {
return self.code.hashValue
}
init(code: String, theme: PresentationTheme) {
self.code = code
self.theme = theme
}
func isEqual(to: GridSection) -> Bool {
if let to = to as? StickerPaneSearchStickerSection {
return self.code == to.code && self.theme === to.theme
} else {
return false
}
}
func node() -> ASDisplayNode {
return StickerPaneSearchStickerSectionNode(code: self.code, theme: self.theme)
}
}
private let sectionTitleFont = Font.medium(12.0)
final class StickerPaneSearchStickerSectionNode: ASDisplayNode {
let titleNode: ASTextNode
init(code: String, theme: PresentationTheme) {
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
super.init()
self.titleNode.attributedText = NSAttributedString(string: code, font: sectionTitleFont, textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
self.addSubnode(self.titleNode)
}
override func layout() {
super.layout()
let bounds = self.bounds
let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 9.0), size: titleSize)
}
}
public final class StickerPaneSearchStickerItem: GridItem {
public let context: AccountContext
public let theme: PresentationTheme
public let code: String?
public let stickerItem: FoundStickerItem
public let selected: (ASDisplayNode, CALayer, CGRect) -> Void
public let inputNodeInteraction: ChatMediaInputNodeInteraction
public let section: GridSection?
public init(context: AccountContext, theme: PresentationTheme, code: String?, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, selected: @escaping (ASDisplayNode, CALayer, CGRect) -> Void) {
self.context = context
self.theme = theme
self.stickerItem = stickerItem
self.inputNodeInteraction = inputNodeInteraction
self.selected = selected
self.code = code
self.section = nil
}
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPaneSearchStickerItemNode()
node.inputNodeInteraction = self.inputNodeInteraction
node.setup(context: self.context, theme: self.theme, stickerItem: self.stickerItem, code: self.code)
node.selected = self.selected
return node
}
public func update(node: GridItemNode) {
guard let node = node as? StickerPaneSearchStickerItemNode else {
assertionFailure()
return
}
node.inputNodeInteraction = self.inputNodeInteraction
node.setup(context: self.context, theme: self.theme, stickerItem: self.stickerItem, code: self.code)
node.selected = self.selected
}
}
private let textFont = Font.regular(20.0)
public final class StickerPaneSearchStickerItemNode: GridItemNode {
private var currentState: (AccountContext, FoundStickerItem, CGSize)?
var itemLayer: InlineStickerItemLayer?
private let textNode: ASTextNode
private let stickerFetchedDisposable = MetaDisposable()
public var currentIsPreviewing = false
public override var isVisibleInGrid: Bool {
didSet {
self.updateVisibility()
}
}
private var isPlaying = false
public var inputNodeInteraction: ChatMediaInputNodeInteraction?
public var selected: ((ASDisplayNode, CALayer, CGRect) -> Void)?
public var stickerItem: FoundStickerItem? {
return self.currentState?.1
}
public override init() {
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
super.init()
self.textNode.maximumNumberOfLines = 1
self.addSubnode(self.textNode)
}
deinit {
self.stickerFetchedDisposable.dispose()
}
public override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func setup(context: AccountContext, theme: PresentationTheme, stickerItem: FoundStickerItem, code: String?) {
if self.currentState == nil || self.currentState!.0 !== context || self.currentState!.1 != stickerItem {
self.textNode.attributedText = NSAttributedString(string: code ?? "", font: textFont, textColor: .black)
let file = stickerItem.file
let itemDimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
let playbackItemSize = CGSize(width: 96.0, height: 96.0)
let itemPlaybackSize = itemDimensions.aspectFitted(playbackItemSize)
let itemLayer: InlineStickerItemLayer
if let current = self.itemLayer {
itemLayer = current
itemLayer.dynamicColor = .white
} else {
itemLayer = InlineStickerItemLayer(
context: context,
userLocation: .other,
attemptSynchronousLoad: false,
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file),
file: file,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1),
pointSize: itemPlaybackSize,
dynamicColor: .white
)
self.itemLayer = itemLayer
self.layer.insertSublayer(itemLayer, at: 0)
}
self.currentState = (context, stickerItem, itemDimensions)
self.setNeedsLayout()
self.updateVisibility()
}
}
public override func layout() {
super.layout()
let bounds = self.bounds
let boundingSize = bounds.insetBy(dx: 6.0, dy: 6.0).size
if let (_, _, itemDimensions) = self.currentState {
let itemSize = itemDimensions.aspectFitted(boundingSize)
let itemFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - itemSize.width) / 2.0), y: (bounds.size.height - itemSize.height) / 2.0), size: itemSize)
if let itemLayer = self.itemLayer {
itemLayer.frame = itemFrame
}
let textSize = self.textNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
self.textNode.frame = CGRect(origin: CGPoint(x: bounds.size.width - textSize.width, y: bounds.size.height - textSize.height), size: textSize)
}
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
guard let itemLayer = self.itemLayer else {
return
}
self.selected?(self, itemLayer, self.bounds)
}
public func transitionNode() -> ASDisplayNode? {
return self
}
public func updateVisibility() {
guard let context = self.currentState?.0 else {
return
}
let isPlaying = self.isVisibleInGrid && context.sharedContext.energyUsageSettings.loopStickers
if self.isPlaying != isPlaying, let itemLayer = self.itemLayer {
self.isPlaying = isPlaying
itemLayer.isVisibleForAnimations = isPlaying
}
}
public func updatePreviewing(animated: Bool) {
var isPreviewing = false
if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction {
isPreviewing = interaction.previewedStickerPackItemFile?.id == item.file.id
}
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)
}
}
}
}
}