Swiftgram/submodules/StatisticsUI/Sources/StatsMessageItem.swift
2024-06-12 23:04:04 +04:00

769 lines
42 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramStringFormatting
import ItemListUI
import PresentationDataUtils
import PhotoResources
import AvatarStoryIndicatorComponent
import AvatarNode
public class StatsMessageItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let peer: Peer
let item: StatsPostItem
let views: Int32
let reactions: Int32
let forwards: Int32
let isPeer: Bool
public let sectionId: ItemListSectionId
let style: ItemListStyle
let action: (() -> Void)?
let openStory: (UIView) -> Void
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init(context: AccountContext, presentationData: ItemListPresentationData, peer: Peer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, isPeer: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, openStory: @escaping (UIView) -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
self.context = context
self.presentationData = presentationData
self.peer = peer
self.item = item
self.views = views
self.reactions = reactions
self.forwards = forwards
self.isPeer = isPeer
self.sectionId = sectionId
self.style = style
self.action = action
self.openStory = openStory
self.contextAction = contextAction
}
public 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 = StatsMessageItemNode()
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() })
})
}
}
}
public 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? StatsMessageItemNode {
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()
})
}
}
}
}
}
public var selectable: Bool = true
public func selected(listView: ListView){
listView.clearHighlightAnimated(true)
}
}
private let badgeFont = Font.regular(15.0)
final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundImageNode: ASImageNode
private let offsetContainerNode: ASDisplayNode
private let countersContainerNode: ASDisplayNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
var avatarNode: AvatarNode?
let contentImageNode: TransformImageNode
var storyIndicator: ComponentView<Empty>?
var storyButton: HighlightTrackingButton?
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
private var item: StatsMessageItem?
private var contentImageMedia: Media?
override public var canBeSelected: Bool {
return true
}
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.contentImageNode = TransformImageNode()
self.contentImageNode.isLayerBacked = false
self.offsetContainerNode = ASDisplayNode()
self.countersContainerNode = ASDisplayNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.viewsNode = TextNode()
self.viewsNode.isUserInteractionEnabled = false
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
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.contextSourceNode.contentNode.addSubnode(self.countersContainerNode)
self.offsetContainerNode.addSubnode(self.contentImageNode)
self.offsetContainerNode.addSubnode(self.titleNode)
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)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateAlpha(node: strongSelf.countersContainerNode, alpha: isExtracted ? 0.0 : 1.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 {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result
}
override func selected() {
guard let item = self.item else {
return
}
if item.isPeer {
if case .story = item.item {
self.storyPressed()
} else {
item.action?()
}
} else {
item.action?()
}
}
@objc private func storyPressed() {
guard let item = self.item else {
return
}
if let avatarNode = self.avatarNode {
item.openStory(avatarNode.view)
} else {
item.openStory(self.contentImageNode.view)
}
}
public func asyncLayout() -> (_ item: StatsMessageItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
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
let currentContentImageMedia = self.contentImageMedia
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
var totalLeftInset = leftInset
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 }
var text: String
var contentImageMedia: Media?
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 = 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
}
}
if item.isPeer {
text = EnginePeer(item.peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
} else {
text = foldLineBreaks(text)
}
if contentImageMedia != nil || item.isPeer {
totalLeftInset += 46.0
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
if let contentImageMedia = contentImageMedia {
if let currentContentImageMedia = currentContentImageMedia, contentImageMedia.isSemanticallyEqual(to: currentContentImageMedia) {
} else {
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 viewsString: String
if item.views == 0 {
viewsString = item.presentationData.strings.Stats_MessageViews_NoViews
} else {
viewsString = item.presentationData.strings.Stats_MessageViews(item.views)
}
let (viewsLayout, viewsApply) = makeViewsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewsString, 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 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 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
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
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
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.contextSourceNode.isExtractedToContextPreview {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.countersContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.containerNode.isGestureEnabled = item.contextAction != nil
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = text
strongSelf.activateArea.accessibilityValue = label
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
var contentImageSize = CGSize(width: 40.0, height: 40.0)
var contentImageInset = leftInset - 6.0
var dimensions: CGSize?
if item.isPeer {
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)))
strongSelf.offsetContainerNode.addSubnode(avatarNode)
strongSelf.avatarNode = avatarNode
}
avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(item.peer))
if case .story = item.item {
contentImageInset += 3.0
contentImageSize = CGSize(width: 34.0, height: 34.0)
}
} else {
strongSelf.avatarNode?.removeFromSupernode()
strongSelf.avatarNode = nil
if let contentImageMedia = contentImageMedia as? TelegramMediaImage {
dimensions = largestRepresentationForPhoto(contentImageMedia)?.dimensions.cgSize
} else if let contentImageMedia = contentImageMedia as? TelegramMediaFile {
dimensions = contentImageMedia.dimensions?.cgSize
}
}
if let dimensions = dimensions {
let makeImageLayout = strongSelf.contentImageNode.asyncLayout()
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 {
strongSelf.contentImageNode.setSignal(updateImageSignal)
if currentContentImageMedia == nil {
strongSelf.contentImageNode.isHidden = false
}
}
} else {
if currentContentImageMedia != nil {
strongSelf.contentImageNode.removeFromSupernode()
strongSelf.contentImageNode.setSignal(.single({ _ in nil }))
strongSelf.contentImageNode.isHidden = true
}
}
let _ = titleApply()
let _ = labelApply()
let _ = viewsApply()
let _ = forwardsApply()
let _ = reactionsApply()
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 = totalLeftInset
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 contentImageFrame = CGRect(origin: CGPoint(x: contentImageInset, y: floorToScreenPixels((height - contentImageSize.height) / 2.0)), size: contentImageSize)
strongSelf.contentImageNode.frame = contentImageFrame
if let avatarNode = strongSelf.avatarNode {
avatarNode.frame = contentImageFrame
}
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 viewsOriginY: CGFloat = forwardsLayout.size.width > 0.0 || reactionsLayout.size.width > 0.0 ? 13.0 : floorToScreenPixels((contentSize.height - viewsLayout.size.height) / 2.0)
let viewsFrame = CGRect(origin: CGPoint(x: params.width - rightInset - viewsLayout.size.width, y: viewsOriginY), size: viewsLayout.size)
strongSelf.viewsNode.frame = viewsFrame
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: ComponentTransition = .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
)
let storyIndicatorFrame = CGRect(origin: CGPoint(x: contentImageFrame.midX - indicatorSize.width / 2.0, y: contentImageFrame.midY - indicatorSize.height / 2.0), size: indicatorSize)
if let storyIndicatorView = storyIndicator.view {
if storyIndicatorView.superview == nil {
strongSelf.offsetContainerNode.view.addSubview(storyIndicatorView)
}
indicatorTransition.setFrame(view: storyIndicatorView, frame: storyIndicatorFrame)
}
let storyButton: HighlightTrackingButton
if let current = strongSelf.storyButton {
storyButton = current
} else {
storyButton = HighlightTrackingButton()
storyButton.addTarget(strongSelf, action: #selector(strongSelf.storyPressed), for: .touchUpInside)
strongSelf.view.addSubview(storyButton)
strongSelf.storyButton = storyButton
}
storyButton.frame = storyIndicatorFrame
} else if let storyIndicator = strongSelf.storyIndicator {
if let storyIndicatorView = storyIndicator.view {
storyIndicatorView.removeFromSuperview()
}
strongSelf.storyIndicator = nil
if let storyButton = strongSelf.storyButton {
storyButton.removeFromSuperview()
strongSelf.storyButton = nil
}
}
}
})
}
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
var highlighted = highlighted
if let avatarButton = self.storyButton, avatarButton.bounds.contains(self.view.convert(point, to: storyButton)) {
highlighted = false
}
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}