mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
587 lines
24 KiB
Swift
587 lines
24 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import StickerResources
|
|
import AnimatedStickerNode
|
|
import TelegramAnimatedStickerNode
|
|
import ContextUI
|
|
import SolidRoundedButtonNode
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import AppBundle
|
|
import ReactionSelectionNode
|
|
import EntityKeyboard
|
|
|
|
public enum StickerPreviewPeekItem: Equatable {
|
|
public static func == (lhs: StickerPreviewPeekItem, rhs: StickerPreviewPeekItem) -> Bool {
|
|
switch lhs {
|
|
case let .pack(lhsPack):
|
|
if case let .pack(rhsPack) = rhs, lhsPack == rhsPack {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .found(lhsItem):
|
|
if case let .found(rhsItem) = rhs, lhsItem == rhsItem {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .portal(lhsPortal):
|
|
if case let .portal(rhsPortal) = rhs, lhsPortal === rhsPortal {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
case pack(TelegramMediaFile)
|
|
case found(FoundStickerItem)
|
|
case portal(PortalView)
|
|
|
|
public var file: TelegramMediaFile? {
|
|
switch self {
|
|
case let .pack(file):
|
|
return file
|
|
case let .found(item):
|
|
return item.file
|
|
case .portal:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class StickerPreviewPeekContent: PeekControllerContent {
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
public let item: StickerPreviewPeekItem
|
|
let isLocked: Bool
|
|
let isCreating: Bool
|
|
let selectedEmoji: [String]
|
|
let selectedEmojiUpdated: ([String]) -> Void
|
|
let recommendedEmoji: [String]
|
|
let menu: [ContextMenuItem]
|
|
let openPremiumIntro: () -> Void
|
|
|
|
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, item: StickerPreviewPeekItem, isLocked: Bool = false, isCreating: Bool = false, selectedEmoji: [String] = [], selectedEmojiUpdated: @escaping ([String]) -> Void = { _ in }, recommendedEmoji: [String] = [], menu: [ContextMenuItem], openPremiumIntro: @escaping () -> Void) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.item = item
|
|
self.isLocked = isLocked
|
|
self.isCreating = isCreating
|
|
self.selectedEmoji = selectedEmoji
|
|
self.selectedEmojiUpdated = selectedEmojiUpdated
|
|
self.recommendedEmoji = recommendedEmoji
|
|
if isLocked {
|
|
self.menu = []
|
|
} else {
|
|
self.menu = menu
|
|
}
|
|
self.openPremiumIntro = openPremiumIntro
|
|
}
|
|
|
|
public func presentation() -> PeekControllerContentPresentation {
|
|
return .freeform
|
|
}
|
|
|
|
public func menuActivation() -> PeerControllerMenuActivation {
|
|
return .press
|
|
}
|
|
|
|
public func menuItems() -> [ContextMenuItem] {
|
|
return self.menu
|
|
}
|
|
|
|
public func node() -> PeekControllerContentNode & ASDisplayNode {
|
|
return StickerPreviewPeekContentNode(context: self.context, item: self.item, theme: self.theme)
|
|
}
|
|
|
|
public func topAccessoryNode() -> ASDisplayNode? {
|
|
return nil
|
|
}
|
|
|
|
public func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? {
|
|
if self.isCreating {
|
|
return EmojiStickerAccessoryNode(context: self.context, theme: self.theme, recommendedEmoji: self.recommendedEmoji, selectedEmoji: self.selectedEmoji, selectedEmojiUpdated: self.selectedEmojiUpdated)
|
|
}
|
|
if self.isLocked {
|
|
return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings, isEmoji: self.item.file?.isCustomEmoji ?? false, proceed: self.openPremiumIntro)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public func isEqual(to: PeekControllerContent) -> Bool {
|
|
if let to = to as? StickerPreviewPeekContent {
|
|
return self.item == to.item
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode {
|
|
private let context: AccountContext
|
|
private let item: StickerPreviewPeekItem
|
|
|
|
private var textNode: ASTextNode
|
|
public var imageNode: TransformImageNode
|
|
public var animationNode: AnimatedStickerNode?
|
|
public var additionalAnimationNode: AnimatedStickerNode?
|
|
private let portalWrapperNode: ASDisplayNode
|
|
|
|
private let effectDisposable = MetaDisposable()
|
|
|
|
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
private let _ready = Promise<Bool>()
|
|
|
|
init(context: AccountContext, item: StickerPreviewPeekItem, theme: PresentationTheme) {
|
|
self.context = context
|
|
self.item = item
|
|
|
|
self.textNode = ASTextNode()
|
|
self.imageNode = TransformImageNode()
|
|
|
|
var isPremiumSticker = false
|
|
if let file = item.file {
|
|
for case let .Sticker(text, _, _) in file.attributes {
|
|
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black)
|
|
break
|
|
}
|
|
|
|
isPremiumSticker = file.isPremiumSticker
|
|
|
|
if file.isAnimatedSticker || file.isVideoSticker {
|
|
let animationNode = DefaultAnimatedStickerNodeImpl()
|
|
animationNode.overrideVisibility = true
|
|
self.animationNode = animationNode
|
|
|
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
|
let fitSize: CGSize
|
|
if file.isCustomEmoji {
|
|
fitSize = CGSize(width: 200.0, height: 200.0)
|
|
} else {
|
|
fitSize = CGSize(width: 400.0, height: 400.0)
|
|
}
|
|
let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize)
|
|
|
|
if file.isCustomTemplateEmoji {
|
|
animationNode.dynamicColor = theme.list.itemPrimaryTextColor
|
|
}
|
|
|
|
animationNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: isPremiumSticker ? .once : .loop, mode: .direct(cachePathPrefix: nil))
|
|
animationNode.visibility = true
|
|
animationNode.addSubnode(self.textNode)
|
|
|
|
if isPremiumSticker, let effect = file.videoThumbnails.first {
|
|
self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start())
|
|
|
|
let source = AnimatedStickerResourceSource(account: context.account, resource: effect.resource, fitzModifier: nil)
|
|
let additionalAnimationNode = DefaultAnimatedStickerNodeImpl()
|
|
additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 2.0), height: Int(fittedDimensions.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
|
additionalAnimationNode.visibility = true
|
|
self.additionalAnimationNode = additionalAnimationNode
|
|
}
|
|
} else {
|
|
self.imageNode.addSubnode(self.textNode)
|
|
self.animationNode = nil
|
|
}
|
|
|
|
self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: file, small: false, fetched: true))
|
|
} else if case .portal = item {
|
|
self._ready.set(.single(true))
|
|
}
|
|
|
|
self.portalWrapperNode = ASDisplayNode()
|
|
self.portalWrapperNode.clipsToBounds = true
|
|
self.portalWrapperNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.isUserInteractionEnabled = false
|
|
|
|
if let animationNode = self.animationNode {
|
|
self.addSubnode(animationNode)
|
|
|
|
if isPremiumSticker {
|
|
animationNode.completed = { [weak self] _ in
|
|
if let strongSelf = self, let animationNode = strongSelf.animationNode, let additionalAnimationNode = strongSelf.additionalAnimationNode {
|
|
Queue.mainQueue().async {
|
|
animationNode.play(firstFrame: false, fromIndex: nil)
|
|
additionalAnimationNode.play(firstFrame: false, fromIndex: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.addSubnode(self.imageNode)
|
|
}
|
|
|
|
if let additionalAnimationNode = self.additionalAnimationNode {
|
|
self.addSubnode(additionalAnimationNode)
|
|
}
|
|
|
|
self.addSubnode(self.portalWrapperNode)
|
|
|
|
if let animationNode = self.animationNode {
|
|
animationNode.started = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf._ready.set(.single(true))
|
|
}
|
|
} else {
|
|
self.imageNode.imageUpdated = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf._ready.set(.single(true))
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.effectDisposable.dispose()
|
|
}
|
|
|
|
public override func didLoad() {
|
|
super.didLoad()
|
|
|
|
if case let .portal(portalView) = self.item {
|
|
self.portalWrapperNode.view.addSubview(portalView.view)
|
|
}
|
|
}
|
|
|
|
|
|
public func ready() -> Signal<Bool, NoError> {
|
|
return self._ready.get()
|
|
}
|
|
|
|
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
|
let boundingSize: CGSize
|
|
if self.item.file?.isCustomEmoji == true {
|
|
boundingSize = CGSize(width: 120.0, height: 120.0)
|
|
} else if let _ = self.additionalAnimationNode {
|
|
boundingSize = CGSize(width: 240.0, height: 240.0).fitted(size)
|
|
} else {
|
|
boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size)
|
|
}
|
|
|
|
let dimensions: PixelDimensions
|
|
if let dimensionsValue = self.item.file?.dimensions {
|
|
dimensions = dimensionsValue
|
|
} else {
|
|
dimensions = PixelDimensions(width: 512, height: 512)
|
|
}
|
|
|
|
var topOffset: CGFloat = 0.0
|
|
var textSpacing: CGFloat = 50.0
|
|
|
|
if size.width == 292.0 {
|
|
topOffset = 60.0
|
|
textSpacing -= 10.0
|
|
} else if size.width == 347.0 && size.height == 577.0 {
|
|
topOffset = 60.0
|
|
textSpacing -= 10.0
|
|
}
|
|
|
|
let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0))
|
|
if textSize.height.isZero {
|
|
topOffset = 0.0
|
|
textSpacing = 0.0
|
|
}
|
|
|
|
let imageSize = dimensions.cgSize.aspectFitted(boundingSize)
|
|
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
|
var imageFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0), y: textSize.height + textSpacing - topOffset), size: imageSize)
|
|
var centerOffset: CGFloat = 0.0
|
|
if self.item.file?.isPremiumSticker == true {
|
|
let originalImageFrame = imageFrame
|
|
imageFrame.origin.x = min(imageFrame.minX + imageFrame.width * 0.1, size.width - imageFrame.width - 18.0)
|
|
centerOffset = imageFrame.minX - originalImageFrame.minX
|
|
}
|
|
self.imageNode.frame = imageFrame
|
|
|
|
if let animationNode = self.animationNode {
|
|
animationNode.frame = imageFrame
|
|
animationNode.updateLayout(size: imageSize)
|
|
|
|
if let additionalAnimationNode = self.additionalAnimationNode {
|
|
additionalAnimationNode.frame = imageFrame.offsetBy(dx: -imageFrame.width * 0.245 + 21.0, dy: -1.0).insetBy(dx: -imageFrame.width * 0.245, dy: -imageFrame.height * 0.245)
|
|
additionalAnimationNode.updateLayout(size: additionalAnimationNode.frame.size)
|
|
}
|
|
}
|
|
|
|
if case let .portal(portalView) = self.item {
|
|
self.portalWrapperNode.bounds = CGRect(origin: .zero, size: imageFrame.size)
|
|
self.portalWrapperNode.position = imageFrame.center
|
|
self.portalWrapperNode.cornerRadius = imageFrame.size.width / 8.0
|
|
|
|
portalView.view.center = CGPoint(x: imageFrame.size.width / 2.0, y: imageFrame.size.height / 2.0)
|
|
|
|
let scale = 180.0 / (size.width * 1.04)
|
|
portalView.view.transform = CGAffineTransformMakeScale(scale, scale)
|
|
}
|
|
|
|
self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0) - centerOffset, y: -textSize.height - textSpacing), size: textSize)
|
|
|
|
if self.item.file?.isCustomEmoji == true || textSize.height.isZero {
|
|
return CGSize(width: boundingSize.width, height: imageFrame.height)
|
|
} else {
|
|
return CGSize(width: boundingSize.width, height: imageFrame.height + textSize.height + textSpacing)
|
|
}
|
|
}
|
|
}
|
|
|
|
final class PremiumStickerPackAccessoryNode: SparseNode, PeekControllerAccessoryNode {
|
|
var dismiss: () -> Void = {}
|
|
let proceed: () -> Void
|
|
|
|
let textNode: ImmediateTextNode
|
|
let proceedButton: SolidRoundedButtonNode
|
|
let cancelButton: HighlightableButtonNode
|
|
|
|
init(theme: PresentationTheme, strings: PresentationStrings, isEmoji: Bool, proceed: @escaping () -> Void) {
|
|
self.proceed = proceed
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.displaysAsynchronously = false
|
|
self.textNode.textAlignment = .center
|
|
self.textNode.maximumNumberOfLines = 0
|
|
self.textNode.attributedText = NSAttributedString(string: isEmoji ? strings.Premium_Emoji_Description : strings.Premium_Stickers_Description, font: Font.regular(17.0), textColor: theme.actionSheet.secondaryTextColor)
|
|
self.textNode.lineSpacing = 0.1
|
|
|
|
self.proceedButton = SolidRoundedButtonNode(title: isEmoji ? strings.Premium_Emoji_Proceed: strings.Premium_Stickers_Proceed, theme: SolidRoundedButtonTheme(
|
|
backgroundColor: .white,
|
|
backgroundColors: [
|
|
UIColor(rgb: 0x0077ff),
|
|
UIColor(rgb: 0x6b93ff),
|
|
UIColor(rgb: 0x8878ff),
|
|
UIColor(rgb: 0xe46ace)
|
|
], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true)
|
|
self.proceedButton.iconPosition = .right
|
|
self.proceedButton.iconSpacing = 4.0
|
|
self.proceedButton.animation = "premium_unlock"
|
|
|
|
self.cancelButton = HighlightableButtonNode()
|
|
self.cancelButton.setTitle(strings.Common_Cancel, with: Font.regular(17.0), with: theme.list.itemAccentColor, for: .normal)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.textNode)
|
|
self.addSubnode(self.proceedButton)
|
|
self.addSubnode(self.cancelButton)
|
|
|
|
self.proceedButton.pressed = { [weak self] in
|
|
self?.dismiss()
|
|
self?.proceed()
|
|
}
|
|
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
@objc func cancelPressed() {
|
|
self.dismiss()
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
let sideInset: CGFloat = 16.0
|
|
|
|
var bottomOffset: CGFloat = 0.0
|
|
if size.width == 320.0 {
|
|
bottomOffset = 30.0
|
|
} else if size.width == 375.0 && size.height == 667.0 {
|
|
bottomOffset = 30.0
|
|
}
|
|
|
|
let cancelSize = self.cancelButton.measure(size)
|
|
self.cancelButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - cancelSize.width) / 2.0), y: size.height - cancelSize.height - 49.0 + bottomOffset), size: cancelSize)
|
|
|
|
let buttonWidth = size.width - sideInset * 2.0
|
|
let buttonHeight = self.proceedButton.updateLayout(width: buttonWidth, transition: transition)
|
|
self.proceedButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - buttonWidth) / 2.0), y: size.height - cancelSize.height - 49.0 - buttonHeight - 23.0 + bottomOffset), size: CGSize(width: buttonWidth, height: buttonHeight))
|
|
|
|
let textSideInset = size.width == 320.0 ? sideInset : sideInset * 2.0
|
|
|
|
let textSize = self.textNode.updateLayout(CGSize(width: size.width - textSideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
|
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: size.height - cancelSize.height - 48.0 - buttonHeight - 20.0 - textSize.height - 31.0 + bottomOffset), size: textSize)
|
|
}
|
|
}
|
|
|
|
private func topItems(selectedEmoji: [String] = [], recommendedEmoji: [String], count: Int) -> [String] {
|
|
var defaultItems: [String] = [
|
|
"👍", "👎", "❤", "🔥", "🥰", "👏", "😁", "😎"
|
|
]
|
|
if !recommendedEmoji.isEmpty, let firstEmoji = recommendedEmoji.first {
|
|
defaultItems.remove(at: defaultItems.count - 2)
|
|
defaultItems.insert(firstEmoji, at: defaultItems.count - 1)
|
|
}
|
|
var result = selectedEmoji.filter { !defaultItems.contains($0) }
|
|
result.append(contentsOf: defaultItems)
|
|
return Array(result.prefix(count))
|
|
}
|
|
|
|
final class EmojiStickerAccessoryNode: SparseNode, PeekControllerAccessoryNode {
|
|
let context: AccountContext
|
|
|
|
let reactionContextNode: ReactionContextNode
|
|
var selectedItemsDisposable: Disposable?
|
|
|
|
var dismiss: () -> Void = {}
|
|
|
|
private var scheduledCollapse = false
|
|
|
|
init(context: AccountContext, theme: PresentationTheme, recommendedEmoji: [String], selectedEmoji: [String], selectedEmojiUpdated: @escaping ([String]) -> Void) {
|
|
self.context = context
|
|
|
|
var layoutImpl: ((ContainedViewLayoutTransition) -> Void)?
|
|
|
|
let items = topItems(selectedEmoji: selectedEmoji, recommendedEmoji: recommendedEmoji, count: 7)
|
|
let selectedItems = ValuePromise<[String]>(selectedEmoji)
|
|
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
|
let reactionContextNode = ReactionContextNode(
|
|
context: self.context,
|
|
animationCache: self.context.animationCache,
|
|
presentationData: presentationData,
|
|
items: items.map { .staticEmoji($0) },
|
|
selectedItems: Set(selectedEmoji),
|
|
title: presentationData.strings.MediaEditor_SetStickerEmoji,
|
|
reactionsLocked: false,
|
|
alwaysAllowPremiumReactions: true,
|
|
allPresetReactionsAreAvailable: true,
|
|
getEmojiContent: { animationCache, animationRenderer in
|
|
return selectedItems.get()
|
|
|> mapToSignal { selectedItems in
|
|
return EmojiPagerContentComponent.emojiInputData(
|
|
context: context,
|
|
animationCache: animationCache,
|
|
animationRenderer: animationRenderer,
|
|
isStandalone: false,
|
|
subject: .stickerAlt,
|
|
hasTrending: false,
|
|
topReactionItems: [],
|
|
topEmojiItems: topItems(selectedEmoji: selectedItems, recommendedEmoji: recommendedEmoji, count: 8),
|
|
areUnicodeEmojiEnabled: true,
|
|
areCustomEmojiEnabled: false,
|
|
chatPeerId: context.account.peerId,
|
|
selectedItems: Set(selectedItems),
|
|
hasRecent: false,
|
|
premiumIfSavedMessages: false
|
|
)
|
|
}
|
|
},
|
|
isExpandedUpdated: { transition in
|
|
layoutImpl?(transition)
|
|
},
|
|
requestLayout: { transition in
|
|
layoutImpl?(transition)
|
|
},
|
|
requestUpdateOverlayWantsToBeBelowKeyboard: { transition in
|
|
layoutImpl?(transition)
|
|
}
|
|
)
|
|
reactionContextNode.hideBackground = true
|
|
reactionContextNode.displayTail = true
|
|
reactionContextNode.forceTailToRight = true
|
|
reactionContextNode.forceDark = true
|
|
reactionContextNode.isEmojiOnly = true
|
|
self.reactionContextNode = reactionContextNode
|
|
|
|
super.init()
|
|
|
|
layoutImpl = { [weak self] transition in
|
|
self?.requestLayout(forceUpdate: true, transition: transition)
|
|
}
|
|
|
|
reactionContextNode.emojiSelected = { [weak self] emoji in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let _ = (selectedItems.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] items in
|
|
guard let self else {
|
|
return
|
|
}
|
|
var items = items
|
|
if let index = items.firstIndex(where: { $0 == emoji }) {
|
|
items.remove(at: index)
|
|
} else {
|
|
items.append(emoji)
|
|
}
|
|
selectedItems.set(items)
|
|
selectedEmojiUpdated(items)
|
|
|
|
self.reactionContextNode.collapse()
|
|
})
|
|
}
|
|
|
|
self.addSubnode(reactionContextNode)
|
|
|
|
self.selectedItemsDisposable = (selectedItems.get()
|
|
|> deliverOnMainQueue).start(next: { [weak self] items in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.reactionContextNode.selectedItems = Set(items)
|
|
self.reactionContextNode.items = topItems(selectedEmoji: items, recommendedEmoji: recommendedEmoji, count: 7).map { .staticEmoji($0) }
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.selectedItemsDisposable?.dispose()
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if let result, result.isDescendant(of: self.reactionContextNode.view) {
|
|
return result
|
|
} else if self.reactionContextNode.isExpanded {
|
|
if !self.scheduledCollapse {
|
|
self.scheduledCollapse = true
|
|
Queue.mainQueue().after(0.01, {
|
|
self.scheduledCollapse = false
|
|
self.reactionContextNode.collapse()
|
|
})
|
|
}
|
|
return self.reactionContextNode.view
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func requestLayout(forceUpdate: Bool = false, transition: ContainedViewLayoutTransition) {
|
|
guard let size = self.currentLayout else {
|
|
return
|
|
}
|
|
self.updateLayout(size: size, forceUpdate: forceUpdate, transition: transition)
|
|
}
|
|
|
|
private var currentLayout: CGSize?
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.updateLayout(size: size, forceUpdate: false, transition: transition)
|
|
}
|
|
func updateLayout(size: CGSize, forceUpdate: Bool, transition: ContainedViewLayoutTransition) {
|
|
let isFirstTime = self.currentLayout == nil
|
|
self.currentLayout = size
|
|
|
|
let anchorRect = CGRect(x: size.width / 2.0, y: size.height / 3.0 - 50.0, width: 0.0, height: 0.0)
|
|
|
|
transition.updateFrame(view: self.reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: size))
|
|
self.reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, forceUpdate: forceUpdate, transition: transition)
|
|
|
|
if isFirstTime {
|
|
Queue.mainQueue().justDispatch {
|
|
self.reactionContextNode.animateIn(from: anchorRect)
|
|
}
|
|
}
|
|
}
|
|
}
|