mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
590 lines
35 KiB
Swift
590 lines
35 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 UniversalMediaPlayer
|
|
import TelegramUniversalVideoContent
|
|
import WallpaperBackgroundNode
|
|
import ChatControllerInteraction
|
|
import ChatMessageBubbleContentNode
|
|
import CountrySelectionUI
|
|
import TelegramStringFormatting
|
|
|
|
public final class ChatUserInfoItem: ListViewItem {
|
|
fileprivate let title: String
|
|
fileprivate let registrationDate: String?
|
|
fileprivate let phoneCountry: String?
|
|
fileprivate let locationCountry: String?
|
|
fileprivate let groupsInCommon: [EnginePeer]
|
|
fileprivate let controllerInteraction: ChatControllerInteraction
|
|
fileprivate let presentationData: ChatPresentationData
|
|
fileprivate let context: AccountContext
|
|
|
|
public init(
|
|
title: String,
|
|
registrationDate: String?,
|
|
phoneCountry: String?,
|
|
locationCountry: String?,
|
|
groupsInCommon: [EnginePeer],
|
|
controllerInteraction: ChatControllerInteraction,
|
|
presentationData: ChatPresentationData,
|
|
context: AccountContext
|
|
) {
|
|
self.title = title
|
|
self.registrationDate = registrationDate
|
|
self.phoneCountry = phoneCountry
|
|
self.locationCountry = locationCountry
|
|
self.groupsInCommon = groupsInCommon
|
|
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 {
|
|
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 locationCountryTitleTextNode: TextNode
|
|
private let locationCountryValueTextNode: TextNode
|
|
private var locationCountryText: String?
|
|
|
|
private let groupsTextNode: TextNode
|
|
|
|
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.locationCountryTitleTextNode = TextNode()
|
|
self.locationCountryTitleTextNode.isUserInteractionEnabled = false
|
|
self.locationCountryTitleTextNode.displaysAsynchronously = false
|
|
self.locationCountryValueTextNode = TextNode()
|
|
self.locationCountryValueTextNode.isUserInteractionEnabled = false
|
|
self.locationCountryValueTextNode.displaysAsynchronously = false
|
|
|
|
self.groupsTextNode = TextNode()
|
|
self.groupsTextNode.isUserInteractionEnabled = false
|
|
self.groupsTextNode.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.groupsTextNode)
|
|
self.wantsTrailingItemSpaceUpdates = true
|
|
}
|
|
|
|
override public func didLoad() {
|
|
super.didLoad()
|
|
|
|
// let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
// recognizer.tapActionAtPoint = { [weak self] point in
|
|
// if let strongSelf = self {
|
|
// let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true)
|
|
// switch tapAction.content {
|
|
// case .none:
|
|
// break
|
|
// case .ignore:
|
|
// return .fail
|
|
// case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom:
|
|
// return .waitForSingleTap
|
|
// }
|
|
// }
|
|
//
|
|
// return .waitForDoubleTap
|
|
// }
|
|
// recognizer.highlight = { [weak self] point in
|
|
// if let strongSelf = self {
|
|
// strongSelf.updateTouchesAtPoint(point)
|
|
// }
|
|
// }
|
|
// self.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 makeLocationCountryTitleLayout = TextNode.asyncLayout(self.locationCountryTitleTextNode)
|
|
let makeLocationCountryValueLayout = TextNode.asyncLayout(self.locationCountryValueTextNode)
|
|
let makeGroupsLayout = TextNode.asyncLayout(self.groupsTextNode)
|
|
|
|
let currentRegistrationDateText = self.registrationDateText
|
|
let currentPhoneCountryText = self.phoneCountryText
|
|
let currentLocationCountryText = self.locationCountryText
|
|
|
|
return { [weak self] item, params in
|
|
self?.item = item
|
|
|
|
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
|
|
//TODO:localize
|
|
let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0
|
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, 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: "Not a contact", 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 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: "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
|
|
}
|
|
|
|
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 let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) {
|
|
countryName = country.localizedName ?? country.name
|
|
}
|
|
phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName
|
|
}
|
|
phoneCountryTitleLayoutAndApply = makePhoneCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Phone Number", 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 locationCountryText: String?
|
|
let locationCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
let locationCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
if let locationCountry = item.locationCountry {
|
|
if let currentLocationCountryText {
|
|
locationCountryText = currentLocationCountryText
|
|
} else {
|
|
var countryName = ""
|
|
let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 }
|
|
if let country = countriesConfiguration.countries.first(where: { $0.id == locationCountry }) {
|
|
countryName = country.localizedName ?? country.name
|
|
}
|
|
locationCountryText = emojiFlagForISOCountryCode(locationCountry) + " " + countryName
|
|
}
|
|
locationCountryTitleLayoutAndApply = makeLocationCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Location", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
locationCountryValueLayoutAndApply = makeLocationCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: locationCountryText ?? "", 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 += locationCountryValueLayoutAndApply?.0.size.height ?? 0
|
|
|
|
maxTitleWidth = max(maxTitleWidth, (locationCountryTitleLayoutAndApply?.0.size.width ?? 0))
|
|
maxValueWidth = max(maxValueWidth, (locationCountryValueLayoutAndApply?.0.size.width ?? 0))
|
|
} else {
|
|
locationCountryTitleLayoutAndApply = nil
|
|
locationCountryValueLayoutAndApply = nil
|
|
}
|
|
|
|
backgroundSize.width = horizontalContentInset * 3.0 + maxTitleWidth + attributeSpacing + maxValueWidth
|
|
|
|
let (groupsLayout, groupsApply) = makeGroupsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No groups in common", 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 += verticalSpacing * 2.0 + paragraphSpacing
|
|
backgroundSize.height += groupsLayout.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.theme = item.presentationData.theme
|
|
|
|
if item.presentationData.theme.theme.overallDarkAppearance {
|
|
strongSelf.registrationDateTitleTextNode.layer.compositingFilter = nil
|
|
strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = nil
|
|
strongSelf.locationCountryTitleTextNode.layer.compositingFilter = nil
|
|
strongSelf.subtitleNode.layer.compositingFilter = nil
|
|
strongSelf.groupsTextNode.layer.compositingFilter = nil
|
|
} else {
|
|
strongSelf.registrationDateTitleTextNode.layer.compositingFilter = "overlayBlendMode"
|
|
strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode"
|
|
strongSelf.locationCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode"
|
|
strongSelf.subtitleNode.layer.compositingFilter = "overlayBlendMode"
|
|
strongSelf.groupsTextNode.layer.compositingFilter = "overlayBlendMode"
|
|
}
|
|
|
|
strongSelf.registrationDateText = registrationDateText
|
|
strongSelf.phoneCountryText = phoneCountryText
|
|
strongSelf.locationCountryText = locationCountryText
|
|
|
|
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?) {
|
|
if let titleLayout, let valueLayout {
|
|
let totalWidth = titleLayout.size.width + attributeSpacing + valueLayout.size.width
|
|
let titleOffset = titleLayout.size.width + attributeSpacing / 2.0
|
|
let midpoint = (backgroundSize.width - totalWidth) / 2.0 + titleOffset
|
|
attributeMidpoints.append(midpoint)
|
|
}
|
|
}
|
|
appendAttributeMidpoint(titleLayout: registrationDateTitleLayoutAndApply?.0, valueLayout: registrationDateValueLayoutAndApply?.0)
|
|
appendAttributeMidpoint(titleLayout: phoneCountryTitleLayoutAndApply?.0, valueLayout: phoneCountryValueLayoutAndApply?.0)
|
|
appendAttributeMidpoint(titleLayout: locationCountryTitleLayoutAndApply?.0, valueLayout: locationCountryValueLayoutAndApply?.0)
|
|
|
|
let middleX = floorToScreenPixels(attributeMidpoints.isEmpty ? backgroundSize.width / 2.0 : attributeMidpoints.reduce(0, +) / CGFloat(attributeMidpoints.count))
|
|
|
|
let titleMaxX: CGFloat = backgroundFrame.minX + middleX - attributeSpacing / 2.0
|
|
let valueMinX: CGFloat = backgroundFrame.minX + middleX + attributeSpacing / 2.0
|
|
|
|
func positionAttributeNodes(
|
|
titleTextNode: TextNode,
|
|
valueTextNode: TextNode,
|
|
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, y: contentOriginY),
|
|
size: valueLayout.size
|
|
)
|
|
contentOriginY += valueLayout.size.height + verticalSpacing
|
|
}
|
|
}
|
|
|
|
positionAttributeNodes(
|
|
titleTextNode: strongSelf.registrationDateTitleTextNode,
|
|
valueTextNode: strongSelf.registrationDateValueTextNode,
|
|
titleLayoutAndApply: registrationDateTitleLayoutAndApply,
|
|
valueLayoutAndApply: registrationDateValueLayoutAndApply
|
|
)
|
|
positionAttributeNodes(
|
|
titleTextNode: strongSelf.phoneCountryTitleTextNode,
|
|
valueTextNode: strongSelf.phoneCountryValueTextNode,
|
|
titleLayoutAndApply: phoneCountryTitleLayoutAndApply,
|
|
valueLayoutAndApply: phoneCountryValueLayoutAndApply
|
|
)
|
|
positionAttributeNodes(
|
|
titleTextNode: strongSelf.locationCountryTitleTextNode,
|
|
valueTextNode: strongSelf.locationCountryValueTextNode,
|
|
titleLayoutAndApply: locationCountryTitleLayoutAndApply,
|
|
valueLayoutAndApply: locationCountryValueLayoutAndApply
|
|
)
|
|
|
|
contentOriginY += verticalSpacing + paragraphSpacing
|
|
let _ = groupsApply()
|
|
let groupsFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - groupsLayout.size.width) / 2.0), y: contentOriginY), size: groupsLayout.size)
|
|
strongSelf.groupsTextNode.frame = groupsFrame
|
|
|
|
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
|
|
}
|
|
|
|
// public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
|
// let textNodeFrame = self.textNode.frame
|
|
// if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) {
|
|
// if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
// var concealed = true
|
|
// if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
|
// concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
|
// }
|
|
// return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
|
|
// } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
|
// return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
|
|
// } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
// return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
|
|
// } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
|
|
// return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
|
|
// } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
// return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
|
|
// } else {
|
|
// return ChatMessageBubbleContentTapAction(content: .none)
|
|
// }
|
|
// } else {
|
|
// return ChatMessageBubbleContentTapAction(content: .none)
|
|
// }
|
|
// }
|
|
|
|
// @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
// switch recognizer.state {
|
|
// case .ended:
|
|
// if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
// switch gesture {
|
|
// case .tap:
|
|
// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
|
|
// switch tapAction.content {
|
|
// case .none, .ignore:
|
|
// break
|
|
// case let .url(url):
|
|
// self.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, progress: tapAction.activate?()))
|
|
// case let .peerMention(peerId, _, _):
|
|
// if let item = self.item {
|
|
// let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
|
// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
|
// if let peer = peer {
|
|
// self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
|
|
// }
|
|
// })
|
|
// }
|
|
// case let .textMention(name):
|
|
// self.item?.controllerInteraction.openPeerMention(name, tapAction.activate?())
|
|
// case let .botCommand(command):
|
|
// self.item?.controllerInteraction.sendBotCommand(nil, command)
|
|
// case let .hashtag(peerName, hashtag):
|
|
// self.item?.controllerInteraction.openHashtag(peerName, hashtag)
|
|
// default:
|
|
// break
|
|
// }
|
|
// case .longTap, .doubleTap:
|
|
// if let item = self.item, self.backgroundNode.frame.contains(location) {
|
|
// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
|
|
// switch tapAction.content {
|
|
// case .none, .ignore:
|
|
// break
|
|
// case let .url(url):
|
|
// item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams())
|
|
// case let .peerMention(peerId, mention, _):
|
|
// item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams())
|
|
// case let .textMention(name):
|
|
// item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams())
|
|
// case let .botCommand(command):
|
|
// item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams())
|
|
// case let .hashtag(_, hashtag):
|
|
// item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams())
|
|
// default:
|
|
// break
|
|
// }
|
|
// }
|
|
// default:
|
|
// break
|
|
// }
|
|
// }
|
|
// default:
|
|
// break
|
|
// }
|
|
// }
|
|
}
|