Ilya Laktyushin 5ee53ad996 Various fixes
2025-03-12 00:40:11 +04:00

622 lines
37 KiB
Swift

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<Void, NoError>?, (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), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, 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: 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 + 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
}
}