mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
332 lines
15 KiB
Swift
332 lines
15 KiB
Swift
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import TextFormat
|
|
|
|
enum PeerInfoScreenLabeledValueTextColor {
|
|
case primary
|
|
case accent
|
|
}
|
|
|
|
enum PeerInfoScreenLabeledValueTextBehavior: Equatable {
|
|
case singleLine
|
|
case multiLine(maxLines: Int, enabledEntities: EnabledEntityTypes)
|
|
}
|
|
|
|
final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem {
|
|
let id: AnyHashable
|
|
let label: String
|
|
let text: String
|
|
let textColor: PeerInfoScreenLabeledValueTextColor
|
|
let textBehavior: PeerInfoScreenLabeledValueTextBehavior
|
|
let action: (() -> Void)?
|
|
let longTapAction: ((ASDisplayNode) -> Void)?
|
|
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
|
|
let requestLayout: () -> Void
|
|
|
|
init(
|
|
id: AnyHashable,
|
|
label: String,
|
|
text: String,
|
|
textColor: PeerInfoScreenLabeledValueTextColor = .primary,
|
|
textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine,
|
|
action: (() -> Void)?,
|
|
longTapAction: ((ASDisplayNode) -> Void)? = nil,
|
|
linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil,
|
|
requestLayout: @escaping () -> Void
|
|
) {
|
|
self.id = id
|
|
self.label = label
|
|
self.text = text
|
|
self.textColor = textColor
|
|
self.textBehavior = textBehavior
|
|
self.action = action
|
|
self.longTapAction = longTapAction
|
|
self.linkItemAction = linkItemAction
|
|
self.requestLayout = requestLayout
|
|
}
|
|
|
|
func node() -> PeerInfoScreenItemNode {
|
|
return PeerInfoScreenLabeledValueItemNode()
|
|
}
|
|
}
|
|
|
|
private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
|
|
private let labelNode: ImmediateTextNode
|
|
private let textNode: ImmediateTextNode
|
|
private let bottomSeparatorNode: ASDisplayNode
|
|
|
|
private let expandNode: ImmediateTextNode
|
|
private let expandButonNode: HighlightTrackingButtonNode
|
|
|
|
private var linkHighlightingNode: LinkHighlightingNode?
|
|
|
|
private var item: PeerInfoScreenLabeledValueItem?
|
|
private var theme: PresentationTheme?
|
|
|
|
private var isExpanded: Bool = false
|
|
|
|
override init() {
|
|
var bringToFrontForHighlightImpl: (() -> Void)?
|
|
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
|
|
self.selectionNode.isUserInteractionEnabled = false
|
|
|
|
self.labelNode = ImmediateTextNode()
|
|
self.labelNode.displaysAsynchronously = false
|
|
self.labelNode.isUserInteractionEnabled = false
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.displaysAsynchronously = false
|
|
self.textNode.isUserInteractionEnabled = false
|
|
|
|
self.bottomSeparatorNode = ASDisplayNode()
|
|
self.bottomSeparatorNode.isLayerBacked = true
|
|
|
|
self.expandNode = ImmediateTextNode()
|
|
self.expandNode.displaysAsynchronously = false
|
|
self.expandNode.isUserInteractionEnabled = false
|
|
|
|
self.expandButonNode = HighlightTrackingButtonNode()
|
|
|
|
super.init()
|
|
|
|
bringToFrontForHighlightImpl = { [weak self] in
|
|
self?.bringToFrontForHighlight?()
|
|
}
|
|
|
|
self.addSubnode(self.bottomSeparatorNode)
|
|
self.addSubnode(self.selectionNode)
|
|
self.addSubnode(self.labelNode)
|
|
self.addSubnode(self.textNode)
|
|
|
|
self.addSubnode(self.expandNode)
|
|
self.addSubnode(self.expandButonNode)
|
|
|
|
self.expandButonNode.addTarget(self, action: #selector(self.expandPressed), forControlEvents: .touchUpInside)
|
|
self.expandButonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.expandNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.expandNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.expandNode.alpha = 1.0
|
|
strongSelf.expandNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func expandPressed() {
|
|
self.isExpanded = true
|
|
self.item?.requestLayout()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
recognizer.tapActionAtPoint = { [weak self] point in
|
|
guard let strongSelf = self, let item = strongSelf.item else {
|
|
return .keepWithSingleTap
|
|
}
|
|
if !strongSelf.expandButonNode.isHidden, strongSelf.expandButonNode.view.hitTest(strongSelf.view.convert(point, to: strongSelf.expandButonNode.view), with: nil) != nil {
|
|
return .fail
|
|
}
|
|
if let _ = strongSelf.linkItemAtPoint(point) {
|
|
return .waitForSingleTap
|
|
}
|
|
if item.longTapAction != nil {
|
|
return .waitForSingleTap
|
|
}
|
|
if item.action != nil {
|
|
return .keepWithSingleTap
|
|
}
|
|
return .fail
|
|
}
|
|
recognizer.highlight = { [weak self] point in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateTouchesAtPoint(point)
|
|
}
|
|
self.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .ended:
|
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
switch gesture {
|
|
case .tap, .longTap:
|
|
if let item = self.item {
|
|
if let linkItem = self.linkItemAtPoint(location) {
|
|
item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem)
|
|
} else if case .longTap = gesture {
|
|
item.longTapAction?(self)
|
|
} else if case .tap = gesture {
|
|
item.action?()
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
|
|
guard let item = item as? PeerInfoScreenLabeledValueItem else {
|
|
return 10.0
|
|
}
|
|
|
|
self.item = item
|
|
self.theme = presentationData.theme
|
|
|
|
self.selectionNode.pressed = item.action
|
|
|
|
let sideInset: CGFloat = 16.0
|
|
|
|
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
|
|
|
let textColorValue: UIColor
|
|
switch item.textColor {
|
|
case .primary:
|
|
textColorValue = presentationData.theme.list.itemPrimaryTextColor
|
|
case .accent:
|
|
textColorValue = presentationData.theme.list.itemAccentColor
|
|
}
|
|
|
|
self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor)
|
|
let expandSize = self.expandNode.updateLayout(CGSize(width: width, height: 100.0))
|
|
|
|
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
|
|
|
switch item.textBehavior {
|
|
case .singleLine:
|
|
self.textNode.cutout = nil
|
|
self.textNode.maximumNumberOfLines = 1
|
|
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
|
|
case let .multiLine(maxLines, enabledEntities):
|
|
self.textNode.maximumNumberOfLines = self.isExpanded ? maxLines : 3
|
|
self.textNode.cutout = self.isExpanded ? nil : TextNodeCutout(bottomRight: CGSize(width: expandSize.width + 4.0, height: expandSize.height))
|
|
if enabledEntities.isEmpty {
|
|
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
|
|
} else {
|
|
let fontSize: CGFloat = 17.0
|
|
|
|
var baseFont = Font.regular(fontSize)
|
|
var linkFont = baseFont
|
|
var boldFont = Font.medium(fontSize)
|
|
var italicFont = Font.italic(fontSize)
|
|
var boldItalicFont = Font.semiboldItalic(fontSize)
|
|
let titleFixedFont = Font.monospace(fontSize)
|
|
|
|
let entities = generateTextEntities(item.text, enabledTypes: enabledEntities)
|
|
self.textNode.attributedText = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColorValue, linkColor: presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont)
|
|
}
|
|
}
|
|
|
|
let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
|
let textLayout = self.textNode.updateLayoutInfo(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
|
let textSize = textLayout.size
|
|
|
|
if case .multiLine = item.textBehavior, textLayout.truncated, !self.isExpanded {
|
|
self.expandNode.isHidden = false
|
|
self.expandButonNode.isHidden = false
|
|
} else {
|
|
self.expandNode.isHidden = true
|
|
self.expandButonNode.isHidden = true
|
|
}
|
|
|
|
let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: labelSize)
|
|
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: labelFrame.maxY + 3.0), size: textSize)
|
|
|
|
let expandFrame = CGRect(origin: CGPoint(x: textFrame.minX + max(self.textNode.trailingLineWidth ?? 0.0, textFrame.width) - expandSize.width, y: textFrame.maxY - expandSize.height), size: expandSize)
|
|
self.expandNode.frame = expandFrame
|
|
self.expandButonNode.frame = expandFrame.insetBy(dx: -8.0, dy: -8.0)
|
|
|
|
transition.updateFrame(node: self.labelNode, frame: labelFrame)
|
|
transition.updateFrame(node: self.textNode, frame: textFrame)
|
|
|
|
let height = labelSize.height + 3.0 + textSize.height + 22.0
|
|
|
|
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
|
|
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
|
|
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
|
|
|
|
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
|
|
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
|
|
|
|
return height
|
|
}
|
|
|
|
private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? {
|
|
let textNodeFrame = self.textNode.frame
|
|
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
return .url(url: url, concealed: false)
|
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
return .mention(peerName)
|
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
return .hashtag(hashtag.peerName, hashtag.hashtag)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func updateTouchesAtPoint(_ point: CGPoint?) {
|
|
guard let item = self.item, let theme = self.theme else {
|
|
return
|
|
}
|
|
var rects: [CGRect]?
|
|
if let point = point {
|
|
let textNodeFrame = self.textNode.frame
|
|
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
|
let possibleNames: [String] = [
|
|
TelegramTextAttributes.URL,
|
|
TelegramTextAttributes.PeerMention,
|
|
TelegramTextAttributes.PeerTextMention,
|
|
TelegramTextAttributes.BotCommand,
|
|
TelegramTextAttributes.Hashtag
|
|
]
|
|
for name in possibleNames {
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
|
|
rects = self.textNode.attributeRects(name: name, at: index)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let rects = rects {
|
|
let linkHighlightingNode: LinkHighlightingNode
|
|
if let current = self.linkHighlightingNode {
|
|
linkHighlightingNode = current
|
|
} else {
|
|
linkHighlightingNode = LinkHighlightingNode(color: theme.list.itemAccentColor.withAlphaComponent(0.5))
|
|
self.linkHighlightingNode = linkHighlightingNode
|
|
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
|
|
}
|
|
linkHighlightingNode.frame = self.textNode.frame
|
|
linkHighlightingNode.updateRects(rects)
|
|
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
|
self.linkHighlightingNode = nil
|
|
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
|
linkHighlightingNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
if point != nil && rects == nil && item.action != nil {
|
|
self.selectionNode.updateIsHighlighted(true)
|
|
} else {
|
|
self.selectionNode.updateIsHighlighted(false)
|
|
}
|
|
}
|
|
}
|