mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Various improvements
This commit is contained in:
347
submodules/DrawingUI/Sources/DrawingReactionView.swift
Normal file
347
submodules/DrawingUI/Sources/DrawingReactionView.swift
Normal file
@@ -0,0 +1,347 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user