mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
Reaction animation updates
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import RadialStatusNode
|
||||
import AnimatedCountLabelNode
|
||||
import AnimatedAvatarSetNode
|
||||
import ReactionButtonListComponent
|
||||
import AccountContext
|
||||
|
||||
final class MessageReactionButtonsNode: ASDisplayNode {
|
||||
enum DisplayType {
|
||||
case incoming
|
||||
case outgoing
|
||||
case freeform
|
||||
}
|
||||
|
||||
private let container: ReactionButtonsLayoutContainer
|
||||
var reactionSelected: ((String) -> Void)?
|
||||
|
||||
override init() {
|
||||
self.container = ReactionButtonsLayoutContainer()
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func prepareUpdate(
|
||||
context: AccountContext,
|
||||
presentationData: ChatPresentationData,
|
||||
availableReactions: AvailableReactions?,
|
||||
reactions: ReactionsMessageAttribute,
|
||||
constrainedWidth: CGFloat,
|
||||
type: DisplayType
|
||||
) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
|
||||
let reactionColors: ReactionButtonComponent.Colors
|
||||
switch type {
|
||||
case .incoming, .freeform:
|
||||
reactionColors = ReactionButtonComponent.Colors(
|
||||
background: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
foreground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
||||
stroke: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb
|
||||
)
|
||||
case .outgoing:
|
||||
reactionColors = ReactionButtonComponent.Colors(
|
||||
background: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
foreground: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
|
||||
stroke: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb
|
||||
)
|
||||
}
|
||||
|
||||
let reactionButtons = self.container.update(
|
||||
context: context,
|
||||
action: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.reactionSelected?(value)
|
||||
},
|
||||
reactions: reactions.reactions.map { reaction in
|
||||
var iconFile: TelegramMediaFile?
|
||||
|
||||
if let availableReactions = availableReactions {
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if availableReaction.value == reaction.value {
|
||||
iconFile = availableReaction.staticIcon
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ReactionButtonsLayoutContainer.Reaction(
|
||||
reaction: ReactionButtonComponent.Reaction(
|
||||
value: reaction.value,
|
||||
iconFile: iconFile
|
||||
),
|
||||
count: Int(reaction.count),
|
||||
isSelected: reaction.isSelected
|
||||
)
|
||||
},
|
||||
colors: reactionColors,
|
||||
constrainedWidth: constrainedWidth,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
var reactionButtonsSize = CGSize()
|
||||
var currentRowWidth: CGFloat = 0.0
|
||||
for item in reactionButtons.items {
|
||||
if currentRowWidth + item.size.width > constrainedWidth {
|
||||
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
|
||||
if !reactionButtonsSize.height.isZero {
|
||||
reactionButtonsSize.height += 6.0
|
||||
}
|
||||
reactionButtonsSize.height += item.size.height
|
||||
currentRowWidth = 0.0
|
||||
}
|
||||
|
||||
if !currentRowWidth.isZero {
|
||||
currentRowWidth += 6.0
|
||||
}
|
||||
currentRowWidth += item.size.width
|
||||
}
|
||||
if !currentRowWidth.isZero && !reactionButtons.items.isEmpty {
|
||||
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
|
||||
if !reactionButtonsSize.height.isZero {
|
||||
reactionButtonsSize.height += 6.0
|
||||
}
|
||||
reactionButtonsSize.height += reactionButtons.items[0].size.height
|
||||
}
|
||||
|
||||
let topInset: CGFloat = 0.0
|
||||
let bottomInset: CGFloat = 2.0
|
||||
|
||||
return (proposedWidth: reactionButtonsSize.width, continueLayout: { [weak self] boundingWidth in
|
||||
return (size: CGSize(width: boundingWidth, height: topInset + reactionButtonsSize.height + bottomInset), apply: { animation in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var reactionButtonPosition = CGPoint(x: 0.0, y: topInset)
|
||||
for item in reactionButtons.items {
|
||||
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
||||
reactionButtonPosition.x = 0.0
|
||||
reactionButtonPosition.y += item.size.height + 6.0
|
||||
}
|
||||
|
||||
if item.view.superview == nil {
|
||||
strongSelf.view.addSubview(item.view)
|
||||
item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size)
|
||||
if animation.isAnimated {
|
||||
item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil)
|
||||
}
|
||||
reactionButtonPosition.x += item.size.width + 6.0
|
||||
}
|
||||
|
||||
for view in reactionButtons.removedViews {
|
||||
if animation.isAnimated {
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
|
||||
view?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func reactionTargetView(value: String) -> UIView? {
|
||||
for (_, button) in self.container.buttons {
|
||||
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View {
|
||||
return result.iconView
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
for (_, button) in self.container.buttons {
|
||||
button.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode {
|
||||
private let buttonsNode: MessageReactionButtonsNode
|
||||
|
||||
required init() {
|
||||
self.buttonsNode = MessageReactionButtonsNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.buttonsNode)
|
||||
|
||||
self.buttonsNode.reactionSelected = { [weak self] value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||
let buttonsNode = self.buttonsNode
|
||||
|
||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
//let displaySeparator: Bool
|
||||
let topOffset: CGFloat
|
||||
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
|
||||
//displaySeparator = false
|
||||
topOffset = 2.0
|
||||
} else {
|
||||
//displaySeparator = true
|
||||
topOffset = 0.0
|
||||
}
|
||||
|
||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
||||
let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
let buttonsUpdate = buttonsNode.prepareUpdate(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, constrainedWidth: constrainedSize.width, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing)
|
||||
|
||||
return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in
|
||||
var boundingSize = CGSize()
|
||||
|
||||
let buttonsSizeAndApply = buttonsUpdate.continueLayout(boundingWidth - (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right))
|
||||
|
||||
boundingSize = buttonsSizeAndApply.size
|
||||
|
||||
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
|
||||
boundingSize.height += topOffset + 2.0
|
||||
|
||||
return (boundingSize, { [weak self] animation, synchronousLoad in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.buttonsNode.layer, frame: CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: topOffset - 2.0), size: buttonsSizeAndApply.size), completion: nil)
|
||||
buttonsSizeAndApply.apply(animation)
|
||||
|
||||
let _ = synchronousLoad
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
self.buttonsNode.animateOut()
|
||||
}
|
||||
|
||||
override func animateInsertionIntoBubble(_ duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), to: CGPoint(), duration: duration, removeOnCompletion: true, additive: true)
|
||||
}
|
||||
|
||||
override func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) {
|
||||
self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), duration: duration, removeOnCompletion: false, additive: true)
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.buttonsNode.animateOut()
|
||||
}
|
||||
|
||||
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
if self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil) != nil {
|
||||
return .ignore
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override func reactionTargetView(value: String) -> UIView? {
|
||||
return self.buttonsNode.reactionTargetView(value: value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user