Swiftgram/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift
2023-09-21 00:12:53 +02:00

610 lines
30 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import TelegramCore
import Display
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import AccountContext
import Emoji
import ChatPresentationInterfaceState
import AnimationCache
import MultiAnimationRenderer
import TextFormat
import ChatControllerInteraction
import ContextUI
import SwiftSignalKit
import PremiumUI
import StickerPeekUI
import UndoUI
import Pasteboard
import ChatContextQuery
private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable {
case symbol(String)
case media(MediaId)
}
private func backgroundCenterImage(_ theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 55.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor)
context.setFillColor(theme.list.plainBackgroundColor.cgColor)
let lineWidth = UIScreenPixel
context.setLineWidth(lineWidth)
context.translateBy(x: 460.5, y: 364.0 - 27.0)
let path: StaticString = "M-490.476836,-365 L-394.167708,-365 L-394.167708,-291.918214 C-394.167708,-291.918214 -383.538396,-291.918214 -397.691655,-291.918214 C-402.778486,-291.918214 -424.555168,-291.918214 -434.037301,-291.918214 C-440.297129,-291.918214 -440.780682,-283.5 -445.999879,-283.5 C-450.393041,-283.5 -452.491241,-291.918214 -456.502636,-291.918214 C-465.083339,-291.918214 -476.209155,-291.918214 -483.779021,-291.918214 C-503.033963,-291.918214 -490.476836,-291.918214 -490.476836,-291.918214 L-490.476836,-365 "
let _ = try? drawSvgPath(context, path: path)
context.fillPath()
context.translateBy(x: 0.0, y: lineWidth / 2.0)
let _ = try? drawSvgPath(context, path: path)
context.strokePath()
context.translateBy(x: -460.5, y: -lineWidth / 2.0 - 364.0 + 27.0)
context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0))
context.strokePath()
})
}
private func backgroundLeftImage(_ theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 8.0, height: 16.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor)
context.setFillColor(theme.list.plainBackgroundColor.cgColor)
let lineWidth = UIScreenPixel
context.setLineWidth(lineWidth)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height)))
context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.height - lineWidth, height: size.height - lineWidth)))
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
}
private struct EmojisChatInputContextPanelEntry: Comparable, Identifiable {
let index: Int
let theme: PresentationTheme
let symbol: String
let text: String
let file: TelegramMediaFile?
var stableId: EmojisChatInputContextPanelEntryStableId {
if let file = self.file {
return .media(file.fileId)
} else {
return .symbol(self.symbol)
}
}
func withUpdatedTheme(_ theme: PresentationTheme) -> EmojisChatInputContextPanelEntry {
return EmojisChatInputContextPanelEntry(index: self.index, theme: theme, symbol: self.symbol, text: self.text, file: self.file)
}
static func ==(lhs: EmojisChatInputContextPanelEntry, rhs: EmojisChatInputContextPanelEntry) -> Bool {
return lhs.index == rhs.index && lhs.symbol == rhs.symbol && lhs.text == rhs.text && lhs.theme === rhs.theme && lhs.file?.fileId == rhs.file?.fileId
}
static func <(lhs: EmojisChatInputContextPanelEntry, rhs: EmojisChatInputContextPanelEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, emojiSelected: @escaping (String, TelegramMediaFile?) -> Void) -> ListViewItem {
return EmojisChatInputPanelItem(context: context, theme: self.theme, symbol: self.symbol, text: self.text, file: self.file, animationCache: animationCache, animationRenderer: animationRenderer, emojiSelected: emojiSelected)
}
}
private struct EmojisChatInputContextPanelTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private func preparedTransition(from fromEntries: [EmojisChatInputContextPanelEntry], to toEntries: [EmojisChatInputContextPanelEntry], context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, emojiSelected: @escaping (String, TelegramMediaFile?) -> Void) -> EmojisChatInputContextPanelTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, animationCache: animationCache, animationRenderer: animationRenderer, emojiSelected: emojiSelected), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, animationCache: animationCache, animationRenderer: animationRenderer, emojiSelected: emojiSelected), directionHint: nil) }
return EmojisChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
}
final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
private let backgroundLeftNode: ASImageNode
private let backgroundNode: ASImageNode
private let backgroundRightNode: ASImageNode
private let clippingNode: ASDisplayNode
private let listView: ListView
private var currentEntries: [EmojisChatInputContextPanelEntry]?
private var enqueuedTransitions: [(EmojisChatInputContextPanelTransition, Bool)] = []
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)?
private var presentationInterfaceState: ChatPresentationInterfaceState?
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private weak var peekController: PeekController?
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
self.animationCache = chatPresentationContext.animationCache
self.animationRenderer = chatPresentationContext.animationRenderer
self.backgroundNode = ASImageNode()
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.image = backgroundCenterImage(theme)
self.backgroundLeftNode = ASImageNode()
self.backgroundLeftNode.displayWithoutProcessing = true
self.backgroundLeftNode.displaysAsynchronously = false
self.backgroundLeftNode.image = backgroundLeftImage(theme)
self.backgroundRightNode = ASImageNode()
self.backgroundRightNode.displayWithoutProcessing = true
self.backgroundRightNode.displaysAsynchronously = false
self.backgroundRightNode.image = backgroundLeftImage(theme)
self.backgroundRightNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.listView = ListView()
self.listView.isOpaque = false
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
self.listView.accessibilityPageScrolledString = { row, count in
return strings.VoiceOver_ScrollStatus(row, count).string
}
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext)
self.placement = .overTextInput
self.isOpaque = false
self.addSubnode(self.backgroundNode)
self.addSubnode(self.backgroundLeftNode)
self.addSubnode(self.backgroundRightNode)
self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.listView)
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
guard let self else {
return nil
}
return self.peekContentAtPoint(point: point)
}, present: { [weak self] content, sourceView, sourceRect in
guard let strongSelf = self else {
return nil
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
return (sourceView, sourceRect)
})
/*controller.visibilityUpdated = { [weak self] visible in
self?.previewingStickersPromise.set(visible)
self?.requestDisableStickerAnimations?(visible)
self?.simulateUpdateLayout(isVisible: !visible)
}*/
strongSelf.peekController = controller
strongSelf.interfaceInteraction?.presentController(controller, nil)
return controller
}, updateContent: { [weak self] content in
guard let strongSelf = self else {
return
}
let _ = strongSelf
})
self.view.addGestureRecognizer(peekRecognizer)
}
private func peekContentAtPoint(point: CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? {
guard let presentationInterfaceState = self.presentationInterfaceState else {
return nil
}
guard let chatPeerId = presentationInterfaceState.renderedPeer?.peer?.id else {
return nil
}
var maybeFile: TelegramMediaFile?
var maybeItemLayer: CALayer?
self.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? EmojisChatInputPanelItemNode, let item = itemNode.item {
let localPoint = self.view.convert(point, to: itemNode.view)
if itemNode.view.bounds.contains(localPoint) {
maybeFile = item.file
maybeItemLayer = itemNode.layer
}
}
}
guard let file = maybeFile else {
return nil
}
guard let itemLayer = maybeItemLayer else {
return nil
}
let _ = chatPeerId
let _ = file
let _ = itemLayer
var collectionId: ItemCollectionId?
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
collectionId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
default:
break
}
}
}
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
if let collectionId {
bubbleUpEmojiOrStickersets.append(collectionId)
}
let context = self.context
let accountPeerId = context.account.peerId
let _ = bubbleUpEmojiOrStickersets
let _ = context
let _ = accountPeerId
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId))
|> map { peer -> Bool in
var hasPremium = false
if case let .user(user) = peer, user.isPremium {
hasPremium = true
}
return hasPremium
}
|> deliverOnMainQueue
|> map { [weak self, weak itemLayer] hasPremium -> (UIView, CGRect, PeekControllerContent)? in
guard let strongSelf = self, let itemLayer = itemLayer else {
return nil
}
let _ = strongSelf
let _ = itemLayer
var menuItems: [ContextMenuItem] = []
menuItems.removeAll()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = presentationData
var isLocked = false
if !hasPremium {
isLocked = file.isPremiumEmoji
if isLocked && chatPeerId == context.account.peerId {
isLocked = false
}
}
if let interaction = strongSelf.interfaceInteraction {
let _ = interaction
let sendEmoji: (TelegramMediaFile) -> Void = { file in
guard let self else {
return
}
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
return
}
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, stickerPackReference):
text = displayText
var packId: ItemCollectionId?
if case let .id(id, _) = stickerPackReference {
packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id)
}
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: packId, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let emojiAttribute {
controller.controllerInteraction?.sendEmoji(text, emojiAttribute, true)
}
}
let setStatus: (TelegramMediaFile) -> Void = { file in
guard let self else {
return
}
guard let controller = (self.interfaceInteraction?.chatController() as? ChatControllerImpl) else {
return
}
let _ = self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).startStandalone()
var animateInAsReplacement = false
animateInAsReplacement = false
/*if let currentUndoOverlayController = strongSelf.currentUndoOverlayController {
currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation()
strongSelf.currentUndoOverlayController = nil
animateInAsReplacement = true
}*/
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false })
//strongSelf.currentUndoOverlayController = controller
controller.controllerInteraction?.presentController(undoController, nil)
}
let copyEmoji: (TelegramMediaFile) -> Void = { file in
var text = "."
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
if let _ = emojiAttribute {
storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))])
}
}
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SendEmoji, icon: { theme in
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) {
return generateImage(image.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size))
}
})
} else {
return nil
}
}, action: { _, f in
sendEmoji(file)
f(.default)
})))
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SetAsStatus, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.default)
guard let strongSelf = self else {
return
}
if hasPremium {
setStatus(file)
} else {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: {
let controller = PremiumIntroScreen(context: context, source: .animatedEmoji)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(controller)
}
})))
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_CopyEmoji, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
copyEmoji(file)
f(.default)
})))
}
if menuItems.isEmpty {
return nil
}
let content = StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { [weak self] in
guard let self else {
return
}
guard let interfaceInteraction = self.interfaceInteraction else {
return
}
let _ = self
let _ = interfaceInteraction
let controller = PremiumIntroScreen(context: context, source: .stickers)
//let _ = controller
interfaceInteraction.getNavigationController()?.pushViewController(controller)
})
let _ = content
//return nil
return (strongSelf.view, itemLayer.convert(itemLayer.bounds, to: strongSelf.view.layer), content)
}
}
func updateResults(_ results: [(String, TelegramMediaFile?, String)]) {
var entries: [EmojisChatInputContextPanelEntry] = []
var index = 0
var stableIds = Set<EmojisChatInputContextPanelEntryStableId>()
for (symbol, file, text) in results {
let entry = EmojisChatInputContextPanelEntry(index: index, theme: self.theme, symbol: symbol.normalizedEmoji, text: text, file: file)
if stableIds.contains(entry.stableId) {
continue
}
stableIds.insert(entry.stableId)
entries.append(entry)
index += 1
}
self.prepareTransition(from: self.currentEntries, to: entries)
}
private func prepareTransition(from: [EmojisChatInputContextPanelEntry]? , to: [EmojisChatInputContextPanelEntry]) {
let firstTime = self.currentEntries == nil
let transition = preparedTransition(from: from ?? [], to: to, context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, emojiSelected: { [weak self] text, file in
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else {
return
}
var text = text
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
var hashtagQueryRange: NSRange?
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) {
if type == [.emojiSearch] {
var range = range
range.location -= 1
range.length += 1
hashtagQueryRange = range
break inner
}
}
if let range = hashtagQueryRange {
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
if let file = file {
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, displayText, _):
text = displayText
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
break loop
default:
break
}
}
}
var replacementText = NSAttributedString(string: text)
if let emojiAttribute = emojiAttribute {
replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
}
inputText.replaceCharacters(in: range, with: replacementText)
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
}
return (textInputState, inputMode)
}
})
self.currentEntries = to
self.enqueueTransition(transition, firstTime: firstTime)
if let presentationInterfaceState = presentationInterfaceState, let (size, leftInset, rightInset, bottomInset) = self.validLayout {
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .immediate, interfaceState: presentationInterfaceState)
}
}
private func enqueueTransition(_ transition: EmojisChatInputContextPanelTransition, firstTime: Bool) {
enqueuedTransitions.append((transition, firstTime))
if self.validLayout != nil {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let validLayout = self.validLayout, let (transition, _) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), duration: 0.0, curve: .Default(duration: nil))
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil)
}
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
let hadValidLayout = self.validLayout != nil
self.validLayout = (size, leftInset, rightInset, bottomInset)
self.presentationInterfaceState = interfaceState
let sideInsets: CGFloat = 10.0 + leftInset
let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries?.count ?? 0) * 45.0))
var contentLeftInset: CGFloat = 40.0
var leftOffset: CGFloat = 0.0
if sideInsets + floor(contentWidth / 2.0) < sideInsets + contentLeftInset + 15.0 {
let updatedLeftInset = sideInsets + floor(contentWidth / 2.0) - 15.0 - sideInsets
leftOffset = contentLeftInset - updatedLeftInset
contentLeftInset = updatedLeftInset
}
let backgroundFrame = CGRect(origin: CGPoint(x: sideInsets + leftOffset, y: size.height - 55.0 + 4.0), size: CGSize(width: contentWidth, height: 55.0))
let backgroundLeftFrame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: contentLeftInset, height: backgroundFrame.size.height - 10.0 + UIScreenPixel))
let backgroundCenterFrame = CGRect(origin: CGPoint(x: backgroundLeftFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: 30.0, height: 55.0))
let backgroundRightFrame = CGRect(origin: CGPoint(x: backgroundCenterFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: max(0.0, backgroundFrame.minX + backgroundFrame.size.width - backgroundCenterFrame.maxX), height: backgroundFrame.size.height - 10.0 + UIScreenPixel))
transition.updateFrame(node: self.backgroundLeftNode, frame: backgroundLeftFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundCenterFrame)
transition.updateFrame(node: self.backgroundRightNode, frame: backgroundRightFrame)
let gridFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 2.0), size: CGSize(width: backgroundFrame.size.width, height: 45.0))
transition.updateFrame(node: self.clippingNode, frame: gridFrame)
self.listView.frame = CGRect(origin: CGPoint(), size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width))
let gridBounds = self.listView.bounds
self.listView.bounds = CGRect(x: gridBounds.minX, y: gridBounds.minY, width: gridFrame.size.height, height: gridFrame.size.width)
self.listView.position = CGPoint(x: gridFrame.size.width / 2.0, y: gridFrame.size.height / 2.0)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width), insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), duration: 0.0, curve: .Default(duration: 0.0))
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if self.theme !== interfaceState.theme {
self.theme = interfaceState.theme
let updatedEntries = self.currentEntries?.map({$0.withUpdatedTheme(interfaceState.theme)}) ?? []
self.prepareTransition(from: self.currentEntries, to: updatedEntries)
}
}
override func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
completion()
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.clippingNode.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
}