2024-01-28 23:48:48 +04:00

1095 lines
62 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AvatarNode
import AccountContext
import PhoneNumberFormat
import TelegramStringFormatting
import Markdown
import ShimmerEffect
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageAttachedContentButtonNode
import ChatControllerInteraction
private let titleFont = Font.medium(15.0)
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, UIGestureRecognizerDelegate {
private let dateAndStatusNode: ChatMessageDateAndStatusNode
private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode
private let prizeTitleNode: TextNode
private let prizeTextNode: TextNode
private let additionalPrizeSeparatorNode: TextNode
private let additionalPrizeTextNode: TextNode
private let additionalPrizeLeftLine: ASDisplayNode
private let additionalPrizeRightLine: ASDisplayNode
private let participantsTitleNode: TextNode
private let participantsTextNode: TextNode
private let countriesTextNode: TextNode
private let dateTitleNode: TextNode
private let dateTextNode: TextNode
private let badgeBackgroundNode: ASImageNode
private let badgeTextNode: TextNode
private var giveaway: TelegramMediaGiveaway?
private let buttonNode: ChatMessageAttachedContentButtonNode
private let channelButtons: PeerButtonsStackNode
override public var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = oldValue != .none
let isVisible = self.visibility != .none
if wasVisible != isVisible {
self.visibilityStatus = isVisible
}
}
}
private var visibilityStatus: Bool? {
didSet {
if self.visibilityStatus != oldValue {
self.updateVisibility()
}
}
}
private var currentProgressDisposable: Disposable?
required public init() {
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.alpha = 0.75
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
self.prizeTitleNode = TextNode()
self.prizeTextNode = TextNode()
self.additionalPrizeSeparatorNode = TextNode()
self.additionalPrizeTextNode = TextNode()
self.additionalPrizeLeftLine = ASDisplayNode()
self.additionalPrizeRightLine = ASDisplayNode()
self.participantsTitleNode = TextNode()
self.participantsTextNode = TextNode()
self.countriesTextNode = TextNode()
self.dateTitleNode = TextNode()
self.dateTextNode = TextNode()
self.badgeBackgroundNode = ASImageNode()
self.badgeBackgroundNode.displaysAsynchronously = false
self.badgeTextNode = TextNode()
self.buttonNode = ChatMessageAttachedContentButtonNode()
self.channelButtons = PeerButtonsStackNode()
super.init()
self.addSubnode(self.prizeTitleNode)
self.addSubnode(self.prizeTextNode)
self.addSubnode(self.additionalPrizeSeparatorNode)
self.addSubnode(self.additionalPrizeTextNode)
self.addSubnode(self.additionalPrizeLeftLine)
self.addSubnode(self.additionalPrizeRightLine)
self.addSubnode(self.participantsTitleNode)
self.addSubnode(self.participantsTextNode)
self.addSubnode(self.countriesTextNode)
self.addSubnode(self.dateTitleNode)
self.addSubnode(self.dateTextNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.channelButtons)
self.addSubnode(self.animationNode)
self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTextNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.dateAndStatusNode.reactionSelected = { [weak self] _, value in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceView, value in
guard let strongSelf = self, let item = strongSelf.item else {
gesture?.cancel()
return
}
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceView, gesture, value)
}
self.channelButtons.openPeer = { [weak self] peer in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
if case .user = peer {
item.controllerInteraction.openPeer(peer, .info(nil), nil, .default)
} else {
item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
}
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.currentProgressDisposable?.dispose()
}
override public func didLoad() {
super.didLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.bubbleTap(_:)))
tapRecognizer.delegate = self
self.view.addGestureRecognizer(tapRecognizer)
}
override public func accessibilityActivate() -> Bool {
self.buttonPressed()
return true
}
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let point = gestureRecognizer.location(in: self.view)
if case .ignore = self.tapActionAtPoint(point, gesture: .tap, isEstimating: false).content {
return false
}
return self.bounds.contains(point)
}
@objc private func bubbleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let item = self.item else {
return
}
item.controllerInteraction.displayGiveawayParticipationStatus(item.message.id)
}
private func removePlaceholder(animated: Bool) {
self.placeholderNode.alpha = 0.0
if !animated {
self.placeholderNode.removeFromSupernode()
} else {
self.placeholderNode.layer.animateAlpha(from: self.placeholderNode.alpha, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.placeholderNode.removeFromSupernode()
})
}
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let statusLayout = self.dateAndStatusNode.asyncLayout()
let makePrizeTitleLayout = TextNode.asyncLayout(self.prizeTitleNode)
let makePrizeTextLayout = TextNode.asyncLayout(self.prizeTextNode)
let makeAdditionalPrizeSeparatorLayout = TextNode.asyncLayout(self.additionalPrizeSeparatorNode)
let makeAdditionalPrizeTextLayout = TextNode.asyncLayout(self.additionalPrizeTextNode)
let makeParticipantsTitleLayout = TextNode.asyncLayout(self.participantsTitleNode)
let makeParticipantsTextLayout = TextNode.asyncLayout(self.participantsTextNode)
let makeCountriesTextLayout = TextNode.asyncLayout(self.countriesTextNode)
let makeDateTitleLayout = TextNode.asyncLayout(self.dateTitleNode)
let makeDateTextLayout = TextNode.asyncLayout(self.dateTextNode)
let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode)
let makeChannelsLayout = PeerButtonsStackNode.asyncLayout(self.channelButtons)
let currentItem = self.item
return { item, layoutConstants, _, _, constrainedSize, _ in
var giveaway: TelegramMediaGiveaway?
var giveawayResults: TelegramMediaGiveawayResults?
for media in item.message.media {
if let media = media as? TelegramMediaGiveaway {
giveaway = media;
} else if let media = media as? TelegramMediaGiveawayResults {
giveawayResults = media
}
}
var themeUpdated = false
if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme {
themeUpdated = true
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}
let backgroundColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first!
let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
let secondaryTextColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
let lineColor = secondaryTextColor.withMultipliedAlpha(0.2)
let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
var badgeTextColor: UIColor = .white
if badgeTextColor.distance(to: accentColor) < 1 {
badgeTextColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first!
}
var updatedBadgeImage: UIImage?
if themeUpdated {
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)
}
let badgeText: String
if let giveaway {
badgeText = "X\(giveaway.quantity)"
} else if let giveawayResults {
badgeText = "X\(giveawayResults.winnersCount)"
} else {
badgeText = ""
}
let badgeString = NSAttributedString(string: badgeText, font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor)
let prizeTitleText: String
if let giveawayResults {
if giveawayResults.winnersCount > 1 {
prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_Many
} else {
prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_One
}
} else {
prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle
}
let prizeTitleString = NSAttributedString(string: prizeTitleText, font: titleFont, textColor: textColor)
var prizeTextString: NSAttributedString?
var additionalPrizeSeparatorString: NSAttributedString?
var additionalPrizeTextString: NSAttributedString?
if let giveaway {
var prizeDescription: String?
if let description = giveaway.prizeDescription {
prizeDescription = description
}
var trimSubscriptionCount = false
if let prizeDescription {
additionalPrizeSeparatorString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_With, font: textFont, textColor: secondaryTextColor)
let quantityString = item.presentationData.strings.Chat_Giveaway_Message_CustomPrizeQuantity(giveaway.quantity)
additionalPrizeTextString = parseMarkdownIntoAttributedString("**\(quantityString)** \(prizeDescription)", attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
trimSubscriptionCount = true
}
var subscriptionsString = item.presentationData.strings.Chat_Giveaway_Message_Subscriptions(giveaway.quantity)
if trimSubscriptionCount {
subscriptionsString = item.presentationData.strings.Chat_Giveaway_Message_WithSubscriptions(giveaway.quantity)
subscriptionsString = subscriptionsString.replacingOccurrences(of: "**\(giveaway.quantity)** ", with: "")
}
prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_PrizeText(
subscriptionsString,
item.presentationData.strings.Chat_Giveaway_Message_Months(giveaway.months)
).string, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
} else if let giveawayResults {
prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedText(giveawayResults.winnersCount), attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
}
var showWinners = false
if let giveawayResults, !giveawayResults.winnersPeerIds.isEmpty {
showWinners = true
}
let participantsTitleText: String
if let giveawayResults {
if showWinners {
if giveawayResults.winnersCount > 1 {
participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_Many
} else {
participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_One
}
} else {
participantsTitleText = ""
}
} else {
participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle
}
let participantsTitleString = NSAttributedString(string: participantsTitleText, font: titleFont, textColor: textColor)
let participantsText: String
let countriesText: String
if let giveaway {
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNewMany
} else {
participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNew
}
} else {
if giveaway.channelPeerIds.count > 1 {
participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsMany
} else {
participantsText = item.presentationData.strings.Chat_Giveaway_Message_Participants
}
}
if !giveaway.countries.isEmpty {
let locale = localeWithStrings(item.presentationData.strings)
let countryNames = giveaway.countries.map { id in
if let countryName = locale.localizedString(forRegionCode: id) {
return "\(flagEmoji(countryCode: id))\u{feff}\(countryName)"
} else {
return id
}
}
var countries: String = ""
if countryNames.count == 1, let country = countryNames.first {
countries = country
} else {
for i in 0 ..< countryNames.count {
countries.append(countryNames[i])
if i == countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Giveaway_Message_CountriesLastDelimiter)
} else if i < countryNames.count - 2 {
countries.append(item.presentationData.strings.Chat_Giveaway_Message_CountriesDelimiter)
}
}
}
countriesText = item.presentationData.strings.Chat_Giveaway_Message_CountriesFrom(countries).string
} else {
countriesText = ""
}
} else {
participantsText = ""
countriesText = ""
}
let participantsTextString = NSAttributedString(string: participantsText, font: textFont, textColor: textColor)
let countriesTextString = NSAttributedString(string: countriesText, font: textFont, textColor: textColor)
var dateTitleText = item.presentationData.strings.Chat_Giveaway_Message_DateTitle
if let giveawayResults {
if giveawayResults.winnersCount > giveawayResults.winnersPeerIds.count {
let moreCount = giveawayResults.winnersCount - Int32(giveawayResults.winnersPeerIds.count)
dateTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersMore(moreCount)
} else {
dateTitleText = ""
}
}
let dateTitleString = NSAttributedString(string: dateTitleText, font: titleFont, textColor: textColor)
var dateTextString: NSAttributedString?
if let giveaway {
dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor)
} else if let giveawayResults {
dateTextString = NSAttributedString(string: giveawayResults.winnersCount > 1 ? item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_Many : item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_One, font: textFont, textColor: textColor)
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none, hidesHeaders: true)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let sideInsets = layoutConstants.text.bubbleInsets.right * 2.0
let maxTextWidth = min(200.0, max(1.0, constrainedSize.width - 7.0 - sideInsets))
let (badgeTextLayout, badgeTextApply) = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badgeString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (prizeTitleLayout, prizeTitleApply) = makePrizeTitleLayout(TextNodeLayoutArguments(attributedString: prizeTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (additionalPrizeTextLayout, additionalPrizeTextApply) = makeAdditionalPrizeTextLayout(TextNodeLayoutArguments(attributedString: additionalPrizeTextString, backgroundColor: nil, maximumNumberOfLines: 6, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (additionalPrizeSeparatorLayout, additionalPrizeSeparatorApply) = makeAdditionalPrizeSeparatorLayout(TextNodeLayoutArguments(attributedString: additionalPrizeSeparatorString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (prizeTextLayout, prizeTextApply) = makePrizeTextLayout(TextNodeLayoutArguments(attributedString: prizeTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (participantsTitleLayout, participantsTitleApply) = makeParticipantsTitleLayout(TextNodeLayoutArguments(attributedString: participantsTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (participantsTextLayout, participantsTextApply) = makeParticipantsTextLayout(TextNodeLayoutArguments(attributedString: participantsTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (countriesTextLayout, countriesTextApply) = makeCountriesTextLayout(TextNodeLayoutArguments(attributedString: countriesTextString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dateTitleLayout, dateTitleApply) = makeDateTitleLayout(TextNodeLayoutArguments(attributedString: dateTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dateTextLayout, dateTextApply) = makeDateTextLayout(TextNodeLayoutArguments(attributedString: dateTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var edited = false
if item.attributes.updatingMedia != nil {
edited = true
}
var viewCount: Int?
var dateReplies = 0
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
dateReactionsAndPeers = ([], [])
}
for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden
} else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
dateReplies = Int(attribute.count)
}
}
}
let dateFormat: MessageTimestampStatusFormat
if item.presentationData.isPreview {
dateFormat = .full
} else {
dateFormat = .regular
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType {
var isReplyThread = false
if case .replyThread = item.chatLocation {
isReplyThread = true
}
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
context: item.context,
presentationData: item.presentationData,
edited: edited,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions,
savedMessageTags: item.associatedData.savedMessageTags,
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
areReactionsTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId),
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
}
let titleColor: UIColor
if incoming {
titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor
} else {
titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, nil, false, item.presentationData.strings.Chat_Giveaway_Message_LearnMore.uppercased(), titleColor, false, true)
let animationName: String
let months = giveaway?.months ?? 0
if let _ = giveaway {
switch months {
case 12:
animationName = "Gift12"
case 6:
animationName = "Gift6"
case 3:
animationName = "Gift3"
default:
animationName = "Gift3"
}
} else {
animationName = "Celebrate"
}
var maxContentWidth: CGFloat = 0.0
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
maxContentWidth = max(maxContentWidth, statusSuggestedWidthAndContinue.0)
}
maxContentWidth = max(maxContentWidth, prizeTitleLayout.size.width)
maxContentWidth = max(maxContentWidth, prizeTextLayout.size.width)
maxContentWidth = max(maxContentWidth, additionalPrizeSeparatorLayout.size.width)
maxContentWidth = max(maxContentWidth, additionalPrizeTextLayout.size.width)
maxContentWidth = max(maxContentWidth, participantsTitleLayout.size.width)
maxContentWidth = max(maxContentWidth, participantsTextLayout.size.width)
maxContentWidth = max(maxContentWidth, dateTitleLayout.size.width)
maxContentWidth = max(maxContentWidth, dateTextLayout.size.width)
maxContentWidth = max(maxContentWidth, buttonWidth)
var chipPeers: [EnginePeer] = []
if let channelPeerIds = giveaway?.channelPeerIds {
for peerId in channelPeerIds {
if let peer = item.message.peers[peerId] {
chipPeers.append(EnginePeer(peer))
}
}
} else if let winnerPeerIds = giveawayResults?.winnersPeerIds {
for peerId in winnerPeerIds {
if let peer = item.message.peers[peerId] {
chipPeers.append(EnginePeer(peer))
}
}
}
let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 220.0, chipPeers, accentColor, accentColor.withAlphaComponent(0.1), incoming, item.presentationData.theme.theme.overallDarkAppearance)
maxContentWidth = max(maxContentWidth, channelsWidth)
maxContentWidth += 30.0
let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0
return (contentWidth, { boundingWidth in
let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, 33.0)
let buttonSpacing: CGFloat = 4.0
let (channelButtonsSize, channelButtonsApply) = continueChannelLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, !item.presentationData.isPreview)
let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets)
let smallSpacing: CGFloat = 2.0
let largeSpacing: CGFloat = 14.0
var layoutSize = CGSize(width: boundingWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTextLayout.size.height + dateTextLayout.size.height + 99.0)
if !item.presentationData.isPreview {
layoutSize.height += buttonSize.height + buttonSpacing + 7.0
}
if additionalPrizeTextLayout.size.height > 0.0 {
layoutSize.height += additionalPrizeSeparatorLayout.size.height + additionalPrizeTextLayout.size.height + 7.0
}
if countriesTextLayout.size.height > 0.0 {
layoutSize.height += countriesTextLayout.size.height + 7.0
}
if dateTitleLayout.size.height > 0.0 {
layoutSize.height += dateTitleLayout.size.height
}
if participantsTitleLayout.size.height > 0.0 {
layoutSize.height += participantsTitleLayout.size.height + largeSpacing
}
layoutSize.height += channelButtonsSize.height
if let statusSizeAndApply = statusSizeAndApply {
layoutSize.height += statusSizeAndApply.0.height - 4.0
}
let buttonFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutSize.height - 9.0 - buttonSize.height), size: buttonSize)
return (layoutSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
if strongSelf.item == nil {
strongSelf.animationNode.autoplay = true
if let animationNode = strongSelf.animationNode as? DefaultAnimatedStickerNodeImpl, item.presentationData.isPreview {
animationNode.displaysAsynchronously = false
animationNode.forceSynchronous = true
}
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.start), mode: .direct(cachePathPrefix: nil))
}
strongSelf.item = item
strongSelf.giveaway = giveaway
let displaysAsynchronously = !item.presentationData.isPreview
strongSelf.badgeTextNode.displaysAsynchronously = displaysAsynchronously
strongSelf.prizeTitleNode.displaysAsynchronously = displaysAsynchronously
strongSelf.prizeTextNode.displaysAsynchronously = displaysAsynchronously
strongSelf.additionalPrizeTextNode.displaysAsynchronously = displaysAsynchronously
strongSelf.additionalPrizeSeparatorNode.displaysAsynchronously = displaysAsynchronously
strongSelf.participantsTitleNode.displaysAsynchronously = displaysAsynchronously
strongSelf.participantsTextNode.displaysAsynchronously = displaysAsynchronously
strongSelf.countriesTextNode.displaysAsynchronously = displaysAsynchronously
strongSelf.dateTitleNode.displaysAsynchronously = displaysAsynchronously
strongSelf.dateTextNode.displaysAsynchronously = displaysAsynchronously
strongSelf.buttonNode.titleNode.displaysAsynchronously = displaysAsynchronously
strongSelf.channelButtons.displaysAsynchronously = displaysAsynchronously
strongSelf.updateVisibility()
let _ = badgeTextApply()
let _ = prizeTitleApply()
let _ = prizeTextApply()
let _ = additionalPrizeSeparatorApply()
let _ = additionalPrizeTextApply()
let _ = participantsTitleApply()
let _ = participantsTextApply()
let _ = countriesTextApply()
let _ = dateTitleApply()
let _ = dateTextApply()
let _ = channelButtonsApply()
let _ = buttonApply(animation)
strongSelf.buttonNode.isHidden = item.presentationData.isPreview
var originY: CGFloat = 0.0
let animationOffset: CGFloat = giveaway != nil ? -40.0 : 15.0
let iconSize = giveaway != nil ? CGSize(width: 140.0, height: 140.0) : CGSize(width: 84.0, height: 84.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - iconSize.width) / 2.0), y: originY + animationOffset), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let badgeTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - badgeTextLayout.size.width) / 2.0) + 1.0, y: originY + 88.0), size: badgeTextLayout.size)
strongSelf.badgeTextNode.frame = badgeTextFrame
strongSelf.badgeBackgroundNode.frame = badgeTextFrame.insetBy(dx: -6.0, dy: -5.0).offsetBy(dx: -1.0, dy: 0.0)
if let updatedBadgeImage {
strongSelf.badgeBackgroundNode.image = updatedBadgeImage
}
originY += 112.0
strongSelf.prizeTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTitleLayout.size.width) / 2.0), y: originY), size: prizeTitleLayout.size)
originY += prizeTitleLayout.size.height + smallSpacing
if additionalPrizeTextLayout.size.height > 0.0 {
strongSelf.additionalPrizeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - additionalPrizeTextLayout.size.width) / 2.0), y: originY), size: additionalPrizeTextLayout.size)
originY += additionalPrizeTextLayout.size.height + smallSpacing
let separatorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - additionalPrizeSeparatorLayout.size.width) / 2.0), y: originY), size: additionalPrizeSeparatorLayout.size)
strongSelf.additionalPrizeSeparatorNode.frame = separatorFrame
originY += additionalPrizeSeparatorLayout.size.height + smallSpacing
let lineSpacing: CGFloat = 7.0
let lineWidth = (layoutSize.width - additionalPrizeSeparatorLayout.size.width - (27.0 + lineSpacing) * 2.0) / 2.0
let lineHeight: CGFloat = 1.0 - UIScreenPixel
let lineSize = CGSize(width: lineWidth, height: lineHeight)
strongSelf.additionalPrizeLeftLine.backgroundColor = lineColor
strongSelf.additionalPrizeLeftLine.isHidden = false
strongSelf.additionalPrizeLeftLine.frame = CGRect(origin: CGPoint(x: separatorFrame.minX - lineSize.width - lineSpacing, y: floorToScreenPixels(separatorFrame.midY - lineSize.height / 2.0)), size: lineSize)
strongSelf.additionalPrizeRightLine.backgroundColor = lineColor
strongSelf.additionalPrizeRightLine.isHidden = false
strongSelf.additionalPrizeRightLine.frame = CGRect(origin: CGPoint(x: separatorFrame.maxX + lineSpacing, y: floorToScreenPixels(separatorFrame.midY - lineSize.height / 2.0)), size: lineSize)
} else {
strongSelf.additionalPrizeLeftLine.isHidden = true
strongSelf.additionalPrizeRightLine.isHidden = true
}
strongSelf.prizeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTextLayout.size.width) / 2.0), y: originY), size: prizeTextLayout.size)
originY += prizeTextLayout.size.height + largeSpacing
strongSelf.participantsTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTitleLayout.size.width) / 2.0), y: originY), size: participantsTitleLayout.size)
if participantsTitleLayout.size.height > 0.0 {
originY += participantsTitleLayout.size.height + smallSpacing
}
strongSelf.participantsTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTextLayout.size.width) / 2.0), y: originY), size: participantsTextLayout.size)
originY += participantsTextLayout.size.height + smallSpacing * 2.0 + 3.0
strongSelf.channelButtons.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - channelButtonsSize.width) / 2.0), y: originY), size: channelButtonsSize)
originY += channelButtonsSize.height
if countriesTextLayout.size.height > 0.0 {
originY += smallSpacing * 2.0 + 3.0
strongSelf.countriesTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - countriesTextLayout.size.width) / 2.0), y: originY), size: countriesTextLayout.size)
originY += countriesTextLayout.size.height
}
if participantsTitleLayout.size.height > 0.0 {
originY += largeSpacing
}
strongSelf.dateTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTitleLayout.size.width) / 2.0), y: originY), size: dateTitleLayout.size)
originY += dateTitleLayout.size.height + smallSpacing
strongSelf.dateTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTextLayout.size.width) / 2.0), y: originY), size: dateTextLayout.size)
originY += dateTextLayout.size.height + largeSpacing
strongSelf.buttonNode.frame = buttonFrame
if let statusSizeAndApply = statusSizeAndApply {
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: strongSelf.dateTextNode.frame.maxY + 2.0), size: statusSizeAndApply.0)
if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
statusSizeAndApply.1(.None)
} else {
statusSizeAndApply.1(animation)
}
} else if strongSelf.dateAndStatusNode.supernode != nil {
strongSelf.dateAndStatusNode.removeFromSupernode()
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
})
})
})
}
}
private func updateVisibility() {
}
private var absoluteRect: (CGRect, CGSize)?
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize)
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
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.2, removeOnCompletion: false)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.channelButtons.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.prizeTextNode.frame.contains(point), let item = self.item {
if let giveawayResults = item.message.media.first(where: { $0 is TelegramMediaGiveawayResults }) as? TelegramMediaGiveawayResults {
item.controllerInteraction.navigateToMessageStandalone(giveawayResults.launchMessageId)
return ChatMessageBubbleContentTapAction(content: .ignore)
}
}
return ChatMessageBubbleContentTapAction(content: .none)
}
@objc private func buttonPressed() {
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress()))
}
}
private func makeProgress() -> Promise<Bool> {
let progress = Promise<Bool>()
self.currentProgressDisposable?.dispose()
self.currentProgressDisposable = (progress.get()
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
guard let self else {
return
}
self.displayProgress = hasProgress
})
return progress
}
private var displayProgress = false {
didSet {
if self.displayProgress != oldValue {
if self.displayProgress {
self.buttonNode.startShimmering()
} else {
self.buttonNode.stopShimmering()
}
}
}
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.dateAndStatusNode.isHidden {
return self.dateAndStatusNode.reactionView(value: value)
}
return nil
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}
private final class PeerButtonsStackNode: ASDisplayNode {
var buttonNodes: [PeerButtonNode] = []
var openPeer: (EnginePeer) -> Void = { _ in }
static func asyncLayout(_ current: PeerButtonsStackNode) -> (_ context: AccountContext, _ width: CGFloat, _ peers: [EnginePeer], _ titleColor: UIColor, _ backgroundColor: UIColor, _ incoming: Bool, _ dark: Bool) -> (CGFloat, (CGFloat, Bool) -> (CGSize, () -> PeerButtonsStackNode)) {
let currentChannelButtons = current.buttonNodes.isEmpty ? nil : current.buttonNodes
let maybeMakeChannelButtons = current.buttonNodes.map(PeerButtonNode.asyncLayout)
return { context, width, peers, titleColor, backgroundColor, incoming, dark in
let targetNode = current
var buttonNodes: [PeerButtonNode] = []
let makeChannelButtonLayouts: [(_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat, Bool) -> (CGSize, () -> PeerButtonNode))]
if let currentChannelButtons {
buttonNodes = currentChannelButtons
makeChannelButtonLayouts = maybeMakeChannelButtons
} else {
for _ in peers {
buttonNodes.append(PeerButtonNode())
}
makeChannelButtonLayouts = buttonNodes.map(PeerButtonNode.asyncLayout)
}
var maxWidth = 0.0
let buttonHeight: CGFloat = 24.0
let horizontalButtonSpacing: CGFloat = 4.0
let verticalButtonSpacing: CGFloat = 6.0
var sizes: [CGSize] = []
var groups: [[Int]] = []
var currentGroup: [Int] = []
var buttonContinues: [(CGFloat, Bool) -> (CGSize, () -> PeerButtonNode)] = []
for i in 0 ..< makeChannelButtonLayouts.count {
let peer = peers[i]
let makeChannelButtonLayout = makeChannelButtonLayouts[i]
var titleColor = titleColor
var backgroundColor = backgroundColor
if incoming, let nameColor = peer.nameColor, makeChannelButtonLayouts.count > 1 {
titleColor = context.peerNameColors.get(nameColor, dark: dark).main
backgroundColor = titleColor.withAlphaComponent(0.1)
}
let (buttonWidth, buttonContinue) = makeChannelButtonLayout(context, width, peer, titleColor, backgroundColor)
sizes.append(CGSize(width: buttonWidth, height: buttonHeight))
buttonContinues.append(buttonContinue)
var itemsWidth: CGFloat = 0.0
for j in currentGroup {
itemsWidth += sizes[j].width
}
itemsWidth += buttonWidth
itemsWidth += CGFloat(currentGroup.count) * horizontalButtonSpacing
if itemsWidth > width {
groups.append(currentGroup)
currentGroup = []
}
currentGroup.append(i)
}
if !currentGroup.isEmpty {
groups.append(currentGroup)
}
var rowWidths: [CGFloat] = []
for group in groups {
var rowWidth: CGFloat = 0.0
for i in group {
let buttonSize = sizes[i]
rowWidth += buttonSize.width
}
rowWidth += CGFloat(currentGroup.count) * horizontalButtonSpacing
if rowWidth > maxWidth {
maxWidth = rowWidth
}
rowWidths.append(rowWidth)
}
var frames: [CGRect] = []
var originY: CGFloat = 0.0
for i in 0 ..< groups.count {
let rowWidth = rowWidths[i]
var originX = floorToScreenPixels((maxWidth - rowWidth) / 2.0)
for i in groups[i] {
let buttonSize = sizes[i]
frames.append(CGRect(origin: CGPoint(x: originX, y: originY), size: buttonSize))
originX += buttonSize.width + horizontalButtonSpacing
}
originY += buttonHeight + verticalButtonSpacing
}
return (maxWidth, { _, displayAsynchronously in
var buttonLayoutsAndApply: [(CGSize, () -> PeerButtonNode)] = []
for buttonApply in buttonContinues {
buttonLayoutsAndApply.append(buttonApply(maxWidth, displayAsynchronously))
}
return (CGSize(width: maxWidth, height: max(0, originY - verticalButtonSpacing)), {
targetNode.buttonNodes = buttonNodes
for i in 0 ..< buttonNodes.count {
let peer = peers[i]
let buttonNode = buttonNodes[i]
buttonNode.pressed = { [weak targetNode] in
targetNode?.openPeer(peer)
}
if buttonNode.supernode == nil {
targetNode.addSubnode(buttonNode)
}
let frame = frames[i]
buttonNode.frame = frame
}
for (_, apply) in buttonLayoutsAndApply {
let _ = apply()
}
return targetNode
})
})
}
}
}
private final class PeerButtonNode: HighlightTrackingButtonNode {
private let backgroundNode: ASImageNode
private let textNode: TextNode
private let avatarNode: AvatarNode
var currentBackgroundColor: UIColor?
var pressed: (() -> Void)?
init() {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc func buttonPressed() {
self.pressed?()
}
static func asyncLayout(_ current: PeerButtonNode?) -> (_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat, Bool) -> (CGSize, () -> PeerButtonNode)) {
let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout)
return { context, width, peer, titleColor, backgroundColor in
let targetNode: PeerButtonNode
if let current = current {
targetNode = current
} else {
targetNode = PeerButtonNode()
}
let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
if let maybeMakeTextLayout = maybeMakeTextLayout {
makeTextLayout = maybeMakeTextLayout
} else {
makeTextLayout = TextNode.asyncLayout(targetNode.textNode)
}
let inset: CGFloat = 1.0
let avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 5.0
let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: peer?.compactDisplayTitle ?? "", font: Font.medium(14.0), textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - avatarSize.width - (spacing + inset) * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
let refinedWidth = avatarSize.width + textSize.size.width + (spacing + inset) * 2.0
return (refinedWidth, { _, displayAsynchronously in
return (CGSize(width: refinedWidth, height: 24.0), {
let _ = textApply()
targetNode.avatarNode.contentNode.displaysAsynchronously = displayAsynchronously
targetNode.textNode.displaysAsynchronously = displayAsynchronously
let backgroundFrame = CGRect(origin: .zero, size: CGSize(width: refinedWidth, height: 24.0))
let textFrame = CGRect(origin: CGPoint(x: inset + avatarSize.width + spacing, y: floorToScreenPixels((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size)
targetNode.backgroundNode.frame = backgroundFrame
if targetNode.currentBackgroundColor != backgroundColor {
targetNode.currentBackgroundColor = backgroundColor
targetNode.backgroundNode.image = generateStretchableFilledCircleImage(radius: 12.0, color: backgroundColor, backgroundColor: nil)
}
targetNode.avatarNode.setPeer(
context: context,
theme: context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: peer,
synchronousLoad: false
)
targetNode.avatarNode.frame = CGRect(origin: CGPoint(x: inset, y: inset), size: avatarSize)
targetNode.textNode.frame = textFrame
return targetNode
})
})
}
}
}