mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
User profile info improvements
This commit is contained in:
parent
73987dff5d
commit
d677ee44bb
@ -0,0 +1,401 @@
|
|||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import TextFormat
|
||||||
|
import UIKit
|
||||||
|
import AppBundle
|
||||||
|
import TelegramStringFormatting
|
||||||
|
import ContextUI
|
||||||
|
|
||||||
|
final class PeerInfoScreenContactInfoItem: PeerInfoScreenItem {
|
||||||
|
let id: AnyHashable
|
||||||
|
let username: String
|
||||||
|
let phoneNumber: String
|
||||||
|
let additionalText: String?
|
||||||
|
let usernameAction: ((ASDisplayNode) -> Void)?
|
||||||
|
let usernameLongTapAction: ((ASDisplayNode) -> Void)?
|
||||||
|
let phoneAction: ((ASDisplayNode) -> Void)?
|
||||||
|
let phoneLongTapAction: ((ASDisplayNode) -> Void)?
|
||||||
|
let linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)?
|
||||||
|
let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||||
|
let requestLayout: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: AnyHashable,
|
||||||
|
username: String,
|
||||||
|
phoneNumber: String,
|
||||||
|
additionalText: String? = nil,
|
||||||
|
usernameAction: ((ASDisplayNode) -> Void)?,
|
||||||
|
usernameLongTapAction: ((ASDisplayNode) -> Void)?,
|
||||||
|
phoneAction: ((ASDisplayNode) -> Void)?,
|
||||||
|
phoneLongTapAction: ((ASDisplayNode) -> Void)?,
|
||||||
|
linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)? = nil,
|
||||||
|
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil,
|
||||||
|
requestLayout: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.username = username
|
||||||
|
self.phoneNumber = phoneNumber
|
||||||
|
self.additionalText = additionalText
|
||||||
|
self.usernameAction = usernameAction
|
||||||
|
self.usernameLongTapAction = usernameLongTapAction
|
||||||
|
self.phoneAction = phoneAction
|
||||||
|
self.phoneLongTapAction = phoneLongTapAction
|
||||||
|
self.linkItemAction = linkItemAction
|
||||||
|
self.contextAction = contextAction
|
||||||
|
self.requestLayout = requestLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
func node() -> PeerInfoScreenItemNode {
|
||||||
|
return PeerInfoScreenContactInfoItemNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode {
|
||||||
|
private let containerNode: ContextControllerSourceNode
|
||||||
|
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||||
|
|
||||||
|
private let extractedBackgroundImageNode: ASImageNode
|
||||||
|
|
||||||
|
private var extractedRect: CGRect?
|
||||||
|
private var nonExtractedRect: CGRect?
|
||||||
|
|
||||||
|
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
|
||||||
|
private let maskNode: ASImageNode
|
||||||
|
private let usernameNode: ImmediateTextNode
|
||||||
|
private let phoneNumberNode: ImmediateTextNode
|
||||||
|
private let additionalTextNode: ImmediateTextNode
|
||||||
|
private let measureTextNode: ImmediateTextNode
|
||||||
|
private let bottomSeparatorNode: ASDisplayNode
|
||||||
|
|
||||||
|
private var linkHighlightingNode: LinkHighlightingNode?
|
||||||
|
|
||||||
|
private let activateArea: AccessibilityAreaNode
|
||||||
|
|
||||||
|
private var item: PeerInfoScreenContactInfoItem?
|
||||||
|
private var theme: PresentationTheme?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
var bringToFrontForHighlightImpl: (() -> Void)?
|
||||||
|
|
||||||
|
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||||
|
self.containerNode = ContextControllerSourceNode()
|
||||||
|
|
||||||
|
self.extractedBackgroundImageNode = ASImageNode()
|
||||||
|
self.extractedBackgroundImageNode.displaysAsynchronously = false
|
||||||
|
self.extractedBackgroundImageNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
|
||||||
|
self.selectionNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.maskNode = ASImageNode()
|
||||||
|
self.maskNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.usernameNode = ImmediateTextNode()
|
||||||
|
self.usernameNode.displaysAsynchronously = false
|
||||||
|
self.usernameNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.phoneNumberNode = ImmediateTextNode()
|
||||||
|
self.phoneNumberNode.displaysAsynchronously = false
|
||||||
|
self.phoneNumberNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.additionalTextNode = ImmediateTextNode()
|
||||||
|
self.additionalTextNode.displaysAsynchronously = false
|
||||||
|
self.additionalTextNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.measureTextNode = ImmediateTextNode()
|
||||||
|
self.measureTextNode.displaysAsynchronously = false
|
||||||
|
self.measureTextNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.bottomSeparatorNode = ASDisplayNode()
|
||||||
|
self.bottomSeparatorNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.activateArea = AccessibilityAreaNode()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
bringToFrontForHighlightImpl = { [weak self] in
|
||||||
|
self?.bringToFrontForHighlight?()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addSubnode(self.bottomSeparatorNode)
|
||||||
|
self.addSubnode(self.selectionNode)
|
||||||
|
|
||||||
|
self.containerNode.addSubnode(self.contextSourceNode)
|
||||||
|
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||||
|
self.addSubnode(self.containerNode)
|
||||||
|
|
||||||
|
self.addSubnode(self.maskNode)
|
||||||
|
|
||||||
|
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
|
||||||
|
|
||||||
|
self.contextSourceNode.contentNode.addSubnode(self.usernameNode)
|
||||||
|
self.contextSourceNode.contentNode.addSubnode(self.phoneNumberNode)
|
||||||
|
self.contextSourceNode.contentNode.addSubnode(self.additionalTextNode)
|
||||||
|
|
||||||
|
|
||||||
|
self.addSubnode(self.activateArea)
|
||||||
|
|
||||||
|
self.containerNode.activated = { [weak self] gesture, _ in
|
||||||
|
guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else {
|
||||||
|
gesture.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contextAction(strongSelf.contextSourceNode, gesture, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||||
|
guard let strongSelf = self, let theme = strongSelf.theme else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExtracted {
|
||||||
|
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: theme.list.plainBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
|
||||||
|
let rect = isExtracted ? extractedRect : nonExtractedRect
|
||||||
|
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
|
||||||
|
if !isExtracted {
|
||||||
|
self?.extractedBackgroundImageNode.image = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||||
|
recognizer.tapActionAtPoint = { [weak self] point in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return .keepWithSingleTap
|
||||||
|
}
|
||||||
|
if let _ = strongSelf.linkItemAtPoint(point) {
|
||||||
|
return .waitForSingleTap
|
||||||
|
}
|
||||||
|
return .waitForSingleTap
|
||||||
|
}
|
||||||
|
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, self.linkHighlightingNode ?? self, self.linkHighlightingNode?.rects.first)
|
||||||
|
} else if case .longTap = gesture {
|
||||||
|
if self.usernameNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
|
||||||
|
item.usernameLongTapAction?(self.usernameNode)
|
||||||
|
} else if self.phoneNumberNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
|
||||||
|
item.phoneLongTapAction?(self.phoneNumberNode)
|
||||||
|
}
|
||||||
|
} else if case .tap = gesture {
|
||||||
|
if self.usernameNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
|
||||||
|
item.usernameAction?(self.contextSourceNode)
|
||||||
|
} else if self.phoneNumberNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(location) {
|
||||||
|
item.phoneAction?(self.contextSourceNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? {
|
||||||
|
let additionalTextNodeFrame = self.additionalTextNode.frame
|
||||||
|
if let (_, attributes) = self.additionalTextNode.attributesAtPoint(CGPoint(x: point.x - additionalTextNodeFrame.minX, y: point.y - additionalTextNodeFrame.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
|
||||||
|
}
|
||||||
|
|
||||||
|
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||||
|
guard let item = item as? PeerInfoScreenContactInfoItem else {
|
||||||
|
return 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.item = item
|
||||||
|
self.theme = presentationData.theme
|
||||||
|
|
||||||
|
// if let action = item.action {
|
||||||
|
// self.selectionNode.pressed = { [weak self] in
|
||||||
|
// if let strongSelf = self {
|
||||||
|
// action(strongSelf.contextSourceNode)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// self.selectionNode.pressed = nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 16.0 + safeInsets.left
|
||||||
|
|
||||||
|
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
|
|
||||||
|
self.usernameNode.attributedText = NSAttributedString(string: item.username, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
||||||
|
|
||||||
|
self.phoneNumberNode.maximumNumberOfLines = 1
|
||||||
|
self.phoneNumberNode.cutout = nil
|
||||||
|
self.phoneNumberNode.attributedText = NSAttributedString(string: item.phoneNumber, font: Font.regular(15.0), textColor: presentationData.theme.list.itemAccentColor)
|
||||||
|
|
||||||
|
let fontSize: CGFloat = 15.0
|
||||||
|
|
||||||
|
let baseFont = Font.regular(fontSize)
|
||||||
|
let linkFont = baseFont
|
||||||
|
let boldFont = Font.medium(fontSize)
|
||||||
|
let italicFont = Font.italic(fontSize)
|
||||||
|
let boldItalicFont = Font.semiboldItalic(fontSize)
|
||||||
|
let titleFixedFont = Font.monospace(fontSize)
|
||||||
|
|
||||||
|
if let additionalText = item.additionalText {
|
||||||
|
let entities = generateTextEntities(additionalText, enabledTypes: [.mention])
|
||||||
|
let attributedAdditionalText = stringWithAppliedEntities(additionalText, entities: entities, baseColor: presentationData.theme.list.itemPrimaryTextColor, linkColor: presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont, underlineLinks: false, message: nil)
|
||||||
|
|
||||||
|
self.additionalTextNode.maximumNumberOfLines = 10
|
||||||
|
self.additionalTextNode.attributedText = attributedAdditionalText
|
||||||
|
} else {
|
||||||
|
self.additionalTextNode.attributedText = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let usernameSize = self.usernameNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
||||||
|
let phoneSize = self.phoneNumberNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
||||||
|
|
||||||
|
let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
||||||
|
|
||||||
|
let topOffset = 12.0
|
||||||
|
var height = topOffset * 2.0
|
||||||
|
let usernameFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: usernameSize)
|
||||||
|
let phoneFrame = CGRect(origin: CGPoint(x: usernameSize.width > 0.0 ? width - sideInset - phoneSize.width : sideInset, y: topOffset), size: phoneSize)
|
||||||
|
height += max(usernameSize.height, phoneSize.height)
|
||||||
|
|
||||||
|
let additionalTextFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: additionalTextSize)
|
||||||
|
transition.updateFrame(node: self.usernameNode, frame: usernameFrame)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.phoneNumberNode, frame: phoneFrame)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.additionalTextNode, frame: additionalTextFrame)
|
||||||
|
|
||||||
|
if additionalTextSize.height > 0.0 {
|
||||||
|
height += additionalTextSize.height + 3.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)
|
||||||
|
|
||||||
|
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
|
||||||
|
let hasTopCorners = hasCorners && topItem == nil
|
||||||
|
let hasBottomCorners = hasCorners && bottomItem == nil
|
||||||
|
|
||||||
|
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||||
|
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
|
||||||
|
self.bottomSeparatorNode.isHidden = hasBottomCorners
|
||||||
|
|
||||||
|
self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))
|
||||||
|
self.activateArea.accessibilityLabel = item.username
|
||||||
|
self.activateArea.accessibilityValue = item.phoneNumber
|
||||||
|
|
||||||
|
let contentSize = CGSize(width: width, height: height)
|
||||||
|
self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||||
|
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||||
|
self.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||||
|
self.containerNode.isGestureEnabled = item.contextAction != nil
|
||||||
|
|
||||||
|
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height))
|
||||||
|
let extractedRect = nonExtractedRect
|
||||||
|
self.extractedRect = extractedRect
|
||||||
|
self.nonExtractedRect = nonExtractedRect
|
||||||
|
|
||||||
|
if self.contextSourceNode.isExtractedToContextPreview {
|
||||||
|
self.extractedBackgroundImageNode.frame = extractedRect
|
||||||
|
} else {
|
||||||
|
self.extractedBackgroundImageNode.frame = nonExtractedRect
|
||||||
|
}
|
||||||
|
self.contextSourceNode.contentRect = extractedRect
|
||||||
|
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTouchesAtPoint(_ point: CGPoint?) {
|
||||||
|
guard let _ = self.item, let theme = self.theme else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rects: [CGRect]?
|
||||||
|
var textNode: ASDisplayNode?
|
||||||
|
if let point = point {
|
||||||
|
if self.usernameNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(point) {
|
||||||
|
textNode = self.usernameNode
|
||||||
|
rects = [self.usernameNode.bounds]
|
||||||
|
} else if self.phoneNumberNode.frame.insetBy(dx: -10.0, dy: -10.0).contains(point) {
|
||||||
|
textNode = self.phoneNumberNode
|
||||||
|
rects = [self.phoneNumberNode.bounds]
|
||||||
|
} else if self.additionalTextNode.frame.contains(point) {
|
||||||
|
let mappedPoint = CGPoint(x: point.x - self.additionalTextNode.frame.minX, y: point.y - self.additionalTextNode.frame.minY)
|
||||||
|
if mappedPoint.y > 0.0, let (index, attributes) = self.additionalTextNode.attributesAtPoint(mappedPoint) {
|
||||||
|
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.additionalTextNode.attributeRects(name: name, at: index)
|
||||||
|
textNode = self.additionalTextNode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let rects = rects, let textNode = textNode {
|
||||||
|
let linkHighlightingNode: LinkHighlightingNode
|
||||||
|
if let current = self.linkHighlightingNode {
|
||||||
|
linkHighlightingNode = current
|
||||||
|
} else {
|
||||||
|
linkHighlightingNode = LinkHighlightingNode(color: theme.list.itemAccentColor.withAlphaComponent(0.5))
|
||||||
|
self.linkHighlightingNode = linkHighlightingNode
|
||||||
|
self.contextSourceNode.contentNode.insertSubnode(linkHighlightingNode, belowSubnode: textNode)
|
||||||
|
}
|
||||||
|
linkHighlightingNode.frame = 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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -472,9 +472,19 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
self.expandButonNode.isHidden = true
|
self.expandButonNode.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: labelSize)
|
var topOffset = 11.0
|
||||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: labelFrame.maxY + 3.0), size: textSize)
|
var height = topOffset * 2.0
|
||||||
let additionalTextFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 3.0), size: additionalTextSize)
|
let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: labelSize)
|
||||||
|
if labelSize.height > 0.0 {
|
||||||
|
topOffset += labelSize.height + 3.0
|
||||||
|
height += labelSize.height + 3.0
|
||||||
|
}
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: textSize)
|
||||||
|
if textSize.height > 0.0 {
|
||||||
|
topOffset += textSize.height + 3.0
|
||||||
|
height += textSize.height
|
||||||
|
}
|
||||||
|
let additionalTextFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: additionalTextSize)
|
||||||
|
|
||||||
let expandFrame = CGRect(origin: CGPoint(x: width - safeInsets.right - expandSize.width - 14.0 - UIScreenPixel, y: textFrame.maxY - expandSize.height), size: expandSize)
|
let expandFrame = CGRect(origin: CGPoint(x: width - safeInsets.right - expandSize.width - 14.0 - UIScreenPixel, y: textFrame.maxY - expandSize.height), size: expandSize)
|
||||||
self.expandNode.frame = expandFrame
|
self.expandNode.frame = expandFrame
|
||||||
@ -496,8 +506,6 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
|
|
||||||
transition.updateFrame(node: self.additionalTextNode, frame: additionalTextFrame)
|
transition.updateFrame(node: self.additionalTextNode, frame: additionalTextFrame)
|
||||||
|
|
||||||
var height = labelSize.height + 3.0 + textSize.height + 22.0
|
|
||||||
|
|
||||||
let iconButtonFrame = CGRect(x: width - safeInsets.right - height, y: 0.0, width: height, height: height)
|
let iconButtonFrame = CGRect(x: width - safeInsets.right - height, y: 0.0, width: height, height: height)
|
||||||
transition.updateFrame(node: self.iconButtonNode, frame: iconButtonFrame)
|
transition.updateFrame(node: self.iconButtonNode, frame: iconButtonFrame)
|
||||||
if let iconSize = self.iconNode.image?.size {
|
if let iconSize = self.iconNode.image?.size {
|
||||||
|
@ -1142,10 +1142,18 @@ func peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: PeerInfoHeaderButtonKe
|
|||||||
return hiddenWhileExpanded
|
return hiddenWhileExpanded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func peerInfoHeaderActionButtons(peer: Peer?, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] {
|
||||||
|
var result: [PeerInfoHeaderButtonKey] = []
|
||||||
|
if !isContact && !isSecretChat, let user = peer as? TelegramUser, user.botInfo == nil {
|
||||||
|
result = [.message, .addContact]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool, threadInfo: EngineMessageHistoryThread.Info?) -> [PeerInfoHeaderButtonKey] {
|
func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool, threadInfo: EngineMessageHistoryThread.Info?) -> [PeerInfoHeaderButtonKey] {
|
||||||
var result: [PeerInfoHeaderButtonKey] = []
|
var result: [PeerInfoHeaderButtonKey] = []
|
||||||
if let user = peer as? TelegramUser {
|
if let user = peer as? TelegramUser {
|
||||||
if !isOpenedFromChat {
|
if !isOpenedFromChat && isContact {
|
||||||
result.append(.message)
|
result.append(.message)
|
||||||
}
|
}
|
||||||
var callsAvailable = false
|
var callsAvailable = false
|
||||||
|
@ -44,6 +44,7 @@ enum PeerInfoHeaderButtonKey: Hashable {
|
|||||||
case search
|
case search
|
||||||
case leave
|
case leave
|
||||||
case stop
|
case stop
|
||||||
|
case addContact
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PeerInfoHeaderButtonIcon {
|
enum PeerInfoHeaderButtonIcon {
|
||||||
@ -135,7 +136,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
|||||||
self.action(self, nil)
|
self.action(self, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
||||||
let previousIcon = self.icon
|
let previousIcon = self.icon
|
||||||
let themeUpdated = self.theme != presentationData.theme
|
let themeUpdated = self.theme != presentationData.theme
|
||||||
let iconUpdated = self.icon != icon
|
let iconUpdated = self.icon != icon
|
||||||
@ -288,6 +289,88 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class PeerInfoHeaderActionButtonNode: HighlightableButtonNode {
|
||||||
|
let key: PeerInfoHeaderButtonKey
|
||||||
|
private let action: (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void
|
||||||
|
let referenceNode: ContextReferenceContentNode
|
||||||
|
let containerNode: ContextControllerSourceNode
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
|
private let textNode: ImmediateTextNode
|
||||||
|
|
||||||
|
private var theme: PresentationTheme?
|
||||||
|
|
||||||
|
init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderActionButtonNode, ContextGesture?) -> Void) {
|
||||||
|
self.key = key
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
self.referenceNode = ContextReferenceContentNode()
|
||||||
|
self.containerNode = ContextControllerSourceNode()
|
||||||
|
self.containerNode.animateScale = false
|
||||||
|
|
||||||
|
self.backgroundNode = ASDisplayNode()
|
||||||
|
self.backgroundNode.cornerRadius = 11.0
|
||||||
|
|
||||||
|
self.textNode = ImmediateTextNode()
|
||||||
|
self.textNode.displaysAsynchronously = false
|
||||||
|
self.textNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.accessibilityTraits = .button
|
||||||
|
|
||||||
|
self.containerNode.addSubnode(self.referenceNode)
|
||||||
|
self.referenceNode.addSubnode(self.backgroundNode)
|
||||||
|
self.addSubnode(self.containerNode)
|
||||||
|
self.addSubnode(self.textNode)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if highlighted {
|
||||||
|
strongSelf.layer.removeAnimation(forKey: "opacity")
|
||||||
|
strongSelf.alpha = 0.4
|
||||||
|
} else {
|
||||||
|
strongSelf.alpha = 1.0
|
||||||
|
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.containerNode.activated = { [weak self] gesture, _ in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.action(strongSelf, gesture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func buttonPressed() {
|
||||||
|
self.action(self, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(size: CGSize, text: String, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
||||||
|
let themeUpdated = self.theme != presentationData.theme
|
||||||
|
if themeUpdated {
|
||||||
|
self.theme = presentationData.theme
|
||||||
|
|
||||||
|
self.containerNode.isGestureEnabled = false
|
||||||
|
|
||||||
|
self.backgroundNode.backgroundColor = presentationData.theme.list.itemAccentColor
|
||||||
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(16.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||||
|
self.accessibilityLabel = text
|
||||||
|
let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude))
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize))
|
||||||
|
|
||||||
|
self.referenceNode.frame = self.containerNode.bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class PeerInfoHeaderNavigationTransition {
|
final class PeerInfoHeaderNavigationTransition {
|
||||||
let sourceNavigationBar: NavigationBar
|
let sourceNavigationBar: NavigationBar
|
||||||
let sourceTitleView: ChatTitleView
|
let sourceTitleView: ChatTitleView
|
||||||
@ -2285,6 +2368,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
let usernameNodeContainer: ASDisplayNode
|
let usernameNodeContainer: ASDisplayNode
|
||||||
let usernameNodeRawContainer: ASDisplayNode
|
let usernameNodeRawContainer: ASDisplayNode
|
||||||
let usernameNode: MultiScaleTextNode
|
let usernameNode: MultiScaleTextNode
|
||||||
|
var actionButtonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderActionButtonNode] = [:]
|
||||||
var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:]
|
var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:]
|
||||||
let backgroundNode: NavigationBackgroundNode
|
let backgroundNode: NavigationBackgroundNode
|
||||||
let expandedBackgroundNode: NavigationBackgroundNode
|
let expandedBackgroundNode: NavigationBackgroundNode
|
||||||
@ -2834,6 +2918,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight)
|
let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight)
|
||||||
let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight)
|
let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight)
|
||||||
|
|
||||||
|
let actionButtonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderActionButtons(peer: peer, isSecretChat: isSecretChat, isContact: isContact)
|
||||||
let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact, threadInfo: threadData?.info)
|
let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact, threadInfo: threadData?.info)
|
||||||
|
|
||||||
var isPremium = false
|
var isPremium = false
|
||||||
@ -3497,14 +3582,69 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
|
|
||||||
let buttonSpacing: CGFloat = 8.0
|
let buttonSpacing: CGFloat = 8.0
|
||||||
let buttonSideInset = max(16.0, containerInset)
|
let buttonSideInset = max(16.0, containerInset)
|
||||||
var buttonRightOrigin = CGPoint(x: width - buttonSideInset, y: maxY + 25.0 - navigationHeight - UIScreenPixel)
|
|
||||||
|
var actionButtonRightOrigin = CGPoint(x: width - buttonSideInset, y: maxY + 24.0 - navigationHeight - UIScreenPixel)
|
||||||
|
let actionButtonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(actionButtonKeys.count) - buttonSpacing
|
||||||
|
let actionButtonSize = CGSize(width: actionButtonWidth, height: 40.0)
|
||||||
|
|
||||||
|
for buttonKey in actionButtonKeys.reversed() {
|
||||||
|
let buttonNode: PeerInfoHeaderActionButtonNode
|
||||||
|
var wasAdded = false
|
||||||
|
if let current = self.actionButtonNodes[buttonKey] {
|
||||||
|
buttonNode = current
|
||||||
|
} else {
|
||||||
|
wasAdded = true
|
||||||
|
buttonNode = PeerInfoHeaderActionButtonNode(key: buttonKey, action: { [weak self] buttonNode, gesture in
|
||||||
|
self?.actionButtonPressed(buttonNode, gesture: gesture)
|
||||||
|
})
|
||||||
|
self.actionButtonNodes[buttonKey] = buttonNode
|
||||||
|
self.buttonsContainerNode.addSubnode(buttonNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttonFrame = CGRect(origin: CGPoint(x: actionButtonRightOrigin.x - actionButtonSize.width, y: actionButtonRightOrigin.y), size: actionButtonSize)
|
||||||
|
let buttonTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition
|
||||||
|
|
||||||
|
if additive {
|
||||||
|
buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
|
||||||
|
} else {
|
||||||
|
buttonTransition.updateFrame(node: buttonNode, frame: buttonFrame)
|
||||||
|
}
|
||||||
|
let buttonText: String
|
||||||
|
switch buttonKey {
|
||||||
|
case .message:
|
||||||
|
buttonText = "Message"
|
||||||
|
case .addContact:
|
||||||
|
buttonText = "Add"
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonNode.update(size: buttonFrame.size, text: buttonText, presentationData: presentationData, transition: buttonTransition)
|
||||||
|
|
||||||
|
if wasAdded {
|
||||||
|
buttonNode.alpha = 0.0
|
||||||
|
}
|
||||||
|
transition.updateAlpha(node: buttonNode, alpha: 1.0)
|
||||||
|
actionButtonRightOrigin.x -= actionButtonSize.width + buttonSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in self.actionButtonNodes.keys {
|
||||||
|
if !actionButtonKeys.contains(key) {
|
||||||
|
if let buttonNode = self.actionButtonNodes[key] {
|
||||||
|
self.actionButtonNodes.removeValue(forKey: key)
|
||||||
|
transition.updateAlpha(node: buttonNode, alpha: 0.0) { [weak buttonNode] _ in
|
||||||
|
buttonNode?.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttonRightOrigin = CGPoint(x: width - buttonSideInset, y: maxY + 24.0 - navigationHeight - UIScreenPixel)
|
||||||
|
if !actionButtonKeys.isEmpty {
|
||||||
|
buttonRightOrigin.y += actionButtonSize.height + 24.0
|
||||||
|
}
|
||||||
let buttonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(buttonKeys.count) - buttonSpacing
|
let buttonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(buttonKeys.count) - buttonSpacing
|
||||||
|
let buttonSize = CGSize(width: buttonWidth, height: 58.0)
|
||||||
let apparentButtonSize = CGSize(width: buttonWidth, height: 58.0)
|
|
||||||
let buttonsAlpha: CGFloat = 1.0
|
|
||||||
let buttonsVerticalOffset: CGFloat = 0.0
|
|
||||||
|
|
||||||
let buttonsAlphaTransition = transition
|
|
||||||
|
|
||||||
for buttonKey in buttonKeys.reversed() {
|
for buttonKey in buttonKeys.reversed() {
|
||||||
let buttonNode: PeerInfoHeaderButtonNode
|
let buttonNode: PeerInfoHeaderButtonNode
|
||||||
@ -3520,14 +3660,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
self.buttonsContainerNode.addSubnode(buttonNode)
|
self.buttonsContainerNode.addSubnode(buttonNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonFrame = CGRect(origin: CGPoint(x: buttonRightOrigin.x - apparentButtonSize.width, y: buttonRightOrigin.y), size: apparentButtonSize)
|
let buttonFrame = CGRect(origin: CGPoint(x: buttonRightOrigin.x - buttonSize.width, y: buttonRightOrigin.y), size: buttonSize)
|
||||||
let buttonTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition
|
let buttonTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition
|
||||||
|
|
||||||
let apparentButtonFrame = buttonFrame.offsetBy(dx: 0.0, dy: buttonsVerticalOffset)
|
|
||||||
if additive {
|
if additive {
|
||||||
buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: apparentButtonFrame)
|
buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
|
||||||
} else {
|
} else {
|
||||||
buttonTransition.updateFrame(node: buttonNode, frame: apparentButtonFrame)
|
buttonTransition.updateFrame(node: buttonNode, frame: buttonFrame)
|
||||||
}
|
}
|
||||||
let buttonText: String
|
let buttonText: String
|
||||||
let buttonIcon: PeerInfoHeaderButtonIcon
|
let buttonIcon: PeerInfoHeaderButtonIcon
|
||||||
@ -3575,6 +3714,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
case .stop:
|
case .stop:
|
||||||
buttonText = presentationData.strings.PeerInfo_ButtonStop
|
buttonText = presentationData.strings.PeerInfo_ButtonStop
|
||||||
buttonIcon = .stop
|
buttonIcon = .stop
|
||||||
|
case .addContact:
|
||||||
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
var isActive = true
|
var isActive = true
|
||||||
@ -3582,12 +3723,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
isActive = buttonKey == highlightedButton
|
isActive = buttonKey == highlightedButton
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isActive: isActive, isExpanded: false, presentationData: presentationData, transition: buttonTransition)
|
buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isActive: isActive, presentationData: presentationData, transition: buttonTransition)
|
||||||
|
|
||||||
if wasAdded {
|
if wasAdded {
|
||||||
buttonNode.alpha = 0.0
|
buttonNode.alpha = 0.0
|
||||||
}
|
}
|
||||||
buttonsAlphaTransition.updateAlpha(node: buttonNode, alpha: buttonsAlpha)
|
transition.updateAlpha(node: buttonNode, alpha: 1.0)
|
||||||
|
|
||||||
if case .mute = buttonKey, buttonNode.containerNode.alpha.isZero, additive {
|
if case .mute = buttonKey, buttonNode.containerNode.alpha.isZero, additive {
|
||||||
if case let .animated(duration, curve) = transition {
|
if case let .animated(duration, curve) = transition {
|
||||||
@ -3598,7 +3739,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0)
|
transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0)
|
||||||
}
|
}
|
||||||
buttonRightOrigin.x -= apparentButtonSize.width + buttonSpacing
|
buttonRightOrigin.x -= buttonSize.width + buttonSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in self.buttonNodes.keys {
|
for key in self.buttonNodes.keys {
|
||||||
@ -3622,7 +3763,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
let backgroundFrame: CGRect
|
let backgroundFrame: CGRect
|
||||||
let separatorFrame: CGRect
|
let separatorFrame: CGRect
|
||||||
|
|
||||||
let resolvedHeight: CGFloat
|
var resolvedHeight: CGFloat
|
||||||
|
|
||||||
if state.isEditing {
|
if state.isEditing {
|
||||||
resolvedHeight = editingContentHeight
|
resolvedHeight = editingContentHeight
|
||||||
@ -3635,7 +3776,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transition.updateFrame(node: self.regularContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: resolvedHeight)))
|
transition.updateFrame(node: self.regularContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: resolvedHeight)))
|
||||||
transition.updateFrame(node: self.buttonsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + UIScreenPixel), size: CGSize(width: width, height: resolvedHeight - navigationHeight + 180.0)))
|
transition.updateFrame(node: self.buttonsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + UIScreenPixel), size: CGSize(width: width, height: resolvedHeight - navigationHeight + 500.0)))
|
||||||
|
|
||||||
if additive {
|
if additive {
|
||||||
transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame)
|
transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame)
|
||||||
@ -3651,6 +3792,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
transition.updateFrame(node: self.separatorNode, frame: separatorFrame)
|
transition.updateFrame(node: self.separatorNode, frame: separatorFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !state.isEditing && !isSettings {
|
||||||
|
resolvedHeight += 71.0
|
||||||
|
|
||||||
|
if !actionButtonKeys.isEmpty {
|
||||||
|
resolvedHeight += 64.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isFirstTime {
|
if isFirstTime {
|
||||||
self.updateAvatarMask(transition: .immediate)
|
self.updateAvatarMask(transition: .immediate)
|
||||||
}
|
}
|
||||||
@ -3662,6 +3811,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
self.performButtonAction?(buttonNode.key, gesture)
|
self.performButtonAction?(buttonNode.key, gesture)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func actionButtonPressed(_ buttonNode: PeerInfoHeaderActionButtonNode, gesture: ContextGesture?) {
|
||||||
|
self.performButtonAction?(buttonNode.key, gesture)
|
||||||
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
guard let result = super.hitTest(point, with: event) else {
|
guard let result = super.hitTest(point, with: event) else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -976,7 +976,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation) -> [(AnyHashable, [PeerInfoScreenItem])] {
|
private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] {
|
||||||
guard let data = data else {
|
guard let data = data else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -1005,39 +1005,63 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
items[.calls]!.append(PeerInfoScreenCallListItem(id: 20, messages: callMessages))
|
items[.calls]!.append(PeerInfoScreenCallListItem(id: 20, messages: callMessages))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let phone = user.phone {
|
var username: String?
|
||||||
let formattedPhone = formatPhoneNumber(context: context, number: phone)
|
var additionalUsernames: String?
|
||||||
let label: String
|
var phoneNumber: String?
|
||||||
if formattedPhone.hasPrefix("+888 ") {
|
|
||||||
label = presentationData.strings.UserInfo_AnonymousNumberLabel
|
|
||||||
} else {
|
|
||||||
label = presentationData.strings.ContactInfo_PhoneLabelMobile
|
|
||||||
}
|
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node in
|
|
||||||
interaction.openPhone(phone, node, nil)
|
|
||||||
}, longTapAction: nil, contextAction: { node, gesture, _ in
|
|
||||||
interaction.openPhone(phone, node, gesture)
|
|
||||||
}, requestLayout: {
|
|
||||||
interaction.requestLayout(false)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if let mainUsername = user.addressName {
|
if let mainUsername = user.addressName {
|
||||||
var additionalUsernames: String?
|
username = mainUsername
|
||||||
let usernames = user.usernames.filter { $0.isActive && $0.username != mainUsername }
|
let usernames = user.usernames.filter { $0.isActive && $0.username != mainUsername }
|
||||||
if !usernames.isEmpty {
|
if !usernames.isEmpty {
|
||||||
additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string
|
additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if let phone = user.phone {
|
||||||
|
phoneNumber = formatPhoneNumber(context: context, number: phone)
|
||||||
|
if let phone = phoneNumber, !phone.isEmpty && !phone.hasPrefix("+") {
|
||||||
|
phoneNumber = "+\(phone)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.botInfo == nil {
|
||||||
|
if username != nil || phoneNumber != nil {
|
||||||
|
items[.peerInfo]!.append(PeerInfoScreenContactInfoItem(
|
||||||
|
id: 1,
|
||||||
|
username: username.flatMap { "@\($0)" } ?? "",
|
||||||
|
phoneNumber: phoneNumber ?? "",
|
||||||
|
additionalText: additionalUsernames,
|
||||||
|
usernameAction: { _ in
|
||||||
|
interaction.openUsername(username ?? "")
|
||||||
|
},
|
||||||
|
usernameLongTapAction: { sourceNode in
|
||||||
|
interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil)
|
||||||
|
},
|
||||||
|
phoneAction: { node in
|
||||||
|
interaction.openPhone(phoneNumber ?? "", node, nil)
|
||||||
|
},
|
||||||
|
phoneLongTapAction: { _ in },
|
||||||
|
linkItemAction: { type, item, _, _ in
|
||||||
|
if case .tap = type {
|
||||||
|
if case let .mention(username) = item {
|
||||||
|
interaction.openUsername(String(username[username.index(username.startIndex, offsetBy: 1)...]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestLayout: {
|
||||||
|
interaction.requestLayout(false)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else if let username {
|
||||||
items[.peerInfo]!.append(
|
items[.peerInfo]!.append(
|
||||||
PeerInfoScreenLabeledValueItem(
|
PeerInfoScreenLabeledValueItem(
|
||||||
id: 1,
|
id: 1,
|
||||||
label: presentationData.strings.Profile_Username,
|
label: "",
|
||||||
text: "@\(mainUsername)",
|
text: "@\(username)",
|
||||||
additionalText: additionalUsernames,
|
additionalText: additionalUsernames,
|
||||||
textColor: .accent,
|
textColor: .accent,
|
||||||
icon: .qrCode,
|
icon: .qrCode,
|
||||||
action: { _ in
|
action: { _ in
|
||||||
interaction.openUsername(mainUsername)
|
interaction.openUsername(username)
|
||||||
}, longTapAction: { sourceNode in
|
}, longTapAction: { sourceNode in
|
||||||
interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil)
|
interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil)
|
||||||
}, linkItemAction: { type, item, _, _ in
|
}, linkItemAction: { type, item, _, _ in
|
||||||
@ -1054,17 +1078,18 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let cachedData = data.cachedData as? CachedUserData {
|
if let cachedData = data.cachedData as? CachedUserData {
|
||||||
if user.isFake {
|
if user.isFake {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
|
||||||
interaction.requestLayout(false)
|
interaction.requestLayout(false)
|
||||||
}))
|
}))
|
||||||
} else if user.isScam {
|
} else if user.isScam {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
|
||||||
interaction.requestLayout(false)
|
interaction.requestLayout(false)
|
||||||
}))
|
}))
|
||||||
} else if let about = cachedData.about, !about.isEmpty {
|
} else if let about = cachedData.about, !about.isEmpty {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
||||||
interaction.requestLayout(false)
|
interaction.requestLayout(false)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1086,14 +1111,6 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
interaction.openReport(.user)
|
interaction.openReport(.user)
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
if !data.isContact {
|
|
||||||
if user.botInfo == nil {
|
|
||||||
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 3, text: presentationData.strings.PeerInfo_AddToContacts, action: {
|
|
||||||
interaction.openAddContact()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isBlocked = false
|
var isBlocked = false
|
||||||
if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked {
|
if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked {
|
||||||
isBlocked = true
|
isBlocked = true
|
||||||
@ -1106,7 +1123,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
} else {
|
} else {
|
||||||
if user.flags.contains(.isSupport) || data.isContact {
|
if user.flags.contains(.isSupport) || data.isContact {
|
||||||
} else {
|
} else {
|
||||||
if user.botInfo == nil {
|
if user.botInfo == nil && isOpenedFromChat {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.Conversation_BlockUser, color: .destructive, action: {
|
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.Conversation_BlockUser, color: .destructive, action: {
|
||||||
interaction.updateBlocked(true)
|
interaction.updateBlocked(true)
|
||||||
}))
|
}))
|
||||||
@ -1162,7 +1179,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
items[.peerInfo]!.append(
|
items[.peerInfo]!.append(
|
||||||
PeerInfoScreenLabeledValueItem(
|
PeerInfoScreenLabeledValueItem(
|
||||||
id: ItemUsername,
|
id: ItemUsername,
|
||||||
label: presentationData.strings.Channel_LinkItem,
|
label: "",
|
||||||
text: linkText,
|
text: linkText,
|
||||||
textColor: .accent,
|
textColor: .accent,
|
||||||
icon: .qrCode,
|
icon: .qrCode,
|
||||||
@ -1214,7 +1231,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
items[.peerInfo]!.append(
|
items[.peerInfo]!.append(
|
||||||
PeerInfoScreenLabeledValueItem(
|
PeerInfoScreenLabeledValueItem(
|
||||||
id: ItemUsername,
|
id: ItemUsername,
|
||||||
label: presentationData.strings.Channel_LinkItem,
|
label: "",
|
||||||
text: "https://t.me/\(mainUsername)",
|
text: "https://t.me/\(mainUsername)",
|
||||||
additionalText: additionalUsernames,
|
additionalText: additionalUsernames,
|
||||||
textColor: .accent,
|
textColor: .accent,
|
||||||
@ -1266,7 +1283,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
if case .group = channel.info {
|
if case .group = channel.info {
|
||||||
enabledEntities = enabledPrivateBioEntities
|
enabledEntities = enabledPrivateBioEntities
|
||||||
}
|
}
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: "", text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
||||||
interaction.requestLayout(true)
|
interaction.requestLayout(true)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1312,7 +1329,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let aboutText = aboutText {
|
if let aboutText = aboutText {
|
||||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
|
||||||
interaction.requestLayout(true)
|
interaction.requestLayout(true)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -5426,6 +5443,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
case .stop:
|
case .stop:
|
||||||
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .universal(animation: "anim_banned", scale: 0.066, colors: [:], title: self.presentationData.strings.PeerInfo_BotBlockedTitle, text: self.presentationData.strings.PeerInfo_BotBlockedText, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .universal(animation: "anim_banned", scale: 0.066, colors: [:], title: self.presentationData.strings.PeerInfo_BotBlockedTitle, text: self.presentationData.strings.PeerInfo_BotBlockedText, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
|
||||||
self.updateBlocked(block: true)
|
self.updateBlocked(block: true)
|
||||||
|
case .addContact:
|
||||||
|
self.openAddContact()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8886,10 +8905,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
}
|
}
|
||||||
let headerInset = sectionInset
|
let headerInset = sectionInset
|
||||||
|
|
||||||
var headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive)
|
let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive)
|
||||||
if !self.isSettings && !self.state.isEditing {
|
|
||||||
headerHeight += 71.0
|
|
||||||
}
|
|
||||||
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight))
|
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight))
|
||||||
if additive {
|
if additive {
|
||||||
transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame)
|
transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame)
|
||||||
@ -8906,11 +8922,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
insets.left += sectionInset
|
insets.left += sectionInset
|
||||||
insets.right += sectionInset
|
insets.right += sectionInset
|
||||||
|
|
||||||
let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation)
|
let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat)
|
||||||
|
|
||||||
contentHeight += headerHeight
|
contentHeight += headerHeight
|
||||||
if !(self.isSettings && self.state.isEditing) {
|
if !(self.isSettings && self.state.isEditing) {
|
||||||
contentHeight += sectionSpacing
|
contentHeight += sectionSpacing + 12.0
|
||||||
}
|
}
|
||||||
|
|
||||||
for (sectionId, sectionItems) in items {
|
for (sectionId, sectionItems) in items {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user