Statistics improvements

This commit is contained in:
Ilya Laktyushin
2023-11-17 07:22:25 +04:00
parent 1fced74098
commit 13baadc3e7
29 changed files with 1467 additions and 119 deletions

View File

@@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
@@ -11,23 +12,28 @@ import TelegramStringFormatting
import ItemListUI
import PresentationDataUtils
import PhotoResources
import AvatarStoryIndicatorComponent
public class StatsMessageItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let message: Message
let peer: Peer
let item: StatsPostItem
let views: Int32
let reactions: Int32
let forwards: Int32
public let sectionId: ItemListSectionId
let style: ItemListStyle
let action: (() -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init(context: AccountContext, presentationData: ItemListPresentationData, message: Message, views: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
init(context: AccountContext, presentationData: ItemListPresentationData, peer: Peer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
self.context = context
self.presentationData = presentationData
self.message = message
self.peer = peer
self.item = item
self.views = views
self.reactions = reactions
self.forwards = forwards
self.sectionId = sectionId
self.style = style
@@ -79,7 +85,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem {
private let badgeFont = Font.regular(15.0)
public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@@ -96,9 +102,14 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
private var nonExtractedRect: CGRect?
let contentImageNode: TransformImageNode
var storyIndicator: ComponentView<Empty>?
let titleNode: TextNode
let labelNode: TextNode
let viewsNode: TextNode
let reactionsIconNode: ASImageNode
let reactionsNode: TextNode
let forwardsIconNode: ASImageNode
let forwardsNode: TextNode
private let activateArea: AccessibilityAreaNode
@@ -152,6 +163,15 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
self.forwardsNode = TextNode()
self.forwardsNode.isUserInteractionEnabled = false
self.forwardsIconNode = ASImageNode()
self.forwardsIconNode.displaysAsynchronously = false
self.reactionsNode = TextNode()
self.reactionsNode.isUserInteractionEnabled = false
self.reactionsIconNode = ASImageNode()
self.reactionsIconNode.displaysAsynchronously = false
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
@@ -172,6 +192,9 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
self.offsetContainerNode.addSubnode(self.labelNode)
self.countersContainerNode.addSubnode(self.viewsNode)
self.countersContainerNode.addSubnode(self.forwardsNode)
self.countersContainerNode.addSubnode(self.forwardsIconNode)
self.countersContainerNode.addSubnode(self.reactionsNode)
self.countersContainerNode.addSubnode(self.reactionsIconNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.activateArea)
@@ -200,8 +223,8 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
transition.updateAlpha(node: strongSelf.countersContainerNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateSublayerTransformOffset(layer: strongSelf.countersContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.countersContainerNode.layer, offset: CGPoint(x: isExtracted ? -16.0 : 0.0, y: 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 16.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
@@ -215,6 +238,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeViewsLayout = TextNode.asyncLayout(self.viewsNode)
let makeReactionsLayout = TextNode.asyncLayout(self.reactionsNode)
let makeForwardsLayout = TextNode.asyncLayout(self.forwardsNode)
let currentItem = self.item
@@ -236,67 +260,100 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
var totalLeftInset = leftInset
let additionalRightInset: CGFloat = 128.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(item.message), strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
var text = !item.message.text.isEmpty ? item.message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0.string
text = foldLineBreaks(text)
var text: String
var contentImageMedia: Media?
for media in item.message.media {
if let image = media as? TelegramMediaImage {
contentImageMedia = image
break
} else if let file = media as? TelegramMediaFile {
if file.isVideo && !file.isInstantVideo {
contentImageMedia = file
break
}
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let image = content.image {
let timestamp: Int32
switch item.item {
case let .message(message):
let contentKind: MessageContentKind
contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: .firstLast, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: item.context.account.peerId)
text = !message.text.isEmpty ? message.text : stringForMediaKind(contentKind, strings: item.presentationData.strings).0.string
for media in message.media {
if let image = media as? TelegramMediaImage {
contentImageMedia = image
break
} else if let file = content.file {
} else if let file = media as? TelegramMediaFile {
if file.isVideo && !file.isInstantVideo {
contentImageMedia = file
break
}
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let image = content.image {
contentImageMedia = image
break
} else if let file = content.file {
if file.isVideo && !file.isInstantVideo {
contentImageMedia = file
break
}
}
}
}
timestamp = message.timestamp
case let .story(story):
text = item.presentationData.strings.Message_Story
timestamp = story.timestamp
if let image = story.media._asMedia() as? TelegramMediaImage {
contentImageMedia = image
break
} else if let file = story.media._asMedia() as? TelegramMediaFile {
contentImageMedia = file
break
}
}
text = foldLineBreaks(text)
if let _ = contentImageMedia {
totalLeftInset += 48.0
totalLeftInset += 46.0
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
if let contentImageMedia = contentImageMedia {
if let currentContentImageMedia = currentContentImageMedia, contentImageMedia.isSemanticallyEqual(to: currentContentImageMedia) {
} else {
if let image = contentImageMedia as? TelegramMediaImage {
updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image))
} else if let file = contentImageMedia as? TelegramMediaFile {
updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: file), autoFetchFullSizeThumbnail: true)
switch item.item {
case let .message(message):
if let image = contentImageMedia as? TelegramMediaImage {
updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image))
} else if let file = contentImageMedia as? TelegramMediaFile {
updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true)
}
case let .story(story):
if let peerReference = PeerReference(item.peer) {
if let image = contentImageMedia as? TelegramMediaImage {
updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(item.peer.id), photoReference: .story(peer: peerReference, id: story.id, media: image))
} else if let file = contentImageMedia as? TelegramMediaFile {
updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(item.peer.id), videoReference: .story(peer: peerReference, id: story.id, media: file), autoFetchFullSizeThumbnail: true)
}
}
}
}
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
let label = stringForMediumDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (viewsLayout, viewsApply) = makeViewsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageViews(item.views), font: labelFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let (forwardsLayout, forwardsApply) = makeForwardsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_MessageForwards(item.forwards), font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let reactions = item.reactions > 0 ? compactNumericCountString(Int(item.reactions), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) : ""
let (reactionsLayout, reactionsApply) = makeReactionsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: reactions, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 11.0
let forwards = item.forwards > 0 ? compactNumericCountString(Int(item.forwards), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) : ""
let (forwardsLayout, forwardsApply) = makeForwardsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: forwards, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 128.0, height: CGFloat.greatestFiniteMagnitude), alignment: .right, cutout: nil, insets: UIEdgeInsets()))
let additionalRightInset = max(viewsLayout.size.width, reactionsLayout.size.width + forwardsLayout.size.width + 36.0) + 8.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let label = stringForMediumDate(timestamp: timestamp, strings: item.presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: label, font: labelFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - totalLeftInset - rightInset - additionalRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 10.0
let titleSpacing: CGFloat = 3.0
let height: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
@@ -318,8 +375,14 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
let themeUpdated = strongSelf.item?.presentationData.theme !== item.presentationData.theme
strongSelf.item = item
if themeUpdated {
strongSelf.forwardsIconNode.image = PresentationResourcesItemList.statsForwardsIcon(item.presentationData.theme)
strongSelf.reactionsIconNode.image = PresentationResourcesItemList.statsReactionsIcon(item.presentationData.theme)
}
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
strongSelf.extractedRect = extractedRect
@@ -357,13 +420,21 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
dimensions = contentImageMedia.dimensions?.cgSize
}
let contentImageSize = CGSize(width: 40.0, height: 40.0)
var contentImageSize = CGSize(width: 40.0, height: 40.0)
var contentImageInset = leftInset - 6.0
if let dimensions = dimensions {
let makeImageLayout = strongSelf.contentImageNode.asyncLayout()
let imageSize = contentImageSize
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: dimensions.aspectFilled(imageSize), boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
let cornerRadius: CGFloat
if case .story = item.item {
contentImageInset += 3.0
contentImageSize = CGSize(width: 34.0, height: 34.0)
cornerRadius = contentImageSize.width / 2.0
} else {
cornerRadius = 6.0
}
let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: cornerRadius), imageSize: dimensions.aspectFilled(contentImageSize), boundingSize: contentImageSize, intrinsicInsets: UIEdgeInsets()))
applyImageLayout()
if let updateImageSignal = updateImageSignal {
@@ -384,6 +455,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let _ = labelApply()
let _ = viewsApply()
let _ = forwardsApply()
let _ = reactionsApply()
switch item.style {
case .plain:
@@ -427,7 +499,7 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
bottomStripeInset = totalLeftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
@@ -443,22 +515,106 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
let contentImageFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - contentImageSize.height) / 2.0)), size: contentImageSize)
let contentImageFrame = CGRect(origin: CGPoint(x: contentImageInset, y: floorToScreenPixels((height - contentImageSize.height) / 2.0)), size: contentImageSize)
strongSelf.contentImageNode.frame = contentImageFrame
let titleFrame = CGRect(origin: CGPoint(x: totalLeftInset, y: 11.0), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: totalLeftInset, y: 9.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let labelFrame = CGRect(origin: CGPoint(x: totalLeftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
let viewsFrame = CGRect(origin: CGPoint(x: params.width - rightInset - viewsLayout.size.width, y: 15.0), size: viewsLayout.size)
let viewsFrame = CGRect(origin: CGPoint(x: params.width - rightInset - viewsLayout.size.width, y: 13.0), size: viewsLayout.size)
strongSelf.viewsNode.frame = viewsFrame
let forwardsFrame = CGRect(origin: CGPoint(x: params.width - rightInset - forwardsLayout.size.width, y: titleFrame.maxY + titleSpacing), size: forwardsLayout.size)
strongSelf.forwardsNode.frame = forwardsFrame
let iconSpacing: CGFloat = 3.0 - UIScreenPixel
var rightContentInset: CGFloat = rightInset
if forwardsLayout.size.width > 0.0 {
strongSelf.forwardsIconNode.isHidden = false
strongSelf.forwardsNode.isHidden = false
let forwardsFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - forwardsLayout.size.width, y: titleFrame.maxY + titleSpacing), size: forwardsLayout.size)
strongSelf.forwardsNode.frame = forwardsFrame
if let icon = strongSelf.forwardsIconNode.image {
let forwardsIconFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - forwardsLayout.size.width - icon.size.width - iconSpacing, y: titleFrame.maxY + titleSpacing - 2.0 + UIScreenPixel), size: icon.size)
strongSelf.forwardsIconNode.frame = forwardsIconFrame
rightContentInset += forwardsIconFrame.width + forwardsFrame.width + iconSpacing
}
rightContentInset += 10.0
} else {
strongSelf.forwardsIconNode.isHidden = true
strongSelf.forwardsNode.isHidden = true
}
if reactionsLayout.size.width > 0.0 {
strongSelf.reactionsIconNode.isHidden = false
strongSelf.reactionsNode.isHidden = false
let reactionsFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - reactionsLayout.size.width, y: titleFrame.maxY + titleSpacing), size: reactionsLayout.size)
strongSelf.reactionsNode.frame = reactionsFrame
if let icon = strongSelf.reactionsIconNode.image {
let reactionsIconFrame = CGRect(origin: CGPoint(x: params.width - rightContentInset - reactionsLayout.size.width - icon.size.width - iconSpacing, y: titleFrame.maxY + titleSpacing - 2.0 + UIScreenPixel), size: icon.size)
strongSelf.reactionsIconNode.frame = reactionsIconFrame
rightContentInset += reactionsIconFrame.width + reactionsFrame.width + iconSpacing
}
} else {
strongSelf.reactionsIconNode.isHidden = true
strongSelf.reactionsNode.isHidden = true
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
if case .story = item.item {
let lineWidth: CGFloat = 1.5
let imageSize = CGSize(width: contentImageFrame.width + 6.0, height: contentImageFrame.height + 6.0)
let indicatorSize = CGSize(width: imageSize.width - lineWidth * 4.0, height: imageSize.height - lineWidth * 4.0)
let storyIndicator: ComponentView<Empty>
let indicatorTransition: Transition = .immediate
if let current = strongSelf.storyIndicator {
storyIndicator = current
} else {
storyIndicator = ComponentView()
strongSelf.storyIndicator = storyIndicator
}
let _ = storyIndicator.update(
transition: indicatorTransition,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: true,
hasUnseenCloseFriendsItems: false,
colors: AvatarStoryIndicatorComponent.Colors(
unseenColors: item.presentationData.theme.chatList.storyUnseenColors.array,
unseenCloseFriendsColors: item.presentationData.theme.chatList.storyUnseenPrivateColors.array,
seenColors: item.presentationData.theme.chatList.storySeenColors.array
),
activeLineWidth: lineWidth,
inactiveLineWidth: lineWidth,
counters: AvatarStoryIndicatorComponent.Counters(
totalCount: 1,
unseenCount: 1
),
progress: nil
)),
environment: {},
containerSize: indicatorSize
)
if let storyIndicatorView = storyIndicator.view {
if storyIndicatorView.superview == nil {
strongSelf.offsetContainerNode.view.addSubview(storyIndicatorView)
}
indicatorTransition.setFrame(view: storyIndicatorView, frame: CGRect(origin: CGPoint(x: contentImageFrame.midX - indicatorSize.width / 2.0, y: contentImageFrame.midY - indicatorSize.height / 2.0), size: indicatorSize))
}
} else if let storyIndicator = strongSelf.storyIndicator {
if let storyIndicatorView = storyIndicator.view {
storyIndicatorView.removeFromSuperview()
}
strongSelf.storyIndicator = nil
}
}
})
}