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, ASGestureRecognizerDelegate { 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, sourceView in guard let strongSelf = self, let item = strongSelf.item else { return } item.controllerInteraction.updateMessageReaction(item.topMessage, .reaction(value), false, sourceView) } 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.wrappedGestureRecognizerDelegate 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 let author = item.message.forwardInfo?.author ?? item.message.author var isGroup = false if let channel = author as? TelegramChannel, case .group = channel.info { isGroup = true } if let giveaway { if giveaway.flags.contains(.onlyNewSubscribers) { if giveaway.channelPeerIds.count > 1 { participantsText = isGroup ? item.presentationData.strings.Chat_Giveaway_Message_Group_ParticipantsNewMany : item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNewMany } else { participantsText = isGroup ? item.presentationData.strings.Chat_Giveaway_Message_Group_ParticipantsNew : item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNew } } else { if giveaway.channelPeerIds.count > 1 { participantsText = isGroup ? item.presentationData.strings.Chat_Giveaway_Message_Group_ParticipantsMany : item.presentationData.strings.Chat_Giveaway_Message_ParticipantsMany } else { participantsText = isGroup ? item.presentationData.strings.Chat_Giveaway_Message_Group_Participants : 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 hideHeaders = item.message.forwardInfo == nil let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: hideHeaders, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none, hidesHeaders: hideHeaders) 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.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId), messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects), replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: item.topMessage), 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 { let progress = Promise() 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 messageEffectTargetView() -> UIView? { if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.messageEffectTargetView() } 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 }) }) } } }