Swiftgram/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift
2019-12-03 21:23:18 +04:00

732 lines
39 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import ActivityIndicator
import TextFormat
import AccountContext
private struct StickerPackPreviewGridEntry: Comparable, Identifiable {
let index: Int
let stickerItem: StickerPackItem
var stableId: MediaId {
return self.stickerItem.file.fileId
}
static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem {
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction)
}
}
private struct StickerPackPreviewGridTransaction {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
self.deletions = deleteIndices
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) }
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) }
}
}
final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private weak var controller: StickerPackPreviewController?
private let context: AccountContext
private let openShare: (() -> Void)?
private var presentationData: PresentationData
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
private let cancelButtonNode: ASButtonNode
private let contentContainerNode: ASDisplayNode
private let contentBackgroundNode: ASImageNode
private let contentGridNode: GridNode
private let actionsSeparatorNode: ASDisplayNode
private let actionsBackgroundNode: ASDisplayNode
private let installActionButtonNode: HighlightTrackingButtonNode
private var installActionButtonIsRemove = false
private let shareActionButtonNode: HighlightTrackingButtonNode
private let contentTitleNode: ImmediateTextNode
private let contentSeparatorNode: ASDisplayNode
private var activityIndicator: ActivityIndicator?
private var interaction: StickerPackPreviewInteraction!
var presentInGlobalOverlay: ((ViewController, Any?) -> Void)?
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
let ready = Promise<Bool>()
private var didSetReady = false
private var stickerPack: LoadedStickerPack?
private var stickerPackUpdated = false
private var stickerPackInitiallyInstalled : Bool?
private var stickerSettings: StickerSettings?
private var currentItems: [StickerPackPreviewGridEntry] = []
private var hapticFeedback: HapticFeedback?
init(controller: StickerPackPreviewController, context: AccountContext, openShare: (() -> Void)?, openMention: @escaping (String) -> Void) {
self.controller = controller
self.context = context
self.openShare = openShare
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.cancelButtonNode = ASButtonNode()
self.cancelButtonNode.displaysAsynchronously = false
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.isOpaque = false
self.contentContainerNode.clipsToBounds = true
self.contentBackgroundNode = ASImageNode()
self.contentBackgroundNode.displaysAsynchronously = false
self.contentBackgroundNode.displayWithoutProcessing = true
self.contentGridNode = GridNode()
self.installActionButtonNode = HighlightTrackingButtonNode()
self.installActionButtonNode.displaysAsynchronously = false
self.installActionButtonNode.titleNode.displaysAsynchronously = false
self.shareActionButtonNode = HighlightTrackingButtonNode()
self.shareActionButtonNode.displaysAsynchronously = false
self.shareActionButtonNode.titleNode.displaysAsynchronously = false
self.contentTitleNode = ImmediateTextNode()
self.contentTitleNode.displaysAsynchronously = false
self.contentTitleNode.maximumNumberOfLines = 1
self.contentSeparatorNode = ASDisplayNode()
self.contentSeparatorNode.isLayerBacked = true
self.actionsSeparatorNode = ASDisplayNode()
self.actionsSeparatorNode.isLayerBacked = true
self.actionsBackgroundNode = ASDisplayNode()
super.init()
self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: false)
self.backgroundColor = nil
self.isOpaque = false
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.addSubnode(self.dimNode)
self.wrappingScrollNode.view.delegate = self
self.addSubnode(self.wrappingScrollNode)
self.installActionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside)
self.installActionButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.installActionButtonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.installActionButtonNode.alpha = 0.8
} else {
strongSelf.installActionButtonNode.alpha = 1.0
strongSelf.installActionButtonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3)
}
}
}
self.shareActionButtonNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
self.shareActionButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.shareActionButtonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.shareActionButtonNode.alpha = 0.8
} else {
strongSelf.shareActionButtonNode.alpha = 1.0
strongSelf.shareActionButtonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3)
}
}
}
self.wrappingScrollNode.clipsToBounds = false
self.wrappingScrollNode.addSubnode(self.contentBackgroundNode)
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.contentContainerNode.addSubnode(self.contentGridNode)
self.wrappingScrollNode.addSubnode(self.actionsBackgroundNode)
self.wrappingScrollNode.addSubnode(self.actionsSeparatorNode)
self.wrappingScrollNode.addSubnode(self.installActionButtonNode)
self.wrappingScrollNode.addSubnode(self.shareActionButtonNode)
self.wrappingScrollNode.addSubnode(self.contentTitleNode)
self.wrappingScrollNode.addSubnode(self.contentSeparatorNode)
self.wrappingScrollNode.addSubnode(self.cancelButtonNode)
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
}
self.contentTitleNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)
} else {
return nil
}
}
self.contentTitleNode.tapAttributeAction = { attributes in
if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String, mention.count > 1 {
openMention(String(mention[mention.index(after: mention.startIndex)...]))
}
}
}
override func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in
if let strongSelf = self {
if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem {
return strongSelf.context.account.postbox.transaction { transaction -> Bool in
return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId)
}
|> deliverOnMainQueue
|> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in
if let strongSelf = self {
var menuItems: [PeekControllerMenuItem] = []
if let stickerPack = strongSelf.stickerPack, case let .result(info, _, _) = stickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
if strongSelf.sendSticker != nil {
menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in
if let strongSelf = self {
return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false
} else {
return false
}
}))
}
menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in
if let strongSelf = self {
if isStarred {
let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
} else {
let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
}
}
return true
}))
menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }))
}
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems))
} else {
return nil
}
}
}
}
return nil
}, present: { [weak self] content, sourceNode in
if let strongSelf = self {
let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: {
return sourceNode
})
strongSelf.presentInGlobalOverlay?(controller, nil)
return controller
}
return nil
}, updateContent: { [weak self] content in
if let strongSelf = self {
var item: StickerPreviewPeekItem?
if let content = content as? StickerPreviewPeekContent {
item = content.item
}
strongSelf.updatePreviewingItem(item: item, animated: true)
}
}, activateBySingleTap: true))
self.updatePresentationData(self.presentationData)
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
let theme = presentationData.theme
let solidBackground = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
})?.stretchableImage(withLeftCapWidth: 1, topCapHeight: 1)
let roundedBackground = generateStretchableFilledCircleImage(radius: 10.0, color: presentationData.theme.actionSheet.opaqueItemBackgroundColor)
self.contentBackgroundNode.image = roundedBackground
if !self.installActionButtonIsRemove {
let roundedAccentBackground = generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
self.installActionButtonNode.setBackgroundImage(roundedAccentBackground, for: [])
}
self.shareActionButtonNode.setTitle(presentationData.strings.Conversation_ContextMenuShare.uppercased(), with: Font.semibold(17.0), with: presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.contentSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.actionsSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.actionsBackgroundNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.cancelButtonNode.setTitle(presentationData.strings.Common_Cancel.uppercased(), with: Font.semibold(17.0), with: presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
self.contentTitleNode.linkHighlightColor = presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5)
if let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var insets = layout.insets(options: [.statusBar])
insets.top = 10.0 + insets.top
let cleanInsets = layout.insets(options: [.statusBar])
let hasShareButton = self.shareActionButtonNode.supernode != nil
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
if insets.bottom > 0 {
bottomInset -= 0.0
}
let buttonHeight: CGFloat = 50.0
let actionAreaTopInset: CGFloat = 15.0
let buttonSpacing: CGFloat = 10.0
let sectionSpacing: CGFloat = 8.0
let titleAreaHeight: CGFloat = 54.0
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let sideInset: CGFloat = 0.0
//transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight)))
var actionAreaHeight = bottomInset + buttonHeight + actionAreaTopInset
transition.updateFrame(node: self.actionsBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: actionAreaHeight)))
transition.updateFrame(node: self.actionsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
let maximumContentHeight = layout.size.height - insets.top
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0)
var transaction: StickerPackPreviewGridTransaction?
var itemCount = 0
var animateIn = false
if let stickerPack = self.stickerPack {
switch stickerPack {
case .fetching, .none:
if self.activityIndicator == nil {
let activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(self.presentationData.theme.actionSheet.controlAccentColor, 22.0, 2.0, false))
self.activityIndicator = activityIndicator
self.addSubnode(activityIndicator)
}
case let .result(info, items, _):
if let activityIndicator = self.activityIndicator {
activityIndicator.removeFromSupernode()
self.activityIndicator = nil
}
itemCount = items.count
var updatedItems: [StickerPackPreviewGridEntry] = []
for item in items {
if let item = item as? StickerPackItem {
updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item))
}
}
if self.currentItems.isEmpty && !updatedItems.isEmpty {
let entities = generateTextEntities(info.title, enabledTypes: [.mention])
let font = Font.semibold(17.0)
self.contentTitleNode.attributedText = stringWithAppliedEntities(info.title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font)
animateIn = true
}
transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction)
self.currentItems = updatedItems
}
}
let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
let titleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - titleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + floor((titleAreaHeight - titleSize.height) / 2.0)), size: titleSize)
let deltaTitlePosition = CGPoint(x: titleFrame.midX - self.contentTitleNode.frame.midX, y: titleFrame.midY - self.contentTitleNode.frame.midY)
self.contentTitleNode.frame = titleFrame
transition.animatePosition(node: self.contentTitleNode, from: CGPoint(x: titleFrame.midX + deltaTitlePosition.x, y: titleFrame.midY + deltaTitlePosition.y))
transition.updateFrame(node: self.contentTitleNode, frame: titleFrame)
transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
let itemsPerRow = 4
let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow))
let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0)
let minimallyRevealedRowCount: CGFloat = 4.5
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight - actionAreaHeight)
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
if let activityIndicator = self.activityIndicator {
let indicatorSize = activityIndicator.calculateSizeThatFits(layout.size)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - indicatorSize.width) / 2.0), y: contentFrame.maxY - indicatorSize.height - 54.0), size: indicatorSize))
}
var actionsOffset: CGFloat = layout.size.height - bottomInset
transition.updateFrame(node: self.installActionButtonNode, frame: CGRect(origin: CGPoint(x: 16.0, y: actionsOffset - buttonHeight), size: CGSize(width: contentContainerFrame.size.width - 16.0 * 2.0, height: buttonHeight)))
actionsOffset -= buttonHeight
//transition.updateFrame(node: self.installActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - installButtonOffset - UIScreenPixel - bottomAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
//transition.updateFrame(node: self.shareActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight - bottomAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)))
//transition.updateFrame(node: self.shareActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight - UIScreenPixel - bottomAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight - actionAreaHeight))
self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transaction?.deletions ?? [], insertItems: transaction?.insertions ?? [], updateItems: transaction?.updates ?? [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize))
if animateIn {
self.contentGridNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.installActionButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.shareActionButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.actionsSeparatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.actionsBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if let _ = self.stickerPack, self.stickerPackUpdated {
self.dequeueUpdateStickerPack()
}
}
private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) {
if let (layout, _) = self.containerLayout {
var insets = layout.insets(options: [.statusBar])
insets.top = 10.0 + insets.top
let cleanInsets = layout.insets(options: [.statusBar])
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
if insets.bottom > 0 {
bottomInset -= 0.0
}
let buttonHeight: CGFloat = 50.0
let actionAreaTopInset: CGFloat = 15.0
let buttonSpacing: CGFloat = 10.0
let sectionSpacing: CGFloat = 8.0
let titleAreaHeight: CGFloat = 54.0
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let sideInset: CGFloat = 0.0
var actionAreaHeight = bottomInset + buttonHeight + actionAreaTopInset
let maximumContentHeight = layout.size.height - insets.top
let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - presentationLayout.contentOffset.y), size: CGSize(width: contentFrame.size.width, height: contentFrame.size.height + 500.0))
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
if backgroundFrame.maxY > contentFrame.maxY {
backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY
}
if backgroundFrame.size.height < buttonHeight + 32.0 {
backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height
backgroundFrame.size.height = buttonHeight + 32.0
}
var compactFrame = true
if let stickerPack = self.stickerPack, case .result = stickerPack {
compactFrame = false
}
if compactFrame {
backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.maxY - buttonHeight - 64.0), size: CGSize(width: contentFrame.size.width, height: buttonHeight + 64.0))
}
let backgroundDeltaY = backgroundFrame.minY - self.contentBackgroundNode.frame.minY
transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame)
transition.animatePositionAdditive(node: self.contentGridNode, offset: CGPoint(x: 0.0, y: -backgroundDeltaY))
let titleSize = self.contentTitleNode.bounds.size
let titleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.size.width - titleSize.width) / 2.0), y: backgroundFrame.minY + floor((titleAreaHeight - titleSize.height) / 2.0)), size: titleSize)
transition.updateFrame(node: self.contentTitleNode, frame: titleFrame)
transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: backgroundFrame.minY + titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: UIScreenPixel)))
var modalStyleOverlayTransitionFactor: CGFloat = 0.0
if !compactFrame && presentationLayout.contentOffset.y >= 0.0 {
self.contentSeparatorNode.alpha = 1.0
} else {
self.contentSeparatorNode.alpha = 0.0
}
let activationDistance: CGFloat = 100.0
let offsetFactor = (activationDistance - min(max(0.0, -presentationLayout.contentOffset.y), activationDistance)) / activationDistance
self.controller?.updateModalStyleOverlayTransitionFactor(offsetFactor, transition: transition)
//self.controller?.updateModalStyleOverlayTransitionFactor(offsetFactor.isZero ? 0.0 : 1.0, transition: .animated(duration: 0.4, curve: .spring))
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancelButtonPressed()
}
}
@objc func cancelButtonPressed() {
self.cancel?()
}
@objc func installActionButtonPressed() {
let dismissOnAction: Bool
if let initiallyInstalled = self.stickerPackInitiallyInstalled, initiallyInstalled {
dismissOnAction = true
} else {
dismissOnAction = true
}
if let stickerPack = self.stickerPack, let stickerSettings = self.stickerSettings {
switch stickerPack {
case let .result(info, items, installed):
if installed {
let _ = removeStickerPackInteractively(postbox: self.context.account.postbox, id: info.id, option: .delete).start()
self.updateStickerPack(.result(info: info, items: items, installed: false), stickerSettings: stickerSettings)
} else {
let _ = addStickerPackInteractively(postbox: self.context.account.postbox, info: info, items: items).start()
if !dismissOnAction {
self.updateStickerPack(.result(info: info, items: items, installed: true), stickerSettings: stickerSettings)
}
}
if dismissOnAction {
self.cancelButtonPressed()
}
default:
break
}
}
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
var dimCompleted = false
var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if let strongSelf = self, dimCompleted && offsetCompleted {
strongSelf.dismiss?()
}
completion?()
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
dimCompleted = true
internalCompletion()
})
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
}
func updateStickerPack(_ stickerPack: LoadedStickerPack, stickerSettings: StickerSettings) {
self.stickerPack = stickerPack
self.stickerSettings = stickerSettings
self.stickerPackUpdated = true
self.interaction.playAnimatedStickers = stickerSettings.loopAnimatedStickers
if let _ = self.containerLayout {
self.dequeueUpdateStickerPack()
}
switch stickerPack {
case .none, .fetching:
self.actionsSeparatorNode.alpha = 0.0
self.shareActionButtonNode.alpha = 0.0
self.actionsBackgroundNode.alpha = 0.0
self.installActionButtonNode.alpha = 0.0
self.installActionButtonNode.setTitle("", with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal)
self.installActionButtonIsRemove = true
self.installActionButtonNode.setBackgroundImage(nil, for: [])
case let .result(info, _, installed):
if self.stickerPackInitiallyInstalled == nil {
self.stickerPackInitiallyInstalled = installed
}
self.actionsSeparatorNode.alpha = 1.0
self.actionsBackgroundNode.alpha = 1.0
self.shareActionButtonNode.alpha = 1.0
self.installActionButtonNode.alpha = 1.0
if installed {
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count)
} else {
text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count)
}
self.installActionButtonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal)
self.installActionButtonIsRemove = true
self.installActionButtonNode.setBackgroundImage(nil, for: [])
} else {
let text: String
if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
text = self.presentationData.strings.StickerPack_AddStickerCount(info.count)
} else {
text = self.presentationData.strings.StickerPack_AddMaskCount(info.count)
}
self.installActionButtonNode.setTitle(text.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal)
self.installActionButtonIsRemove = false
let roundedAccentBackground = generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
self.installActionButtonNode.setBackgroundImage(roundedAccentBackground, for: [])
}
}
}
func dequeueUpdateStickerPack() {
if let (layout, navigationBarHeight) = self.containerLayout, let _ = self.stickerPack, self.stickerPackUpdated {
self.stickerPackUpdated = false
let transition: ContainedViewLayoutTransition
if self.didSetReady {
transition = .animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
if !self.didSetReady {
self.didSetReady = true
self.ready.set(.single(true))
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.installActionButtonNode.hitTest(self.installActionButtonNode.convert(point, from: self), with: event) {
return result
}
else if self.shareActionButtonNode.supernode != nil, let result = self.shareActionButtonNode.hitTest(self.shareActionButtonNode.convert(point, from: self), with: event) {
return result
}
if self.bounds.contains(point) {
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) {
return self.dimNode.view
}
}
let result = super.hitTest(point, with: event)
var currentParent: UIView? = result
var enableScrolling = true
while true {
if currentParent == nil {
break
}
if let scrollView = currentParent as? UIScrollView {
if scrollView === self.wrappingScrollNode.view {
break
}
if scrollView.disablesInteractiveModalDismiss {
enableScrolling = false
break
} else {
if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
return self.wrappingScrollNode.view
}
}
}
currentParent = currentParent?.superview
}
return result
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.cancelButtonPressed()
}
}
private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) {
if self.interaction.previewedItem != item {
self.interaction.previewedItem = item
self.contentGridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? StickerPackPreviewGridItemNode {
itemNode.updatePreviewing(animated: animated)
}
}
}
}
@objc private func sharePressed() {
self.openShare?()
}
}