mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
351 lines
15 KiB
Swift
351 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?.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(item: $0, icon: .none) },
|
|
selectedItems: Set(),
|
|
title: nil,
|
|
reactionsLocked: false,
|
|
alwaysAllowPremiumReactions: false,
|
|
allPresetReactionsAreAvailable: false,
|
|
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(onlyTop: false),
|
|
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(.standalone(media: 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))
|
|
}
|
|
}
|