Swiftgram/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift
2021-12-10 19:34:57 +04:00

586 lines
27 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
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 let backgroundMaskView: UIView
private var backgroundMaskButtons: [String: UIView] = [:]
var reactionSelected: ((String) -> Void)?
override init() {
self.container = ReactionButtonsAsyncLayoutContainer()
self.backgroundMaskView = UIView()
super.init()
}
func update() {
}
func prepareUpdate(
context: AccountContext,
presentationData: ChatPresentationData,
presentationContext: ChatPresentationContext,
availableReactions: AvailableReactions?,
reactions: ReactionsMessageAttribute,
message: Message,
alignment: DisplayAlignment,
constrainedWidth: CGFloat,
type: DisplayType
) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
let reactionColors: ReactionButtonComponent.Colors
switch type {
case .incoming:
reactionColors = ReactionButtonComponent.Colors(
deselectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
selectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb,
deselectedForeground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
selectedForeground: presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb
)
case .outgoing:
reactionColors = ReactionButtonComponent.Colors(
deselectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
selectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb,
deselectedForeground: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
selectedForeground: presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb
)
case .freeform:
reactionColors = ReactionButtonComponent.Colors(
deselectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb,
selectedBackground: UIColor(white: 1.0, alpha: 0.8).argb,
deselectedForeground: UIColor(white: 1.0, alpha: 1.0).argb,
selectedForeground: UIColor(white: 0.0, alpha: 0.1).argb
)
}
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 iconFile: TelegramMediaFile?
if let availableReactions = availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction.value {
iconFile = availableReaction.staticIcon
break
}
}
}
var peers: [EnginePeer] = []
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) {
peers.removeAll()
}
return ReactionButtonsLayoutContainer.Reaction(
reaction: ReactionButtonComponent.Reaction(
value: reaction.value,
iconFile: iconFile
),
count: Int(reaction.count),
peers: peers,
isSelected: reaction.isSelected
)
},
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
}
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)
var validIds = Set<String>()
for item in reactionButtons.items {
validIds.insert(item.value)
switch alignment {
case .left:
if reactionButtonPosition.x + item.size.width > boundingWidth {
reactionButtonPosition.x = 0.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.view.superview == nil {
strongSelf.view.addSubview(item.view)
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)
}
item.view.frame = itemFrame
} else {
animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil)
}
if itemMaskView.superview == nil {
strongSelf.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: [String] = []
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?.removeFromSuperview()
})
} else {
view.removeFromSuperview()
}
}
}
for id in removeMaskIds {
strongSelf.backgroundMaskButtons.removeValue(forKey: id)
}
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()
}
}
})
})
}
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: String) -> UIView? {
for (key, button) in self.container.buttons {
if key == value {
return button.iconView
}
}
return nil
}
func animateIn(animation: ListViewItemUpdateAnimation) {
for (_, button) in self.container.buttons {
animation.animator.animateScale(layer: button.layer, from: 0.01, to: 1.0, completion: nil)
}
}
func animateOut(animation: ListViewItemUpdateAnimation) {
for (_, button) in self.container.buttons {
animation.animator.updateScale(layer: button.layer, scale: 0.01, completion: 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))
}
}
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 = 4.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,
presentationContext: item.controllerInteraction.presentationContext,
availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, message: item.message, alignment: .left, 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(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: String) -> 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 isIncoming: Bool
let constrainedWidth: CGFloat
init(
context: AccountContext,
presentationData: ChatPresentationData,
presentationContext: ChatPresentationContext,
availableReactions: AvailableReactions?,
reactions: ReactionsMessageAttribute,
message: Message,
isIncoming: Bool,
constrainedWidth: CGFloat
) {
self.context = context
self.presentationData = presentationData
self.presentationContext = presentationContext
self.availableReactions = availableReactions
self.reactions = reactions
self.message = message
self.isIncoming = isIncoming
self.constrainedWidth = constrainedWidth
}
}
private let buttonsNode: MessageReactionButtonsNode
var reactionSelected: ((String) -> Void)?
override init() {
self.buttonsNode = MessageReactionButtonsNode()
super.init()
self.addSubnode(self.buttonsNode)
self.buttonsNode.reactionSelected = { [weak self] value in
self?.reactionSelected?(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,
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: String) -> 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)
}
}