import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TextFormat import UrlEscaping import PhotoResources import AccountContext import WallpaperBackgroundNode import ChatControllerInteraction import ChatMessageBubbleContentNode import CountrySelectionUI import TelegramStringFormatting import MergedAvatarsNode import ChatControllerInteraction import TextNodeWithEntities public final class ChatUserInfoItem: ListViewItem { fileprivate let peer: EnginePeer fileprivate let verification: PeerVerification? fileprivate let registrationDate: String? fileprivate let phoneCountry: String? fileprivate let groupsInCommonCount: Int32 fileprivate let controllerInteraction: ChatControllerInteraction fileprivate let presentationData: ChatPresentationData fileprivate let context: AccountContext public init( peer: EnginePeer, verification: PeerVerification?, registrationDate: String?, phoneCountry: String?, groupsInCommonCount: Int32, controllerInteraction: ChatControllerInteraction, presentationData: ChatPresentationData, context: AccountContext ) { self.peer = peer self.verification = verification self.registrationDate = registrationDate self.phoneCountry = phoneCountry self.groupsInCommonCount = groupsInCommonCount self.controllerInteraction = controllerInteraction self.presentationData = presentationData self.context = context } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { let configure = { let node = ChatUserInfoItemNode() let nodeLayout = node.asyncLayout() let (layout, apply) = nodeLayout(self, params) node.contentSize = layout.contentSize node.insets = layout.insets Queue.mainQueue().async { completion(node, { return (nil, { _ in apply(.None) }) }) } } if Thread.isMainThread { async { configure() } } else { configure() } } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ChatUserInfoItemNode { let nodeLayout = nodeValue.asyncLayout() async { let (layout, apply) = nodeLayout(self, params) Queue.mainQueue().async { completion(layout, { _ in apply(animation) }) } } } } } } public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDelegate { public var controllerInteraction: ChatControllerInteraction? public let offsetContainer: ASDisplayNode public let titleNode: TextNode public let subtitleNode: TextNode private let registrationDateTitleTextNode: TextNode private let registrationDateValueTextNode: TextNode private var registrationDateText: String? private let phoneCountryTitleTextNode: TextNode private let phoneCountryValueTextNode: TextNode private var phoneCountryText: String? private let groupsTitleTextNode: TextNode private let groupsValueTextNode: TextNode private let groupsButtonNode: HighlightTrackingButtonNode private let groupsAvatarsNode: MergedAvatarsNode private let groupsArrowNode: ASImageNode private var groupsInCommonContext: GroupsInCommonContext? private var groupsInCommonDisposable: Disposable? private var groupsInCommon: [Peer] = [] private let disclaimerTextNode: TextNodeWithEntities private var theme: ChatPresentationThemeData? private var wallpaperBackgroundNode: WallpaperBackgroundNode? private var backgroundContent: WallpaperBubbleBackgroundNode? private var absolutePosition: (CGRect, CGSize)? private var item: ChatUserInfoItem? public init() { self.offsetContainer = ASDisplayNode() self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.subtitleNode = TextNode() self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false self.registrationDateTitleTextNode = TextNode() self.registrationDateTitleTextNode.isUserInteractionEnabled = false self.registrationDateTitleTextNode.displaysAsynchronously = false self.registrationDateValueTextNode = TextNode() self.registrationDateValueTextNode.isUserInteractionEnabled = false self.registrationDateValueTextNode.displaysAsynchronously = false self.phoneCountryTitleTextNode = TextNode() self.phoneCountryTitleTextNode.isUserInteractionEnabled = false self.phoneCountryTitleTextNode.displaysAsynchronously = false self.phoneCountryValueTextNode = TextNode() self.phoneCountryValueTextNode.isUserInteractionEnabled = false self.phoneCountryValueTextNode.displaysAsynchronously = false self.groupsTitleTextNode = TextNode() self.groupsTitleTextNode.isUserInteractionEnabled = false self.groupsTitleTextNode.displaysAsynchronously = false self.groupsValueTextNode = TextNode() self.groupsValueTextNode.isUserInteractionEnabled = false self.groupsValueTextNode.displaysAsynchronously = false self.groupsAvatarsNode = MergedAvatarsNode() self.groupsArrowNode = ASImageNode() self.groupsArrowNode.displaysAsynchronously = false self.groupsButtonNode = HighlightTrackingButtonNode() self.disclaimerTextNode = TextNodeWithEntities() self.disclaimerTextNode.textNode.isUserInteractionEnabled = false self.disclaimerTextNode.textNode.displaysAsynchronously = false super.init(layerBacked: false, dynamicBounce: true, rotated: true) self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.addSubnode(self.offsetContainer) self.offsetContainer.addSubnode(self.titleNode) self.offsetContainer.addSubnode(self.subtitleNode) self.offsetContainer.addSubnode(self.disclaimerTextNode.textNode) self.offsetContainer.addSubnode(self.groupsAvatarsNode) self.offsetContainer.addSubnode(self.groupsArrowNode) self.offsetContainer.addSubnode(self.groupsButtonNode) self.wantsTrailingItemSpaceUpdates = true self.groupsButtonNode.highligthedChanged = { [weak self] highlighted in if let self { if highlighted { self.groupsValueTextNode.layer.removeAnimation(forKey: "opacity") self.groupsValueTextNode.alpha = 0.4 self.groupsAvatarsNode.layer.removeAnimation(forKey: "opacity") self.groupsAvatarsNode.alpha = 0.4 self.groupsArrowNode.layer.removeAnimation(forKey: "opacity") self.groupsArrowNode.alpha = 0.4 } else { self.groupsValueTextNode.alpha = 1.0 self.groupsValueTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) self.groupsAvatarsNode.alpha = 1.0 self.groupsAvatarsNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) self.groupsArrowNode.alpha = 1.0 self.groupsArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.groupsButtonNode.addTarget(self, action: #selector(self.groupsPressed), forControlEvents: .touchUpInside) } override public func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) tapRecognizer.delegate = self.wrappedGestureRecognizerDelegate self.offsetContainer.view.addGestureRecognizer(tapRecognizer) } public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer.view === self.offsetContainer.view { let location = gestureRecognizer.location(in: self.offsetContainer.view) if let backgroundContent = self.backgroundContent, backgroundContent.frame.contains(location) { return true } return false } return true } @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { guard let item = self.item else { return } item.controllerInteraction.openPeer(item.peer, .info(nil), nil, .default) } override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { super.updateAbsoluteRect(rect, within: containerSize) self.absolutePosition = (rect, containerSize) if let backgroundContent = self.backgroundContent { var backgroundFrame = backgroundContent.frame backgroundFrame.origin.x += rect.minX backgroundFrame.origin.y += containerSize.height - rect.minY backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } } @objc private func groupsPressed() { guard let item = self.item else { return } item.controllerInteraction.openPeer(item.peer, .info(ChatControllerInteractionNavigateToPeer.InfoParams(switchToGroupsInCommon: true)), nil, .default) } public func asyncLayout() -> (_ item: ChatUserInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeRegistrationDateTitleLayout = TextNode.asyncLayout(self.registrationDateTitleTextNode) let makeRegistrationDateValueLayout = TextNode.asyncLayout(self.registrationDateValueTextNode) let makePhoneCountryTitleLayout = TextNode.asyncLayout(self.phoneCountryTitleTextNode) let makePhoneCountryValueLayout = TextNode.asyncLayout(self.phoneCountryValueTextNode) let makeGroupsTitleLayout = TextNode.asyncLayout(self.groupsTitleTextNode) let makeGroupsValueLayout = TextNode.asyncLayout(self.groupsValueTextNode) let makeDisclaimerLayout = TextNodeWithEntities.asyncLayout(self.disclaimerTextNode) let currentItem = self.item let currentRegistrationDateText = self.registrationDateText let currentPhoneCountryText = self.phoneCountryText return { [weak self] item, params in let themeUpdated = item.presentationData.theme !== currentItem?.presentationData.theme var backgroundSize = CGSize(width: 240.0, height: 0.0) let verticalItemInset: CGFloat = 10.0 let horizontalInset: CGFloat = 10.0 + params.leftInset let horizontalContentInset: CGFloat = 16.0 let verticalInset: CGFloat = 17.0 let verticalSpacing: CGFloat = 6.0 let paragraphSpacing: CGFloat = 3.0 let attributeSpacing: CGFloat = 10.0 let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText let subtitleColor = primaryTextColor.withAlphaComponent(item.presentationData.theme.theme.overallDarkAppearance ? 0.7 : 0.8) backgroundSize.height += verticalInset let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0 let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) backgroundSize.height += titleLayout.size.height backgroundSize.height += verticalSpacing let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_Subtitle, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) backgroundSize.height += subtitleLayout.size.height backgroundSize.height += verticalSpacing + paragraphSpacing let infoConstrainedSize = CGSize(width: floor(constrainedWidth * 0.7), height: CGFloat.greatestFiniteMagnitude) var maxTitleWidth: CGFloat = 0.0 var maxValueWidth: CGFloat = 0.0 var phoneCountryText: String? let phoneCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? let phoneCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)? if let phoneCountry = item.phoneCountry { if let currentPhoneCountryText { phoneCountryText = currentPhoneCountryText } else { var countryName = "" let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 } if phoneCountry == "FT" { countryName = item.presentationData.strings.Chat_NonContactUser_AnonymousNumber } else if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) { countryName = country.localizedName ?? country.name } else if phoneCountry == "TS" { countryName = "Test" } phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName } phoneCountryTitleLayoutAndApply = makePhoneCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_PhoneNumber, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) phoneCountryValueLayoutAndApply = makePhoneCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: phoneCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) backgroundSize.height += verticalSpacing backgroundSize.height += phoneCountryValueLayoutAndApply?.0.size.height ?? 0 maxTitleWidth = max(maxTitleWidth, (phoneCountryTitleLayoutAndApply?.0.size.width ?? 0)) maxValueWidth = max(maxValueWidth, (phoneCountryValueLayoutAndApply?.0.size.width ?? 0)) } else { phoneCountryTitleLayoutAndApply = nil phoneCountryValueLayoutAndApply = nil } var registrationDateText: String? let registrationDateTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? let registrationDateValueLayoutAndApply: (TextNodeLayout, () -> TextNode)? if let registrationDate = item.registrationDate { if let currentRegistrationDateText { registrationDateText = currentRegistrationDateText } else { let components = registrationDate.components(separatedBy: ".") if components.count == 2, let first = Int32(components[0]), let second = Int32(components[1]) { let month = first - 1 let year = second - 1900 registrationDateText = stringForMonth(strings: item.presentationData.strings, month: month, ofYear: year) } else { registrationDateText = "" } } registrationDateTitleLayoutAndApply = makeRegistrationDateTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_Registration, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) registrationDateValueLayoutAndApply = makeRegistrationDateValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: registrationDateText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) backgroundSize.height += verticalSpacing backgroundSize.height += registrationDateValueLayoutAndApply?.0.size.height ?? 0 maxTitleWidth = max(maxTitleWidth, (registrationDateTitleLayoutAndApply?.0.size.width ?? 0)) maxValueWidth = max(maxValueWidth, (registrationDateValueLayoutAndApply?.0.size.width ?? 0)) } else { registrationDateTitleLayoutAndApply = nil registrationDateValueLayoutAndApply = nil } let avatarImageSize: CGFloat = 18.0 let avatarSpacing: CGFloat = 9.0 let avatarBorder: CGFloat = 1.0 let groupsValueText: NSMutableAttributedString let groupsInCommonCount = item.groupsInCommonCount var estimatedValueOffset: CGFloat = 0.0 if groupsInCommonCount > 0 { groupsValueText = NSMutableAttributedString(string: item.presentationData.strings.Chat_NonContactUser_GroupsCount(groupsInCommonCount), font: Font.semibold(13.0), textColor: primaryTextColor) estimatedValueOffset = avatarImageSize + CGFloat(min(2, max(0, item.groupsInCommonCount - 1))) * avatarSpacing + 4.0 + 10.0 } else { groupsValueText = NSMutableAttributedString(string: "", font: Font.semibold(13.0), textColor: primaryTextColor) } let groupsTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? let groupsValueLayoutAndApply: (TextNodeLayout, () -> TextNode)? if !groupsValueText.string.isEmpty { groupsTitleLayoutAndApply = makeGroupsTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_Groups, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) groupsValueLayoutAndApply = makeGroupsValueLayout(TextNodeLayoutArguments(attributedString: groupsValueText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) backgroundSize.height += verticalSpacing backgroundSize.height += groupsValueLayoutAndApply?.0.size.height ?? 0.0 maxTitleWidth = max(maxTitleWidth, groupsTitleLayoutAndApply?.0.size.width ?? 0) maxValueWidth = max(maxValueWidth, (groupsValueLayoutAndApply?.0.size.width ?? 0) + estimatedValueOffset) } else { groupsTitleLayoutAndApply = nil groupsValueLayoutAndApply = nil } backgroundSize.width = horizontalContentInset * 2.0 + max(titleLayout.size.width, maxTitleWidth + attributeSpacing + maxValueWidth) let disclaimerText: NSMutableAttributedString if let verification = item.verification { disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor) if let range = disclaimerText.string.range(of: "#") { disclaimerText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: verification.iconFileId, file: nil), range: NSRange(range, in: disclaimerText.string)) disclaimerText.addAttribute(.foregroundColor, value: subtitleColor, range: NSRange(range, in: disclaimerText.string)) disclaimerText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: disclaimerText.string)) } } else { disclaimerText = NSMutableAttributedString(string: " # \(item.presentationData.strings.Chat_NonContactUser_Disclaimer)", font: Font.regular(13.0), textColor: subtitleColor) if let range = disclaimerText.string.range(of: "#") { disclaimerText.addAttribute(.attachment, value: PresentationResourcesChat.chatUserInfoWarningIcon(item.presentationData.theme.theme)!, range: NSRange(range, in: disclaimerText.string)) disclaimerText.addAttribute(.foregroundColor, value: subtitleColor, range: NSRange(range, in: disclaimerText.string)) disclaimerText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: disclaimerText.string)) } } let (disclaimerLayout, disclaimerApply) = makeDisclaimerLayout(TextNodeLayoutArguments(attributedString: disclaimerText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: backgroundSize.width - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) backgroundSize.height += verticalSpacing * 2.0 + paragraphSpacing backgroundSize.height += disclaimerLayout.size.height backgroundSize.height += verticalInset let backgroundFrame = CGRect(origin: CGPoint(x: floor((params.width - backgroundSize.width) / 2.0), y: verticalItemInset + 4.0), size: backgroundSize) let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: backgroundSize.height + verticalItemInset * 2.0), insets: UIEdgeInsets()) return (itemLayout, { _ in if let strongSelf = self { strongSelf.item = item strongSelf.theme = item.presentationData.theme if themeUpdated { strongSelf.groupsArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: primaryTextColor) } if item.groupsInCommonCount > 0 { if strongSelf.groupsInCommonContext == nil { let groupsInCommonContext = GroupsInCommonContext(account: item.context.account, peerId: item.peer.id) strongSelf.groupsInCommonContext = groupsInCommonContext strongSelf.groupsInCommonDisposable = (groupsInCommonContext.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self, let item = self.item else { return } self.groupsInCommon = Array(state.peers.compactMap { $0.peer }.prefix(3)) self.groupsAvatarsNode.update(context: item.context, peers: self.groupsInCommon, synchronousLoad: true, imageSize: avatarImageSize, imageSpacing: avatarSpacing, borderWidth: avatarBorder) }) } } if item.presentationData.theme.theme.overallDarkAppearance { strongSelf.registrationDateTitleTextNode.layer.compositingFilter = nil strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = nil strongSelf.groupsTitleTextNode.layer.compositingFilter = nil strongSelf.subtitleNode.layer.compositingFilter = nil strongSelf.disclaimerTextNode.textNode.layer.compositingFilter = nil } else { strongSelf.registrationDateTitleTextNode.layer.compositingFilter = "overlayBlendMode" strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode" strongSelf.groupsTitleTextNode.layer.compositingFilter = "overlayBlendMode" strongSelf.subtitleNode.layer.compositingFilter = "overlayBlendMode" strongSelf.disclaimerTextNode.textNode.layer.compositingFilter = "overlayBlendMode" } strongSelf.registrationDateText = registrationDateText strongSelf.phoneCountryText = phoneCountryText strongSelf.controllerInteraction = item.controllerInteraction strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) let _ = titleApply() var contentOriginY = backgroundFrame.origin.y + verticalInset let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - titleLayout.size.width) / 2.0), y: contentOriginY), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame contentOriginY += titleLayout.size.height contentOriginY += verticalSpacing - paragraphSpacing let _ = subtitleApply() let subtitleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - subtitleLayout.size.width) / 2.0), y: contentOriginY), size: subtitleLayout.size) strongSelf.subtitleNode.frame = subtitleFrame contentOriginY += subtitleLayout.size.height contentOriginY += verticalSpacing * 2.0 + paragraphSpacing var attributeMidpoints: [CGFloat] = [] func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?, valueOffset: CGFloat = 0.0) { if let valueLayout { let midpoint = backgroundSize.width - horizontalContentInset - valueLayout.size.width - valueOffset - attributeSpacing / 2.0 attributeMidpoints.append(midpoint) } } appendAttributeMidpoint(titleLayout: phoneCountryTitleLayoutAndApply?.0, valueLayout: phoneCountryValueLayoutAndApply?.0) appendAttributeMidpoint(titleLayout: registrationDateTitleLayoutAndApply?.0, valueLayout: registrationDateValueLayoutAndApply?.0) appendAttributeMidpoint(titleLayout: groupsTitleLayoutAndApply?.0, valueLayout: groupsValueLayoutAndApply?.0, valueOffset: estimatedValueOffset) let middleX = floorToScreenPixels(attributeMidpoints.min() ?? backgroundSize.width / 2.0) let titleMaxX: CGFloat = backgroundFrame.minX + middleX - attributeSpacing / 2.0 let valueMinX: CGFloat = backgroundFrame.minX + middleX + attributeSpacing / 2.0 func positionAttributeNodes( titleTextNode: TextNode, valueTextNode: TextNode, valueOffset: CGFloat = 0.0, titleLayoutAndApply: (TextNodeLayout, () -> TextNode)?, valueLayoutAndApply: (TextNodeLayout, () -> TextNode)? ) { if let (titleLayout, titleApply) = titleLayoutAndApply { if titleTextNode.supernode == nil { strongSelf.offsetContainer.addSubnode(titleTextNode) } let _ = titleApply() titleTextNode.frame = CGRect( origin: CGPoint(x: titleMaxX - titleLayout.size.width, y: contentOriginY), size: titleLayout.size ) } if let (valueLayout, valueApply) = valueLayoutAndApply { if valueTextNode.supernode == nil { strongSelf.offsetContainer.addSubnode(valueTextNode) } let _ = valueApply() valueTextNode.frame = CGRect( origin: CGPoint(x: valueMinX + valueOffset, y: contentOriginY), size: valueLayout.size ) contentOriginY += valueLayout.size.height + verticalSpacing } } positionAttributeNodes( titleTextNode: strongSelf.phoneCountryTitleTextNode, valueTextNode: strongSelf.phoneCountryValueTextNode, titleLayoutAndApply: phoneCountryTitleLayoutAndApply, valueLayoutAndApply: phoneCountryValueLayoutAndApply ) positionAttributeNodes( titleTextNode: strongSelf.registrationDateTitleTextNode, valueTextNode: strongSelf.registrationDateValueTextNode, titleLayoutAndApply: registrationDateTitleLayoutAndApply, valueLayoutAndApply: registrationDateValueLayoutAndApply ) var valueOffset: CGFloat = 0.0 if let groupsValueLayoutAndApply { let avatarsFrame = CGRect(origin: CGPoint(x: valueMinX + groupsValueLayoutAndApply.0.size.width + 4.0, y: contentOriginY + floor((groupsValueLayoutAndApply.0.size.height - avatarImageSize) / 2.0)), size: CGSize(width: avatarImageSize + avatarSpacing * 2.0, height: avatarImageSize)) strongSelf.groupsAvatarsNode.frame = avatarsFrame strongSelf.groupsAvatarsNode.updateLayout(size: avatarsFrame.size) strongSelf.groupsAvatarsNode.update(context: item.context, peers: strongSelf.groupsInCommon, synchronousLoad: true, imageSize: avatarImageSize, imageSpacing: avatarSpacing, borderWidth: avatarBorder) if groupsInCommonCount > 0 { valueOffset = avatarImageSize + CGFloat(min(2, max(0, groupsInCommonCount - 1))) * avatarSpacing + 4.0 strongSelf.groupsButtonNode.frame = CGRect(origin: CGPoint(x: valueMinX, y: contentOriginY), size: CGSize(width: groupsValueLayoutAndApply.0.size.width + 20.0, height: 18.0)) strongSelf.groupsButtonNode.isHidden = false strongSelf.groupsAvatarsNode.isHidden = false strongSelf.groupsArrowNode.isHidden = false if let icon = strongSelf.groupsArrowNode.image { strongSelf.groupsArrowNode.frame = CGRect(origin: CGPoint(x: avatarsFrame.minX + valueOffset, y: contentOriginY + 4.0 - UIScreenPixel), size: icon.size) } } else { strongSelf.groupsAvatarsNode.isHidden = true strongSelf.groupsButtonNode.isHidden = true strongSelf.groupsArrowNode.isHidden = true } } positionAttributeNodes( titleTextNode: strongSelf.groupsTitleTextNode, valueTextNode: strongSelf.groupsValueTextNode, titleLayoutAndApply: groupsTitleLayoutAndApply, valueLayoutAndApply: groupsValueLayoutAndApply ) contentOriginY += verticalSpacing + paragraphSpacing let _ = disclaimerApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: primaryTextColor.withMultipliedAlpha(0.4), attemptSynchronous: true)) let disclaimerFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - disclaimerLayout.size.width) / 2.0), y: contentOriginY), size: disclaimerLayout.size) strongSelf.disclaimerTextNode.textNode.frame = disclaimerFrame if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { backgroundContent.clipsToBounds = true strongSelf.backgroundContent = backgroundContent strongSelf.offsetContainer.insertSubnode(backgroundContent, at: 0) } if let backgroundContent = strongSelf.backgroundContent { backgroundContent.cornerRadius = item.presentationData.chatBubbleCorners.mainRadius backgroundContent.frame = backgroundFrame if let (rect, containerSize) = strongSelf.absolutePosition { var backgroundFrame = backgroundContent.frame backgroundFrame.origin.x += rect.minX backgroundFrame.origin.y += containerSize.height - rect.minY backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } } } }) } } override public func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { if height.isLessThanOrEqualTo(0.0) { transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) } else { transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: -floorToScreenPixels(height / 2.0)), size: self.offsetContainer.bounds.size)) } } override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let result = super.point(inside: point, with: event) let extra = self.offsetContainer.frame.contains(point) return result || extra } }