mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
703 lines
34 KiB
Swift
703 lines
34 KiB
Swift
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
|
|
import WallpaperBackgroundNode
|
|
import ChatControllerInteraction
|
|
import ChatMessageBubbleContentNode
|
|
import ChatMessageItemCommon
|
|
|
|
final class MessageReactionButtonsNode: ASDisplayNode {
|
|
enum DisplayType {
|
|
case incoming
|
|
case outgoing
|
|
case freeform
|
|
}
|
|
|
|
enum DisplayAlignment {
|
|
case left
|
|
case right
|
|
}
|
|
|
|
private var bubbleBackgroundNode: WallpaperBubbleBackgroundNode?
|
|
private let container: ReactionButtonsAsyncLayoutContainer
|
|
private var backgroundMaskView: UIView?
|
|
private var backgroundMaskButtons: [MessageReaction.Reaction: UIView] = [:]
|
|
|
|
var reactionSelected: ((MessageReaction.Reaction) -> Void)?
|
|
var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)?
|
|
|
|
override init() {
|
|
self.container = ReactionButtonsAsyncLayoutContainer()
|
|
|
|
super.init()
|
|
}
|
|
|
|
deinit {
|
|
|
|
}
|
|
|
|
func update() {
|
|
}
|
|
|
|
func prepareUpdate(
|
|
context: AccountContext,
|
|
presentationData: ChatPresentationData,
|
|
presentationContext: ChatPresentationContext,
|
|
availableReactions: AvailableReactions?,
|
|
reactions: ReactionsMessageAttribute,
|
|
accountPeer: EnginePeer?,
|
|
message: Message,
|
|
alignment: DisplayAlignment,
|
|
constrainedWidth: CGFloat,
|
|
type: DisplayType
|
|
) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
|
|
let reactionColors: ReactionButtonComponent.Colors
|
|
let themeColors: PresentationThemeBubbleColorComponents
|
|
switch type {
|
|
case .incoming:
|
|
themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
|
reactionColors = ReactionButtonComponent.Colors(
|
|
deselectedBackground: themeColors.reactionInactiveBackground.argb,
|
|
selectedBackground: themeColors.reactionActiveBackground.argb,
|
|
deselectedForeground: themeColors.reactionInactiveForeground.argb,
|
|
selectedForeground: themeColors.reactionActiveForeground.argb,
|
|
extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
|
|
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
|
|
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
|
|
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
|
|
)
|
|
case .outgoing:
|
|
themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
|
reactionColors = ReactionButtonComponent.Colors(
|
|
deselectedBackground: themeColors.reactionInactiveBackground.argb,
|
|
selectedBackground: themeColors.reactionActiveBackground.argb,
|
|
deselectedForeground: themeColors.reactionInactiveForeground.argb,
|
|
selectedForeground: themeColors.reactionActiveForeground.argb,
|
|
extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
|
|
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
|
|
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
|
|
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
|
|
)
|
|
case .freeform:
|
|
if presentationData.theme.wallpaper.isEmpty {
|
|
themeColors = presentationData.theme.theme.chat.message.freeform.withoutWallpaper
|
|
} else {
|
|
themeColors = presentationData.theme.theme.chat.message.freeform.withWallpaper
|
|
}
|
|
|
|
reactionColors = ReactionButtonComponent.Colors(
|
|
deselectedBackground: selectReactionFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb,
|
|
selectedBackground: themeColors.reactionActiveBackground.argb,
|
|
deselectedForeground: themeColors.reactionInactiveForeground.argb,
|
|
selectedForeground: themeColors.reactionActiveForeground.argb,
|
|
extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
|
|
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
|
|
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
|
|
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
|
|
)
|
|
}
|
|
|
|
var totalReactionCount: Int = 0
|
|
for reaction in reactions.reactions {
|
|
totalReactionCount += Int(reaction.count)
|
|
}
|
|
|
|
let reactionButtonsResult = 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 centerAnimation: TelegramMediaFile?
|
|
var animationFileId: Int64?
|
|
|
|
switch reaction.value {
|
|
case .builtin:
|
|
if let availableReactions = availableReactions {
|
|
for availableReaction in availableReactions.reactions {
|
|
if availableReaction.value == reaction.value {
|
|
centerAnimation = availableReaction.centerAnimation
|
|
break
|
|
}
|
|
}
|
|
}
|
|
case let .custom(fileId):
|
|
animationFileId = fileId
|
|
}
|
|
|
|
var peers: [EnginePeer] = []
|
|
|
|
if message.id.peerId.namespace == Namespaces.Peer.CloudUser {
|
|
if reaction.isSelected, let accountPeer = accountPeer {
|
|
peers.append(accountPeer)
|
|
}
|
|
if !reaction.isSelected || reaction.count >= 2 {
|
|
if let peer = message.peers[message.id.peerId] {
|
|
peers.append(EnginePeer(peer))
|
|
}
|
|
}
|
|
} else {
|
|
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
|
} else {
|
|
for recentPeer in reactions.recentPeers {
|
|
if recentPeer.value == reaction.value {
|
|
if let peer = message.peers[recentPeer.peerId] {
|
|
peers.append(EnginePeer(peer))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if peers.count != Int(reaction.count) || totalReactionCount != reactions.recentPeers.count {
|
|
peers.removeAll()
|
|
}
|
|
}
|
|
|
|
return ReactionButtonsAsyncLayoutContainer.Reaction(
|
|
reaction: ReactionButtonComponent.Reaction(
|
|
value: reaction.value,
|
|
centerAnimation: centerAnimation,
|
|
animationFileId: animationFileId
|
|
),
|
|
count: Int(reaction.count),
|
|
peers: peers,
|
|
chosenOrder: reaction.chosenOrder
|
|
)
|
|
},
|
|
colors: reactionColors,
|
|
constrainedWidth: constrainedWidth
|
|
)
|
|
|
|
var reactionButtonsSize = CGSize()
|
|
var currentRowWidth: CGFloat = 0.0
|
|
for item in reactionButtonsResult.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 && !reactionButtonsResult.items.isEmpty {
|
|
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
|
|
if !reactionButtonsSize.height.isZero {
|
|
reactionButtonsSize.height += 6.0
|
|
}
|
|
reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
|
|
}
|
|
|
|
let topInset: CGFloat = 0.0
|
|
let bottomInset: CGFloat = 2.0
|
|
|
|
return (proposedWidth: reactionButtonsSize.width, continueLayout: { [weak self] boundingWidth in
|
|
let size = CGSize(width: boundingWidth, height: topInset + reactionButtonsSize.height + bottomInset)
|
|
return (size: size, apply: { animation in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
if strongSelf.backgroundMaskView == nil {
|
|
strongSelf.backgroundMaskView = UIView()
|
|
}
|
|
|
|
let backgroundInsets: CGFloat = 10.0
|
|
|
|
switch type {
|
|
case .freeform:
|
|
if let backgroundNode = presentationContext.backgroundNode, backgroundNode.hasBubbleBackground(for: .free) {
|
|
let bubbleBackgroundFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -backgroundInsets, dy: -backgroundInsets)
|
|
if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode {
|
|
animation.animator.updateFrame(layer: bubbleBackgroundNode.layer, frame: bubbleBackgroundFrame, completion: nil)
|
|
if let (rect, containerSize) = strongSelf.absoluteRect {
|
|
bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: animation.transition)
|
|
}
|
|
} else if strongSelf.bubbleBackgroundNode == nil {
|
|
if let bubbleBackgroundNode = backgroundNode.makeBubbleBackground(for: .free) {
|
|
strongSelf.bubbleBackgroundNode = bubbleBackgroundNode
|
|
bubbleBackgroundNode.view.mask = strongSelf.backgroundMaskView
|
|
strongSelf.insertSubnode(bubbleBackgroundNode, at: 0)
|
|
bubbleBackgroundNode.frame = bubbleBackgroundFrame
|
|
}
|
|
}
|
|
} else {
|
|
if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode {
|
|
strongSelf.bubbleBackgroundNode = nil
|
|
bubbleBackgroundNode.removeFromSupernode()
|
|
}
|
|
}
|
|
case .incoming, .outgoing:
|
|
if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode {
|
|
strongSelf.bubbleBackgroundNode = nil
|
|
bubbleBackgroundNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
var reactionButtonPosition: CGPoint
|
|
switch alignment {
|
|
case .left:
|
|
reactionButtonPosition = CGPoint(x: -1.0, y: topInset)
|
|
case .right:
|
|
reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset)
|
|
}
|
|
|
|
let reactionButtons = reactionButtonsResult.apply(
|
|
animation,
|
|
ReactionButtonsAsyncLayoutContainer.Arguments(
|
|
animationCache: presentationContext.animationCache,
|
|
animationRenderer: presentationContext.animationRenderer
|
|
)
|
|
)
|
|
|
|
var validIds = Set<MessageReaction.Reaction>()
|
|
for item in reactionButtons.items {
|
|
validIds.insert(item.value)
|
|
|
|
switch alignment {
|
|
case .left:
|
|
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
|
reactionButtonPosition.x = -1.0
|
|
reactionButtonPosition.y += item.size.height + 6.0
|
|
}
|
|
case .right:
|
|
if reactionButtonPosition.x - item.size.width < -1.0 {
|
|
reactionButtonPosition.x = size.width + 1.0
|
|
reactionButtonPosition.y += item.size.height + 6.0
|
|
}
|
|
}
|
|
|
|
let itemFrame: CGRect
|
|
switch alignment {
|
|
case .left:
|
|
itemFrame = CGRect(origin: reactionButtonPosition, size: item.size)
|
|
reactionButtonPosition.x += item.size.width + 6.0
|
|
case .right:
|
|
itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size)
|
|
reactionButtonPosition.x -= item.size.width + 6.0
|
|
}
|
|
|
|
let itemMaskFrame = itemFrame.offsetBy(dx: backgroundInsets, dy: backgroundInsets)
|
|
|
|
let itemMaskView: UIView
|
|
if let current = strongSelf.backgroundMaskButtons[item.value] {
|
|
itemMaskView = current
|
|
} else {
|
|
itemMaskView = UIView()
|
|
itemMaskView.backgroundColor = .black
|
|
itemMaskView.clipsToBounds = true
|
|
itemMaskView.layer.cornerRadius = 15.0
|
|
strongSelf.backgroundMaskButtons[item.value] = itemMaskView
|
|
}
|
|
|
|
if item.node.view.superview != strongSelf.view {
|
|
assert(item.node.view.superview == nil)
|
|
strongSelf.view.addSubview(item.node.view)
|
|
if animation.isAnimated {
|
|
item.node.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
item.node.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
item.node.view.frame = itemFrame
|
|
} else {
|
|
animation.animator.updateFrame(layer: item.node.view.layer, frame: itemFrame, completion: nil)
|
|
}
|
|
|
|
let itemValue = item.value
|
|
let itemNode = item.node
|
|
item.node.view.isGestureEnabled = true
|
|
let canViewReactionList = canViewMessageReactionList(message: message)
|
|
item.node.view.activateAfterCompletion = !canViewReactionList
|
|
item.node.view.activated = { [weak itemNode] gesture, _ in
|
|
guard let strongSelf = self, let itemNode = itemNode else {
|
|
gesture.cancel()
|
|
return
|
|
}
|
|
if !canViewReactionList {
|
|
return
|
|
}
|
|
strongSelf.openReactionPreview?(gesture, itemNode.view.containerView, itemValue)
|
|
}
|
|
item.node.view.additionalActivationProgressLayer = itemMaskView.layer
|
|
|
|
if let backgroundMaskView = strongSelf.backgroundMaskView {
|
|
if itemMaskView.superview != backgroundMaskView {
|
|
assert(itemMaskView.superview == nil)
|
|
backgroundMaskView.addSubview(itemMaskView)
|
|
if animation.isAnimated {
|
|
itemMaskView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
itemMaskView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
itemMaskView.frame = itemMaskFrame
|
|
} else {
|
|
animation.animator.updateFrame(layer: itemMaskView.layer, frame: itemMaskFrame, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
var removeMaskIds: [MessageReaction.Reaction] = []
|
|
for (id, view) in strongSelf.backgroundMaskButtons {
|
|
if !validIds.contains(id) {
|
|
removeMaskIds.append(id)
|
|
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?.layer.removeAllAnimations()
|
|
view?.removeFromSuperview()
|
|
})
|
|
} else {
|
|
view.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
for id in removeMaskIds {
|
|
strongSelf.backgroundMaskButtons.removeValue(forKey: id)
|
|
}
|
|
|
|
for node in reactionButtons.removedNodes {
|
|
if animation.isAnimated {
|
|
node.view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
|
node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
node.view.removeFromSuperview()
|
|
})
|
|
} else {
|
|
node.view.removeFromSuperview()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
private var absoluteRect: (CGRect, CGSize)?
|
|
|
|
func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.absoluteRect = (rect, containerSize)
|
|
|
|
if let bubbleBackgroundNode = self.bubbleBackgroundNode {
|
|
bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: transition)
|
|
}
|
|
}
|
|
|
|
func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) {
|
|
self.absoluteRect = (rect, containerSize)
|
|
|
|
if let bubbleBackgroundNode = self.bubbleBackgroundNode {
|
|
bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: transition)
|
|
}
|
|
}
|
|
|
|
func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
|
|
if let bubbleBackgroundNode = self.bubbleBackgroundNode {
|
|
bubbleBackgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration)
|
|
}
|
|
}
|
|
|
|
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
|
if let bubbleBackgroundNode = self.bubbleBackgroundNode {
|
|
bubbleBackgroundNode.offsetSpring(value: value, duration: duration, damping: damping)
|
|
}
|
|
}
|
|
|
|
func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
|
|
for (key, button) in self.container.buttons {
|
|
if key == value {
|
|
return button.view.iconView
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func animateIn(animation: ListViewItemUpdateAnimation) {
|
|
for (_, button) in self.container.buttons {
|
|
animation.animator.animateScale(layer: button.view.layer, from: 0.01, to: 1.0, completion: nil)
|
|
}
|
|
}
|
|
|
|
func animateOut(animation: ListViewItemUpdateAnimation) {
|
|
for (_, button) in self.container.buttons {
|
|
animation.animator.updateScale(layer: button.view.layer, scale: 0.01, completion: nil)
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
for (_, button) in self.container.buttons {
|
|
if button.view.frame.contains(point) {
|
|
if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
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, .reaction(value))
|
|
}
|
|
|
|
self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
|
|
guard let strongSelf = self, let item = strongSelf.item else {
|
|
gesture?.cancel()
|
|
return
|
|
}
|
|
|
|
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, 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, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> 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 = 4.0
|
|
} else {
|
|
//displaySeparator = true
|
|
topOffset = 0.0
|
|
}
|
|
|
|
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
|
let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
|
let buttonsUpdate = buttonsNode.prepareUpdate(
|
|
context: item.context,
|
|
presentationData: item.presentationData,
|
|
presentationContext: item.controllerInteraction.presentationContext,
|
|
availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, accountPeer: item.associatedData.accountPeer, message: item.message, alignment: .left, constrainedWidth: constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, 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(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false)))
|
|
}
|
|
|
|
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(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false)))
|
|
}
|
|
|
|
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
|
if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil), result !== self.buttonsNode.view {
|
|
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), result !== self.buttonsNode.view {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
|
|
return self.buttonsNode.reactionTargetView(value: value)
|
|
}
|
|
}
|
|
|
|
final class ChatMessageReactionButtonsNode: ASDisplayNode {
|
|
final class Arguments {
|
|
let context: AccountContext
|
|
let presentationData: ChatPresentationData
|
|
let presentationContext: ChatPresentationContext
|
|
let availableReactions: AvailableReactions?
|
|
let reactions: ReactionsMessageAttribute
|
|
let message: Message
|
|
let accountPeer: EnginePeer?
|
|
let isIncoming: Bool
|
|
let constrainedWidth: CGFloat
|
|
|
|
init(
|
|
context: AccountContext,
|
|
presentationData: ChatPresentationData,
|
|
presentationContext: ChatPresentationContext,
|
|
availableReactions: AvailableReactions?,
|
|
reactions: ReactionsMessageAttribute,
|
|
message: Message,
|
|
accountPeer: EnginePeer?,
|
|
isIncoming: Bool,
|
|
constrainedWidth: CGFloat
|
|
) {
|
|
self.context = context
|
|
self.presentationData = presentationData
|
|
self.presentationContext = presentationContext
|
|
self.availableReactions = availableReactions
|
|
self.reactions = reactions
|
|
self.message = message
|
|
self.accountPeer = accountPeer
|
|
self.isIncoming = isIncoming
|
|
self.constrainedWidth = constrainedWidth
|
|
}
|
|
}
|
|
|
|
private let buttonsNode: MessageReactionButtonsNode
|
|
|
|
var reactionSelected: ((MessageReaction.Reaction) -> Void)?
|
|
var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)?
|
|
|
|
override init() {
|
|
self.buttonsNode = MessageReactionButtonsNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.buttonsNode)
|
|
|
|
self.buttonsNode.reactionSelected = { [weak self] value in
|
|
self?.reactionSelected?(value)
|
|
}
|
|
|
|
self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
|
|
self?.openReactionPreview?(gesture, sourceNode, value)
|
|
}
|
|
}
|
|
|
|
class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) {
|
|
return { arguments in
|
|
let node = maybeNode ?? ChatMessageReactionButtonsNode()
|
|
|
|
let buttonsUpdate = node.buttonsNode.prepareUpdate(
|
|
context: arguments.context,
|
|
presentationData: arguments.presentationData,
|
|
presentationContext: arguments.presentationContext,
|
|
availableReactions: arguments.availableReactions,
|
|
reactions: arguments.reactions,
|
|
accountPeer: arguments.accountPeer,
|
|
message: arguments.message,
|
|
alignment: arguments.isIncoming ? .left : .right,
|
|
constrainedWidth: arguments.constrainedWidth,
|
|
type: .freeform
|
|
)
|
|
|
|
return (buttonsUpdate.proposedWidth, { constrainedWidth in
|
|
let buttonsResult = buttonsUpdate.continueLayout(constrainedWidth)
|
|
|
|
return (CGSize(width: constrainedWidth, height: buttonsResult.size.height), { animation in
|
|
node.buttonsNode.frame = CGRect(origin: CGPoint(), size: buttonsResult.size)
|
|
buttonsResult.apply(animation)
|
|
|
|
return node
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func animateIn(animation: ListViewItemUpdateAnimation) {
|
|
self.buttonsNode.animateIn(animation: animation)
|
|
self.buttonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
func animateOut(animation: ListViewItemUpdateAnimation, completion: @escaping () -> Void) {
|
|
self.buttonsNode.animateOut(animation: animation)
|
|
animation.animator.updateAlpha(layer: self.buttonsNode.layer, alpha: 0.0, completion: { _ in
|
|
completion()
|
|
})
|
|
animation.animator.updateFrame(layer: self.buttonsNode.layer, frame: self.buttonsNode.layer.frame.offsetBy(dx: 0.0, dy: -self.buttonsNode.layer.bounds.height / 2.0), completion: nil)
|
|
}
|
|
|
|
func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
|
|
return self.buttonsNode.reactionTargetView(value: value)
|
|
}
|
|
|
|
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), result !== self.buttonsNode.view {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.buttonsNode.update(rect: rect, within: containerSize, transition: transition)
|
|
}
|
|
|
|
func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) {
|
|
self.buttonsNode.update(rect: rect, within: containerSize, transition: transition)
|
|
}
|
|
|
|
func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
|
|
self.buttonsNode.offset(value: value, animationCurve: animationCurve, duration: duration)
|
|
}
|
|
|
|
func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
|
self.buttonsNode.offsetSpring(value: value, duration: duration, damping: damping)
|
|
}
|
|
}
|