mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
318 lines
19 KiB
Swift
318 lines
19 KiB
Swift
import Foundation
|
||
import UIKit
|
||
import Display
|
||
import AsyncDisplayKit
|
||
import SwiftSignalKit
|
||
import TelegramCore
|
||
import TelegramPresentationData
|
||
import ItemListUI
|
||
import PresentationDataUtils
|
||
|
||
final class MessageStatsOverviewItem: ListViewItem, ItemListItem {
|
||
let presentationData: ItemListPresentationData
|
||
let stats: PostStats
|
||
let publicShares: Int32?
|
||
let reactions: Int32
|
||
let sectionId: ItemListSectionId
|
||
let style: ItemListStyle
|
||
|
||
init(presentationData: ItemListPresentationData, stats: PostStats, publicShares: Int32?, reactions: Int32, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||
self.presentationData = presentationData
|
||
self.stats = stats
|
||
self.publicShares = publicShares
|
||
self.reactions = reactions
|
||
self.sectionId = sectionId
|
||
self.style = style
|
||
}
|
||
|
||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||
async {
|
||
let node = MessageStatsOverviewItemNode()
|
||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||
|
||
node.contentSize = layout.contentSize
|
||
node.insets = layout.insets
|
||
|
||
Queue.mainQueue().async {
|
||
completion(node, {
|
||
return (nil, { _ in apply() })
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||
Queue.mainQueue().async {
|
||
if let nodeValue = node() as? MessageStatsOverviewItemNode {
|
||
let makeLayout = nodeValue.asyncLayout()
|
||
|
||
async {
|
||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||
Queue.mainQueue().async {
|
||
completion(layout, { _ in
|
||
apply()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var selectable: Bool = false
|
||
}
|
||
|
||
class MessageStatsOverviewItemNode: ListViewItemNode {
|
||
private let backgroundNode: ASDisplayNode
|
||
private let topStripeNode: ASDisplayNode
|
||
private let bottomStripeNode: ASDisplayNode
|
||
private let maskNode: ASImageNode
|
||
|
||
private let leftValueLabel: ImmediateTextNode
|
||
private let centerValueLabel: ImmediateTextNode
|
||
private let rightValueLabel: ImmediateTextNode
|
||
|
||
private let leftTitleLabel: ImmediateTextNode
|
||
private let centerTitleLabel: ImmediateTextNode
|
||
private let rightTitleLabel: ImmediateTextNode
|
||
|
||
private var item: MessageStatsOverviewItem?
|
||
|
||
init() {
|
||
self.backgroundNode = ASDisplayNode()
|
||
self.backgroundNode.isLayerBacked = true
|
||
self.backgroundNode.backgroundColor = .white
|
||
|
||
self.topStripeNode = ASDisplayNode()
|
||
self.topStripeNode.isLayerBacked = true
|
||
|
||
self.bottomStripeNode = ASDisplayNode()
|
||
self.bottomStripeNode.isLayerBacked = true
|
||
|
||
self.maskNode = ASImageNode()
|
||
|
||
self.leftValueLabel = ImmediateTextNode()
|
||
self.centerValueLabel = ImmediateTextNode()
|
||
self.rightValueLabel = ImmediateTextNode()
|
||
|
||
self.leftTitleLabel = ImmediateTextNode()
|
||
self.centerTitleLabel = ImmediateTextNode()
|
||
self.rightTitleLabel = ImmediateTextNode()
|
||
|
||
super.init(layerBacked: false, dynamicBounce: false)
|
||
|
||
self.clipsToBounds = true
|
||
|
||
self.addSubnode(self.leftValueLabel)
|
||
self.addSubnode(self.centerValueLabel)
|
||
self.addSubnode(self.rightValueLabel)
|
||
|
||
self.addSubnode(self.leftTitleLabel)
|
||
self.addSubnode(self.centerTitleLabel)
|
||
self.addSubnode(self.rightTitleLabel)
|
||
}
|
||
|
||
func asyncLayout() -> (_ item: MessageStatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||
let makeLeftValueLabelLayout = TextNode.asyncLayout(self.leftValueLabel)
|
||
let makeRightValueLabelLayout = TextNode.asyncLayout(self.rightValueLabel)
|
||
let makeCenterValueLabelLayout = TextNode.asyncLayout(self.centerValueLabel)
|
||
|
||
let makeLeftTitleLabelLayout = TextNode.asyncLayout(self.leftTitleLabel)
|
||
let makeRightTitleLabelLayout = TextNode.asyncLayout(self.rightTitleLabel)
|
||
let makeCenterTitleLabelLayout = TextNode.asyncLayout(self.centerTitleLabel)
|
||
|
||
let currentItem = self.item
|
||
|
||
return { item, params, neighbors in
|
||
let insets: UIEdgeInsets
|
||
let separatorHeight = UIScreenPixel
|
||
let itemBackgroundColor: UIColor
|
||
let itemSeparatorColor: UIColor
|
||
|
||
let topInset: CGFloat = 14.0
|
||
let sideInset: CGFloat = 16.0
|
||
|
||
var height: CGFloat = topInset * 2.0
|
||
|
||
let leftInset = params.leftInset
|
||
let rightInset = params.rightInset
|
||
var updatedTheme: PresentationTheme?
|
||
|
||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||
updatedTheme = item.presentationData.theme
|
||
}
|
||
|
||
switch item.style {
|
||
case .plain:
|
||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||
insets = itemListNeighborsPlainInsets(neighbors)
|
||
case .blocks:
|
||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||
}
|
||
|
||
let valueFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
|
||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||
|
||
let leftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||
let rightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||
let centerValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||
|
||
let leftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||
let rightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||
let centerTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||
|
||
let centerTitle: String
|
||
let centerValue: String
|
||
if let _ = item.stats as? StoryStats {
|
||
centerTitle = "Reactions"
|
||
centerValue = compactNumericCountString(Int(item.reactions))
|
||
} else {
|
||
centerTitle = item.presentationData.strings.Stats_Message_PublicShares
|
||
centerValue = item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–"
|
||
}
|
||
|
||
leftValueLabelLayoutAndApply = makeLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(item.stats.views), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||
|
||
centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerValue, font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||
|
||
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||
|
||
var remainingWidth: CGFloat = params.width - leftInset - rightInset - sideInset * 2.0
|
||
let maxItemWidth: CGFloat = floor(remainingWidth / 2.8)
|
||
|
||
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||
remainingWidth -= leftTitleLabelLayoutAndApply!.0.size.width - 4.0
|
||
|
||
|
||
|
||
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerTitle, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||
remainingWidth -= centerTitleLabelLayoutAndApply!.0.size.width - 4.0
|
||
|
||
rightTitleLabelLayoutAndApply = makeRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_PrivateShares, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||
|
||
var maxLabelHeight = rightTitleLabelLayoutAndApply!.0.size.height
|
||
maxLabelHeight = max(maxLabelHeight, centerTitleLabelLayoutAndApply!.0.size.height)
|
||
maxLabelHeight = max(maxLabelHeight, leftTitleLabelLayoutAndApply!.0.size.height)
|
||
|
||
height += rightValueLabelLayoutAndApply!.0.size.height + maxLabelHeight
|
||
|
||
let contentSize = CGSize(width: params.width, height: height)
|
||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.item = item
|
||
|
||
let _ = leftValueLabelLayoutAndApply?.1()
|
||
let _ = centerValueLabelLayoutAndApply?.1()
|
||
let _ = rightValueLabelLayoutAndApply?.1()
|
||
let _ = leftTitleLabelLayoutAndApply?.1()
|
||
let _ = centerTitleLabelLayoutAndApply?.1()
|
||
let _ = rightTitleLabelLayoutAndApply?.1()
|
||
|
||
if let _ = updatedTheme {
|
||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||
}
|
||
|
||
switch item.style {
|
||
case .plain:
|
||
if strongSelf.backgroundNode.supernode != nil {
|
||
strongSelf.backgroundNode.removeFromSupernode()
|
||
}
|
||
if strongSelf.topStripeNode.supernode != nil {
|
||
strongSelf.topStripeNode.removeFromSupernode()
|
||
}
|
||
if strongSelf.bottomStripeNode.supernode == nil {
|
||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||
}
|
||
if strongSelf.maskNode.supernode != nil {
|
||
strongSelf.maskNode.removeFromSupernode()
|
||
}
|
||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||
case .blocks:
|
||
if strongSelf.backgroundNode.supernode == nil {
|
||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||
}
|
||
if strongSelf.topStripeNode.supernode == nil {
|
||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||
}
|
||
if strongSelf.bottomStripeNode.supernode == nil {
|
||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||
}
|
||
if strongSelf.maskNode.supernode == nil {
|
||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||
}
|
||
|
||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||
var hasTopCorners = false
|
||
var hasBottomCorners = false
|
||
switch neighbors.top {
|
||
case .sameSection(false):
|
||
strongSelf.topStripeNode.isHidden = true
|
||
default:
|
||
hasTopCorners = true
|
||
strongSelf.topStripeNode.isHidden = hasCorners
|
||
}
|
||
let bottomStripeInset: CGFloat
|
||
switch neighbors.bottom {
|
||
case .sameSection(false):
|
||
bottomStripeInset = leftInset
|
||
strongSelf.bottomStripeNode.isHidden = false
|
||
default:
|
||
bottomStripeInset = 0.0
|
||
hasBottomCorners = true
|
||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||
}
|
||
|
||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||
|
||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||
}
|
||
|
||
let maxLeftWidth = max(leftValueLabelLayoutAndApply?.0.size.width ?? 0.0, leftTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||
let maxCenterWidth = max(centerValueLabelLayoutAndApply?.0.size.width ?? 0.0, centerTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||
let maxRightWidth = max(rightValueLabelLayoutAndApply?.0.size.width ?? 0.0, rightTitleLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||
|
||
let horizontalSpacing = max(1.0, min(60, (params.width - leftInset - rightInset - sideInset * 2.0 - maxLeftWidth - maxCenterWidth - maxRightWidth) / 2.0))
|
||
|
||
var x: CGFloat = leftInset + (params.width - leftInset - rightInset - maxLeftWidth - maxCenterWidth - maxRightWidth - horizontalSpacing * 2.0) / 2.0
|
||
if let leftValueLabelLayout = leftValueLabelLayoutAndApply?.0, let leftTitleLabelLayout = leftTitleLabelLayoutAndApply?.0 {
|
||
strongSelf.leftValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: leftValueLabelLayout.size)
|
||
strongSelf.leftTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.leftValueLabel.frame.maxY), size: leftTitleLabelLayout.size)
|
||
x += max(leftValueLabelLayout.size.width, leftTitleLabelLayout.size.width) + horizontalSpacing
|
||
}
|
||
|
||
if let centerValueLabelLayout = centerValueLabelLayoutAndApply?.0, let centerTitleLabelLayout = centerTitleLabelLayoutAndApply?.0 {
|
||
strongSelf.centerValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: centerValueLabelLayout.size)
|
||
strongSelf.centerTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.centerValueLabel.frame.maxY), size: centerTitleLabelLayout.size)
|
||
x += max(centerValueLabelLayout.size.width, centerTitleLabelLayout.size.width) + horizontalSpacing
|
||
}
|
||
|
||
if let rightValueLabelLayout = rightValueLabelLayoutAndApply?.0, let rightTitleLabelLayout = rightTitleLabelLayoutAndApply?.0 {
|
||
strongSelf.rightValueLabel.frame = CGRect(origin: CGPoint(x: x, y: topInset), size: rightValueLabelLayout.size)
|
||
strongSelf.rightTitleLabel.frame = CGRect(origin: CGPoint(x: x, y: strongSelf.rightValueLabel.frame.maxY), size: rightTitleLabelLayout.size)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||
}
|
||
|
||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||
}
|
||
|
||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||
}
|
||
}
|
||
|