Swiftgram/submodules/DrawingUI/Sources/DrawingReactionView.swift
2023-11-22 03:24:33 +04:00

348 lines
15 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import AVFoundation
import Display
import SwiftSignalKit
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import AccountContext
import MediaEditor
import TelegramPresentationData
import ReactionSelectionNode
import UndoUI
import EntityKeyboard
import ComponentFlow
public class DrawingReactionEntityView: DrawingStickerEntityView {
private var backgroundView: UIImageView
private var outlineView: UIImageView
override init(context: AccountContext, entity: DrawingStickerEntity) {
let backgroundView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionShadow"))
backgroundView.layer.zPosition = -1000.0
let outlineView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionOutline"))
outlineView.tintColor = .white
backgroundView.addSubview(outlineView)
self.backgroundView = backgroundView
self.outlineView = outlineView
super.init(context: context, entity: entity)
self.insertSubview(backgroundView, at: 0)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var isReaction: Bool {
return true
}
override func animateInsertion() {
super.animateInsertion()
Queue.mainQueue().after(0.2) {
let _ = self.selectedTapAction()
}
}
override func onSelection() {
self.presentReactionSelection()
}
override func onDeselection() {
let _ = self.dismissReactionSelection()
}
public override func update(animated: Bool) {
super.update(animated: animated)
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
switch style {
case .white:
self.outlineView.tintColor = .white
case .black:
self.outlineView.tintColor = UIColor(rgb: 0x000000, alpha: 0.5)
}
}
}
override func updateMirroring(animated: Bool) {
let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
if animated {
let isCurrentlyMirrored = ((self.backgroundView.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
var animationSourceTransform = CATransform3DIdentity
var animationTargetTransform = CATransform3DIdentity
if isCurrentlyMirrored {
animationSourceTransform = CATransform3DRotate(animationSourceTransform, .pi, 0.0, 1.0, 0.0)
animationSourceTransform.m34 = -1.0 / self.imageNode.frame.width
}
if self.stickerEntity.mirrored {
animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0)
animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width
}
self.backgroundView.layer.transform = animationSourceTransform
let values = [1.0, 0.01, 1.0]
let keyTimes = [0.0, 0.5, 1.0]
self.animationNode?.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.25, keyPath: "transform.scale.x", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
UIView.animate(withDuration: 0.25, animations: {
self.backgroundView.layer.transform = animationTargetTransform
}, completion: { finished in
self.backgroundView.layer.transform = staticTransform
})
} else {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.backgroundView.layer.transform = staticTransform
CATransaction.commit()
}
}
private weak var reactionContextNode: ReactionContextNode?
fileprivate func presentReactionSelection() {
guard let containerView = self.containerView, let superview = containerView.superview?.superview?.superview?.superview, self.reactionContextNode == nil else {
return
}
let availableSize = superview.frame.size
let reactionItems = containerView.getAvailableReactions()
let insets = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 64.0, right: 0.0)
let layout: (ContainedViewLayoutTransition) -> Void = { [weak self, weak superview] transition in
guard let self, let superview, let reactionContextNode = self.reactionContextNode else {
return
}
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition)
}
let reactionContextNodeTransition: Transition = .immediate
let reactionContextNode: ReactionContextNode
reactionContextNode = ReactionContextNode(
context: self.context,
animationCache: self.context.animationCache,
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: Set(),
title: nil,
getEmojiContent: { [weak self] animationCache, animationRenderer in
guard let self else {
preconditionFailure()
}
let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
}
return EmojiPagerContentComponent.emojiInputData(
context: self.context,
animationCache: animationCache,
animationRenderer: animationRenderer,
isStandalone: false,
subject: .reaction,
hasTrending: false,
topReactionItems: mappedReactionItems,
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId,
selectedItems: Set(),
premiumIfSavedMessages: false
)
},
isExpandedUpdated: { transition in
layout(transition)
},
requestLayout: { transition in
layout(transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { transition in
layout(transition)
}
)
reactionContextNode.displayTail = true
reactionContextNode.forceTailToRight = true
reactionContextNode.forceDark = true
self.reactionContextNode = reactionContextNode
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
guard let self else {
return
}
let continueWithAnimationFile: (TelegramMediaFile) -> Void = { [weak self] animation in
guard let self else {
return
}
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
self.stickerEntity.content = .file(animation, .reaction(updateReaction.reaction, style))
}
var nodeToTransitionOut: ASDisplayNode?
if let animationNode = self.animationNode {
nodeToTransitionOut = animationNode
} else if !self.imageNode.isHidden {
nodeToTransitionOut = self.imageNode
}
if let nodeToTransitionOut, let snapshot = nodeToTransitionOut.view.snapshotView(afterScreenUpdates: false) {
snapshot.frame = nodeToTransitionOut.frame
snapshot.layer.transform = nodeToTransitionOut.transform
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshot.removeFromSuperview()
})
snapshot.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
self.addSubview(snapshot)
}
self.animationNode?.removeFromSupernode()
self.animationNode = nil
self.didSetUpAnimationNode = false
self.isPlaying = false
self.currentSize = nil
self.setup()
self.applyVisibility()
self.setNeedsLayout()
let nodeToTransitionIn: ASDisplayNode?
if let animationNode = self.animationNode {
nodeToTransitionIn = animationNode
} else {
nodeToTransitionIn = self.imageNode
}
if let nodeToTransitionIn {
nodeToTransitionIn.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
nodeToTransitionIn.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
let _ = self.dismissReactionSelection()
}
switch updateReaction {
case .builtin:
let _ = (self.context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { availableReactions in
guard let availableReactions else {
return
}
var animation: TelegramMediaFile?
for reaction in availableReactions.reactions {
if reaction.value == updateReaction.reaction {
animation = reaction.selectAnimation
break
}
}
if let animation {
continueWithAnimationFile(animation)
}
})
case let .custom(fileId, file):
if let file {
continueWithAnimationFile(file)
} else {
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> deliverOnMainQueue).start(next: { files in
if let itemFile = files[fileId] {
continueWithAnimationFile(itemFile)
}
})
}
}
}
reactionContextNode.premiumReactionsSelected = { [weak self] file in
guard let self else {
return
}
if let file {
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Story_Editor_TooltipPremiumReaction, undoText: nil, customAction: nil), elevatedLayout: true, animateInAsReplacement: false, blurred: true, action: { [weak self] action in
if case .info = action, let self {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
self.containerView?.push(controller)
}
return false
})
self.containerView?.present(controller)
} else {
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
self.containerView?.push(controller)
}
}
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
superview.addSubnode(reactionContextNode)
reactionContextNode.animateIn(from: anchorRect)
}
fileprivate func dismissReactionSelection() -> Bool {
if let reactionContextNode = self.reactionContextNode {
reactionContextNode.animateOut(to: nil, animatingOutToReaction: false)
self.reactionContextNode = nil
Queue.mainQueue().after(0.35) {
reactionContextNode.view.removeFromSuperview()
}
return false
} else {
return true
}
}
override func selectedTapAction() -> Bool {
if case let .file(file, type) = self.stickerEntity.content, case let .reaction(reaction, style) = type {
guard self.reactionContextNode == nil else {
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
let keyTimes = [0.0, 0.33, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
let updatedStyle: DrawingStickerEntity.Content.FileType.ReactionStyle
switch style {
case .white:
updatedStyle = .black
case .black:
updatedStyle = .white
}
self.stickerEntity.content = .file(file, .reaction(reaction, updatedStyle))
self.update(animated: false)
return true
}
self.presentReactionSelection()
return true
} else {
return super.selectedTapAction()
}
}
override func innerLayoutSubview(boundingSize: CGSize) -> CGSize {
self.backgroundView.frame = CGRect(origin: .zero, size: boundingSize).insetBy(dx: -5.0, dy: -5.0)
self.outlineView.frame = backgroundView.bounds
return CGSize(width: floor(boundingSize.width * 0.63), height: floor(boundingSize.width * 0.63))
}
}