Swiftgram/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift
2020-06-02 16:38:24 +04:00

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)
}
}
}