2021-12-21 17:31:20 +04:00

928 lines
44 KiB
Swift

import Foundation
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import UIKit
import WebPBinding
import AnimatedAvatarSetNode
import ContextUI
import AvatarNode
import ReactionImageComponent
private let avatarFont = avatarPlaceholderFont(size: 16.0)
public final class ReactionListContextMenuContent: ContextControllerItemsContent {
private final class BackButtonNode: HighlightTrackingButtonNode {
let highlightBackgroundNode: ASDisplayNode
let titleLabelNode: ImmediateTextNode
let separatorNode: ASDisplayNode
let iconNode: ASImageNode
var action: (() -> Void)?
private var theme: PresentationTheme?
init() {
self.highlightBackgroundNode = ASDisplayNode()
self.highlightBackgroundNode.alpha = 0.0
self.titleLabelNode = ImmediateTextNode()
self.titleLabelNode.maximumNumberOfLines = 1
self.titleLabelNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.separatorNode = ASDisplayNode()
super.init()
self.addSubnode(self.separatorNode)
self.addSubnode(self.highlightBackgroundNode)
self.addSubnode(self.titleLabelNode)
self.addSubnode(self.iconNode)
self.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.highlightBackgroundNode.alpha = 1.0
} else {
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
strongSelf.highlightBackgroundNode.alpha = 0.0
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
}
}
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.action?()
}
func update(size: CGSize, presentationData: PresentationData, isLast: Bool) {
let standardIconWidth: CGFloat = 32.0
let sideInset: CGFloat = 16.0
let iconSideInset: CGFloat = 12.0
if self.theme !== presentationData.theme {
self.theme = presentationData.theme
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: presentationData.theme.contextMenu.primaryColor)
}
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.titleLabelNode.attributedText = NSAttributedString(string: presentationData.strings.Common_Back, font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: size.width - sideInset - standardIconWidth, height: 100.0))
self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
if let iconImage = self.iconNode.image {
let iconWidth = max(standardIconWidth, iconImage.size.width)
let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size)
self.iconNode.frame = iconFrame
}
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))
self.separatorNode.isHidden = isLast
}
}
private final class ReactionTabListNode: ASDisplayNode {
private final class ItemNode: ASDisplayNode {
let context: AccountContext
let reaction: String?
let count: Int
let titleLabelNode: ImmediateTextNode
let iconNode: ASImageNode?
let reactionIconNode: ReactionImageNode?
private var theme: PresentationTheme?
var action: ((String?) -> Void)?
init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String?, count: Int) {
self.context = context
self.reaction = reaction
self.count = count
self.titleLabelNode = ImmediateTextNode()
self.titleLabelNode.isUserInteractionEnabled = false
if let reaction = reaction {
self.reactionIconNode = ReactionImageNode(context: context, availableReactions: availableReactions, reaction: reaction)
self.reactionIconNode?.isUserInteractionEnabled = false
self.iconNode = nil
} else {
self.reactionIconNode = nil
self.iconNode = ASImageNode()
self.iconNode?.isUserInteractionEnabled = false
}
super.init()
self.addSubnode(self.titleLabelNode)
if let iconNode = self.iconNode {
self.addSubnode(iconNode)
}
if let reactionIconNode = self.reactionIconNode {
self.addSubnode(reactionIconNode)
}
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.action?(self.reaction)
}
}
func update(presentationData: PresentationData, constrainedSize: CGSize, isSelected: Bool) -> CGSize {
if presentationData.theme !== self.theme {
self.theme = presentationData.theme
if let iconNode = self.iconNode {
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.contextMenu.primaryColor)
}
}
let sideInset: CGFloat = 12.0
let iconSpacing: CGFloat = 4.0
var iconSize = CGSize(width: 22.0, height: 22.0)
if let reactionIconNode = self.reactionIconNode {
iconSize = reactionIconNode.size.aspectFitted(iconSize)
} else if let iconNode = self.iconNode, let image = iconNode.image {
iconSize = image.size.aspectFitted(iconSize)
}
self.titleLabelNode.attributedText = NSAttributedString(string: "\(count)", font: Font.medium(11.0), textColor: presentationData.theme.contextMenu.primaryColor)
let titleSize = self.titleLabelNode.updateLayout(constrainedSize)
let contentSize = CGSize(width: sideInset * 2.0 + titleSize.width + iconSize.width + iconSpacing, height: titleSize.height)
self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floorToScreenPixels((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize)
if let reactionIconNode = self.reactionIconNode {
reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize)
} else if let iconNode = self.iconNode {
iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize)
}
return CGSize(width: contentSize.width, height: constrainedSize.height)
}
}
private let scrollNode: ASScrollNode
private let selectionHighlightNode: ASDisplayNode
private let itemNodes: [ItemNode]
private struct ScrollToTabReaction {
var value: String?
}
private var scrollToTabReaction: ScrollToTabReaction?
var action: ((String?) -> Void)?
init(context: AccountContext, availableReactions: AvailableReactions?, reactions: [(String?, Int)], message: EngineMessage) {
self.scrollNode = ASScrollNode()
self.scrollNode.canCancelAllTouchesInViews = true
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.itemNodes = reactions.map { reaction, count in
return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, count: count)
}
self.selectionHighlightNode = ASDisplayNode()
super.init()
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.selectionHighlightNode)
for itemNode in self.itemNodes {
self.scrollNode.addSubnode(itemNode)
itemNode.action = { [weak self] reaction in
guard let strongSelf = self else {
return
}
strongSelf.scrollToTabReaction = ScrollToTabReaction(value: reaction)
strongSelf.action?(reaction)
}
}
}
func update(size: CGSize, presentationData: PresentationData, selectedReaction: String?, transition: ContainedViewLayoutTransition) {
let sideInset: CGFloat = 11.0
let spacing: CGFloat = 0.0
let verticalInset: CGFloat = 7.0
self.selectionHighlightNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
let highlightHeight: CGFloat = size.height - verticalInset * 2.0
self.selectionHighlightNode.cornerRadius = highlightHeight / 2.0
var contentWidth: CGFloat = sideInset
for i in 0 ..< self.itemNodes.count {
if i != 0 {
contentWidth += spacing
}
let itemNode = self.itemNodes[i]
let itemSize = itemNode.update(presentationData: presentationData, constrainedSize: CGSize(width: size.width, height: size.height), isSelected: itemNode.reaction == selectedReaction)
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
itemNode.frame = itemFrame
if itemNode.reaction == selectedReaction {
transition.updateFrame(node: self.selectionHighlightNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: verticalInset), size: CGSize(width: itemFrame.width, height: highlightHeight)))
}
contentWidth += itemSize.width
}
contentWidth += sideInset
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
let contentSize = CGSize(width: contentWidth, height: size.height)
if self.scrollNode.view.contentSize != contentSize {
self.scrollNode.view.contentSize = contentSize
}
if let scrollToTabReaction = self.scrollToTabReaction {
self.scrollToTabReaction = nil
for itemNode in self.itemNodes {
if itemNode.reaction == scrollToTabReaction.value {
self.scrollNode.view.scrollRectToVisible(itemNode.frame.insetBy(dx: -sideInset, dy: 0.0), animated: transition.isAnimated)
break
}
}
}
}
}
private final class ReactionsTabNode: ASDisplayNode, UIScrollViewDelegate {
private final class ItemNode: HighlightTrackingButtonNode {
let context: AccountContext
let availableReactions: AvailableReactions?
let highlightBackgroundNode: ASDisplayNode
let avatarNode: AvatarNode
let titleLabelNode: ImmediateTextNode
let separatorNode: ASDisplayNode
var reactionIconNode: ReactionImageNode?
let action: () -> Void
init(context: AccountContext, availableReactions: AvailableReactions?, action: @escaping () -> Void) {
self.action = action
self.context = context
self.availableReactions = availableReactions
self.avatarNode = AvatarNode(font: avatarFont)
self.highlightBackgroundNode = ASDisplayNode()
self.highlightBackgroundNode.alpha = 0.0
self.titleLabelNode = ImmediateTextNode()
self.titleLabelNode.maximumNumberOfLines = 1
self.titleLabelNode.isUserInteractionEnabled = false
self.separatorNode = ASDisplayNode()
super.init()
self.addSubnode(self.separatorNode)
self.addSubnode(self.highlightBackgroundNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleLabelNode)
self.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.highlightBackgroundNode.alpha = 1.0
} else {
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
strongSelf.highlightBackgroundNode.alpha = 0.0
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
}
}
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.action()
}
func update(size: CGSize, presentationData: PresentationData, item: EngineMessageReactionListContext.Item, isLast: Bool, syncronousLoad: Bool) {
let avatarInset: CGFloat = 12.0
let avatarSpacing: CGFloat = 8.0
let avatarSize: CGFloat = 28.0
let sideInset: CGFloat = 16.0
let reaction: String? = item.reaction
if let reaction = reaction {
if self.reactionIconNode == nil {
let reactionIconNode = ReactionImageNode(context: self.context, availableReactions: self.availableReactions, reaction: reaction)
self.reactionIconNode = reactionIconNode
self.addSubnode(reactionIconNode)
}
} else if let reactionIconNode = self.reactionIconNode {
reactionIconNode.removeFromSupernode()
}
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.avatarNode.frame = CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: item.peer, synchronousLoad: true)
self.titleLabelNode.attributedText = NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
var maxTextWidth: CGFloat = size.width - avatarInset - avatarSize - avatarSpacing - sideInset
if reactionIconNode != nil {
maxTextWidth -= 32.0
}
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0))
self.titleLabelNode.frame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
if let reactionIconNode = self.reactionIconNode {
let reactionSize = reactionIconNode.size.aspectFitted(CGSize(width: 22.0, height: 22.0))
reactionIconNode.frame = CGRect(origin: CGPoint(x: size.width - 32.0 - floor((32.0 - reactionSize.width) / 2.0), y: floor((size.height - reactionSize.height) / 2.0)), size: reactionSize)
}
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel))
self.separatorNode.isHidden = isLast
}
}
private struct ItemsState {
let listState: EngineMessageReactionListContext.State
let readStats: MessageReadStats?
let mergedItems: [EngineMessageReactionListContext.Item]
init(listState: EngineMessageReactionListContext.State, readStats: MessageReadStats?) {
self.listState = listState
self.readStats = readStats
var mergedItems: [EngineMessageReactionListContext.Item] = listState.items
if !listState.canLoadMore, let readStats = readStats {
var existingPeers = Set(mergedItems.map(\.peer.id))
for peer in readStats.peers {
if !existingPeers.contains(peer.id) {
existingPeers.insert(peer.id)
mergedItems.append(EngineMessageReactionListContext.Item(peer: peer, reaction: nil))
}
}
}
self.mergedItems = mergedItems
}
var totalCount: Int {
if !self.listState.canLoadMore {
return self.mergedItems.count
} else {
var value = self.listState.totalCount
if let readStats = self.readStats {
value = max(value, readStats.peers.count)
}
return value
}
}
var canLoadMore: Bool {
return self.listState.canLoadMore
}
func item(at index: Int) -> EngineMessageReactionListContext.Item? {
if index < self.mergedItems.count {
return self.mergedItems[index]
} else {
return nil
}
}
}
private let context: AccountContext
private let availableReactions: AvailableReactions?
let reaction: String?
private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
private let openPeer: (PeerId) -> Void
private var hasMore: Bool = false
private let scrollNode: ASScrollNode
private var ignoreScrolling: Bool = false
private var presentationData: PresentationData?
private var currentSize: CGSize?
private var apparentHeight: CGFloat = 0.0
private let listContext: EngineMessageReactionListContext
private var state: ItemsState
private var stateDisposable: Disposable?
private var itemNodes: [Int: ItemNode] = [:]
private var placeholderItemImage: UIImage?
private var placeholderLayers: [Int: SimpleLayer] = [:]
init(
context: AccountContext,
availableReactions: AvailableReactions?,
message: EngineMessage,
reaction: String?,
readStats: MessageReadStats?,
requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
openPeer: @escaping (PeerId) -> Void
) {
self.context = context
self.availableReactions = availableReactions
self.reaction = reaction
self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight
self.openPeer = openPeer
self.listContext = context.engine.messages.messageReactionList(message: message, reaction: reaction)
self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, reaction: reaction), readStats: readStats)
self.scrollNode = ASScrollNode()
self.scrollNode.canCancelAllTouchesInViews = true
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.showsVerticalScrollIndicator = false
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.clipsToBounds = false
super.init()
self.addSubnode(self.scrollNode)
self.scrollNode.view.delegate = self
self.clipsToBounds = true
self.stateDisposable = (self.listContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
let updatedState = ItemsState(listState: state, readStats: strongSelf.state.readStats)
var animateIn = false
if strongSelf.state.item(at: 0) == nil && updatedState.item(at: 0) != nil {
animateIn = true
}
strongSelf.state = updatedState
strongSelf.requestUpdate(strongSelf, animateIn ? .animated(duration: 0.2, curve: .easeInOut) : .immediate)
if animateIn {
for (_, itemNode) in strongSelf.itemNodes {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
})
}
deinit {
self.stateDisposable?.dispose()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.ignoreScrolling {
return
}
self.updateVisibleItems(animated: false, syncronousLoad: false)
if let size = self.currentSize {
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
apparentHeight = max(apparentHeight, 44.0)
apparentHeight = min(apparentHeight, size.height + 100.0)
if self.apparentHeight != apparentHeight {
self.apparentHeight = apparentHeight
self.requestUpdateApparentHeight(self, .immediate)
}
}
}
private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) {
guard let size = self.currentSize else {
return
}
guard let presentationData = self.presentationData else {
return
}
let itemHeight: CGFloat = 44.0
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0)
var validIds = Set<Int>()
var validPlaceholderIds = Set<Int>()
let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight)))
let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight))
if minVisibleIndex <= maxVisibleIndex {
for index in minVisibleIndex ... maxVisibleIndex {
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight))
if let item = self.state.item(at: index) {
validIds.insert(index)
let itemNode: ItemNode
if let current = self.itemNodes[index] {
itemNode = current
} else {
let openPeer = self.openPeer
let peerId = item.peer.id
itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: {
openPeer(peerId)
})
self.itemNodes[index] = itemNode
self.scrollNode.addSubnode(itemNode)
}
itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad)
itemNode.frame = itemFrame
} else if index < self.state.totalCount {
validPlaceholderIds.insert(index)
let placeholderLayer: SimpleLayer
if let current = self.placeholderLayers[index] {
placeholderLayer = current
} else {
placeholderLayer = SimpleLayer()
if let placeholderItemImage = self.placeholderItemImage {
ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage)
}
self.placeholderLayers[index] = placeholderLayer
self.scrollNode.layer.addSublayer(placeholderLayer)
}
placeholderLayer.frame = itemFrame
}
}
}
var removeIds: [Int] = []
for (id, itemNode) in self.itemNodes {
if !validIds.contains(id) {
removeIds.append(id)
itemNode.removeFromSupernode()
}
}
for id in removeIds {
self.itemNodes.removeValue(forKey: id)
}
var removePlaceholderIds: [Int] = []
for (id, placeholderLayer) in self.placeholderLayers {
if !validPlaceholderIds.contains(id) {
removePlaceholderIds.append(id)
if animated {
placeholderLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderLayer] _ in
placeholderLayer?.removeFromSuperlayer()
})
} else {
placeholderLayer.removeFromSuperlayer()
}
}
}
for id in removePlaceholderIds {
self.placeholderLayers.removeValue(forKey: id)
}
if self.state.canLoadMore && maxVisibleIndex >= self.state.listState.items.count - 16 {
self.listContext.loadMore()
}
}
func update(presentationData: PresentationData, constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) {
let itemHeight: CGFloat = 44.0
if self.presentationData?.theme !== presentationData.theme {
let sideInset: CGFloat = 40.0
let avatarInset: CGFloat = 12.0
let avatarSpacing: CGFloat = 8.0
let avatarSize: CGFloat = 28.0
let lineHeight: CGFloat = 8.0
let shimmeringForegroundColor: UIColor
let shimmeringColor: UIColor
if presentationData.theme.overallDarkAppearance {
let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0)
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1)
shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3)
} else {
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07)
shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3)
}
let _ = shimmeringColor
self.placeholderItemImage = generateImage(CGSize(width: avatarInset + avatarSize + avatarSpacing + lineHeight + 2.0 + sideInset, height: itemHeight), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(shimmeringForegroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - lineHeight) / 2.0)), size: CGSize(width: lineHeight + 2.0, height: lineHeight)))
})?.stretchableImage(withLeftCapWidth: Int(avatarInset + avatarSize + avatarSpacing + lineHeight / 2.0 + 1.0), topCapHeight: 0)
if let placeholderItemImage = self.placeholderItemImage {
for (_, placeholderLayer) in self.placeholderLayers {
ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage)
}
}
}
self.presentationData = presentationData
let size = CGSize(width: constrainedSize.width, height: CGFloat(self.state.totalCount) * itemHeight)
let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height))
self.currentSize = containerSize
self.ignoreScrolling = true
if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) {
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
}
if self.scrollNode.view.contentSize != size {
self.scrollNode.view.contentSize = size
}
self.ignoreScrolling = false
self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated)
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
apparentHeight = max(apparentHeight, 44.0)
apparentHeight = min(apparentHeight, containerSize.height + 100.0)
self.apparentHeight = apparentHeight
return (containerSize, apparentHeight)
}
}
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode {
private let context: AccountContext
private let availableReactions: AvailableReactions?
private let reactions: [(String?, Int)]
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
private var presentationData: PresentationData
private var backButtonNode: BackButtonNode?
private var separatorNode: ASDisplayNode?
private var tabListNode: ReactionTabListNode?
private var currentTabNode: ReactionsTabNode
private var dismissedTabNode: ReactionsTabNode?
private let openPeer: (PeerId) -> Void
private(set) var apparentHeight: CGFloat = 0.0
init(
context: AccountContext,
availableReactions: AvailableReactions?,
message: EngineMessage,
reaction: String?,
readStats: MessageReadStats?,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
back: (() -> Void)?,
openPeer: @escaping (PeerId) -> Void
) {
self.context = context
self.availableReactions = availableReactions
self.openPeer = openPeer
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight
var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
if let back = back {
self.backButtonNode = BackButtonNode()
self.backButtonNode?.action = {
back()
}
}
var reactions: [(String?, Int)] = []
var totalCount: Int = 0
if let reactionsAttribute = message._asMessage().reactionsAttribute {
for listReaction in reactionsAttribute.reactions {
if reaction == nil || listReaction.value == reaction {
totalCount += Int(listReaction.count)
reactions.append((listReaction.value, Int(listReaction.count)))
}
}
}
if reaction == nil {
reactions.insert((nil, totalCount), at: 0)
}
if reactions.count > 2 && totalCount > 10 {
self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message)
}
self.reactions = reactions
self.currentTabNode = ReactionsTabNode(
context: context,
availableReactions: availableReactions,
message: message,
reaction: reaction,
readStats: readStats,
requestUpdate: { tab, transition in
requestUpdateTab?(tab, transition)
},
requestUpdateApparentHeight: { tab, transition in
requestUpdateTabApparentHeight?(tab, transition)
},
openPeer: { id in
openPeer(id)
}
)
super.init()
if self.backButtonNode != nil || self.tabListNode != nil {
self.separatorNode = ASDisplayNode()
}
if let backButtonNode = self.backButtonNode {
self.addSubnode(backButtonNode)
}
if let tabListNode = self.tabListNode {
self.addSubnode(tabListNode)
}
if let separatorNode = self.separatorNode {
self.addSubnode(separatorNode)
}
self.addSubnode(self.currentTabNode)
self.tabListNode?.action = { [weak self] reaction in
guard let strongSelf = self else {
return
}
if strongSelf.currentTabNode.reaction != reaction {
strongSelf.dismissedTabNode = strongSelf.currentTabNode
let currentTabNode = ReactionsTabNode(
context: context,
availableReactions: availableReactions,
message: message,
reaction: reaction,
readStats: nil,
requestUpdate: { tab, transition in
requestUpdateTab?(tab, transition)
},
requestUpdateApparentHeight: { tab, transition in
requestUpdateTabApparentHeight?(tab, transition)
},
openPeer: { id in
openPeer(id)
}
)
strongSelf.currentTabNode = currentTabNode
strongSelf.addSubnode(currentTabNode)
strongSelf.requestUpdate(.animated(duration: 0.45, curve: .spring))
}
}
requestUpdateTab = { [weak self] tab, transition in
guard let strongSelf = self else {
return
}
if strongSelf.currentTabNode == tab {
strongSelf.requestUpdate(transition)
}
}
requestUpdateTabApparentHeight = { [weak self] tab, transition in
guard let strongSelf = self else {
return
}
if strongSelf.currentTabNode == tab {
strongSelf.requestUpdateApparentHeight(transition)
}
}
}
func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) {
let constrainedSize = CGSize(width: min(260.0, constrainedWidth), height: maxHeight)
var topContentHeight: CGFloat = 0.0
if let backButtonNode = self.backButtonNode {
let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: self.tabListNode == nil)
transition.updateFrame(node: backButtonNode, frame: backButtonFrame)
topContentHeight += backButtonFrame.height
}
if let tabListNode = self.tabListNode {
let tabListFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
tabListNode.update(size: tabListFrame.size, presentationData: self.presentationData, selectedReaction: self.currentTabNode.reaction, transition: transition)
transition.updateFrame(node: tabListNode, frame: tabListFrame)
topContentHeight += tabListFrame.height
}
if let separatorNode = self.separatorNode {
let separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 7.0))
separatorNode.backgroundColor = self.presentationData.theme.contextMenu.sectionSeparatorColor
transition.updateFrame(node: separatorNode, frame: separatorFrame)
topContentHeight += separatorFrame.height
}
var currentTabTransition = transition
if self.currentTabNode.bounds.isEmpty {
currentTabTransition = .immediate
}
let currentTabLayout = self.currentTabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition)
currentTabTransition.updateFrame(node: self.currentTabNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: currentTabLayout.size.width, height: currentTabLayout.size.height + 100.0)))
if let dismissedTabNode = self.dismissedTabNode {
self.dismissedTabNode = nil
if let previousIndex = self.reactions.firstIndex(where: { $0.0 == dismissedTabNode.reaction }), let currentIndex = self.reactions.firstIndex(where: { $0.0 == self.currentTabNode.reaction }) {
let offset = previousIndex < currentIndex ? currentTabLayout.size.width : -currentTabLayout.size.width
transition.updateFrame(node: dismissedTabNode, frame: dismissedTabNode.frame.offsetBy(dx: -offset, dy: 0.0), completion: { [weak dismissedTabNode] _ in
dismissedTabNode?.removeFromSupernode()
})
transition.animatePositionAdditive(node: self.currentTabNode, offset: CGPoint(x: offset, y: 0.0))
} else {
dismissedTabNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dismissedTabNode] _ in
dismissedTabNode?.removeFromSupernode()
})
self.currentTabNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let contentSize = CGSize(width: currentTabLayout.size.width, height: topContentHeight + currentTabLayout.size.height)
let apparentHeight = topContentHeight + currentTabLayout.apparentHeight
return (contentSize, apparentHeight)
}
}
let context: AccountContext
let availableReactions: AvailableReactions?
let message: EngineMessage
let reaction: String?
let readStats: MessageReadStats?
let back: (() -> Void)?
let openPeer: (PeerId) -> Void
public init(
context: AccountContext,
availableReactions: AvailableReactions?,
message: EngineMessage,
reaction: String?,
readStats: MessageReadStats?,
back: (() -> Void)?,
openPeer: @escaping (PeerId) -> Void
) {
self.context = context
self.availableReactions = availableReactions
self.message = message
self.reaction = reaction
self.readStats = readStats
self.back = back
self.openPeer = openPeer
}
public func node(
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerItemsNode {
return ItemsNode(
context: self.context,
availableReactions: self.availableReactions,
message: self.message,
reaction: self.reaction,
readStats: self.readStats,
requestUpdate: requestUpdate,
requestUpdateApparentHeight: requestUpdateApparentHeight,
back: self.back,
openPeer: self.openPeer
)
}
}