User profile info improvements

This commit is contained in:
Ilya Laktyushin 2023-06-09 00:26:06 +04:00
parent 73987dff5d
commit d677ee44bb
5 changed files with 653 additions and 67 deletions

View File

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

View File

@ -472,9 +472,19 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
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 additionalTextFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 3.0), size: additionalTextSize)
var topOffset = 11.0
var height = topOffset * 2.0
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)
self.expandNode.frame = expandFrame
@ -496,8 +506,6 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
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)
transition.updateFrame(node: self.iconButtonNode, frame: iconButtonFrame)
if let iconSize = self.iconNode.image?.size {

View File

@ -1142,10 +1142,18 @@ func peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: PeerInfoHeaderButtonKe
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] {
var result: [PeerInfoHeaderButtonKey] = []
if let user = peer as? TelegramUser {
if !isOpenedFromChat {
if !isOpenedFromChat && isContact {
result.append(.message)
}
var callsAvailable = false

View File

@ -44,6 +44,7 @@ enum PeerInfoHeaderButtonKey: Hashable {
case search
case leave
case stop
case addContact
}
enum PeerInfoHeaderButtonIcon {
@ -135,7 +136,7 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode {
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 themeUpdated = self.theme != presentationData.theme
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 {
let sourceNavigationBar: NavigationBar
let sourceTitleView: ChatTitleView
@ -2285,6 +2368,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let usernameNodeContainer: ASDisplayNode
let usernameNodeRawContainer: ASDisplayNode
let usernameNode: MultiScaleTextNode
var actionButtonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderActionButtonNode] = [:]
var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:]
let backgroundNode: NavigationBackgroundNode
let expandedBackgroundNode: NavigationBackgroundNode
@ -2834,6 +2918,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight)
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)
var isPremium = false
@ -3497,14 +3582,69 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let buttonSpacing: CGFloat = 8.0
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 apparentButtonSize = CGSize(width: buttonWidth, height: 58.0)
let buttonsAlpha: CGFloat = 1.0
let buttonsVerticalOffset: CGFloat = 0.0
let buttonsAlphaTransition = transition
let buttonSize = CGSize(width: buttonWidth, height: 58.0)
for buttonKey in buttonKeys.reversed() {
let buttonNode: PeerInfoHeaderButtonNode
@ -3520,14 +3660,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
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 apparentButtonFrame = buttonFrame.offsetBy(dx: 0.0, dy: buttonsVerticalOffset)
if additive {
buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: apparentButtonFrame)
buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame)
} else {
buttonTransition.updateFrame(node: buttonNode, frame: apparentButtonFrame)
buttonTransition.updateFrame(node: buttonNode, frame: buttonFrame)
}
let buttonText: String
let buttonIcon: PeerInfoHeaderButtonIcon
@ -3575,6 +3714,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
case .stop:
buttonText = presentationData.strings.PeerInfo_ButtonStop
buttonIcon = .stop
case .addContact:
fatalError()
}
var isActive = true
@ -3582,12 +3723,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
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 {
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 let .animated(duration, curve) = transition {
@ -3598,7 +3739,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} else {
transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0)
}
buttonRightOrigin.x -= apparentButtonSize.width + buttonSpacing
buttonRightOrigin.x -= buttonSize.width + buttonSpacing
}
for key in self.buttonNodes.keys {
@ -3622,7 +3763,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let backgroundFrame: CGRect
let separatorFrame: CGRect
let resolvedHeight: CGFloat
var resolvedHeight: CGFloat
if state.isEditing {
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.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 {
transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame)
@ -3651,6 +3792,14 @@ final class PeerInfoHeaderNode: ASDisplayNode {
transition.updateFrame(node: self.separatorNode, frame: separatorFrame)
}
if !state.isEditing && !isSettings {
resolvedHeight += 71.0
if !actionButtonKeys.isEmpty {
resolvedHeight += 64.0
}
}
if isFirstTime {
self.updateAvatarMask(transition: .immediate)
}
@ -3662,6 +3811,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
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? {
guard let result = super.hitTest(point, with: event) else {
return nil

View File

@ -976,7 +976,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
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 {
return []
}
@ -1005,39 +1005,63 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
items[.calls]!.append(PeerInfoScreenCallListItem(id: 20, messages: callMessages))
}
if let phone = user.phone {
let formattedPhone = formatPhoneNumber(context: context, number: phone)
let label: 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)
}))
}
var username: String?
var additionalUsernames: String?
var phoneNumber: String?
if let mainUsername = user.addressName {
var additionalUsernames: String?
username = mainUsername
let usernames = user.usernames.filter { $0.isActive && $0.username != mainUsername }
if !usernames.isEmpty {
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(
PeerInfoScreenLabeledValueItem(
id: 1,
label: presentationData.strings.Profile_Username,
text: "@\(mainUsername)",
label: "",
text: "@\(username)",
additionalText: additionalUsernames,
textColor: .accent,
icon: .qrCode,
action: { _ in
interaction.openUsername(mainUsername)
interaction.openUsername(username)
}, longTapAction: { sourceNode in
interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil)
}, linkItemAction: { type, item, _, _ in
@ -1054,17 +1078,18 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
)
)
}
if let cachedData = data.cachedData as? CachedUserData {
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)
}))
} 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)
}))
} 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)
}))
}
@ -1086,14 +1111,6 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
interaction.openReport(.user)
}))
} 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
if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked {
isBlocked = true
@ -1106,7 +1123,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
} else {
if user.flags.contains(.isSupport) || data.isContact {
} else {
if user.botInfo == nil {
if user.botInfo == nil && isOpenedFromChat {
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.Conversation_BlockUser, color: .destructive, action: {
interaction.updateBlocked(true)
}))
@ -1162,7 +1179,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
items[.peerInfo]!.append(
PeerInfoScreenLabeledValueItem(
id: ItemUsername,
label: presentationData.strings.Channel_LinkItem,
label: "",
text: linkText,
textColor: .accent,
icon: .qrCode,
@ -1214,7 +1231,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
items[.peerInfo]!.append(
PeerInfoScreenLabeledValueItem(
id: ItemUsername,
label: presentationData.strings.Channel_LinkItem,
label: "",
text: "https://t.me/\(mainUsername)",
additionalText: additionalUsernames,
textColor: .accent,
@ -1266,7 +1283,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if case .group = channel.info {
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)
}))
}
@ -1312,7 +1329,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}
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)
}))
}
@ -5426,6 +5443,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
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.updateBlocked(block: true)
case .addContact:
self.openAddContact()
}
}
@ -8886,10 +8905,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
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)
if !self.isSettings && !self.state.isEditing {
headerHeight += 71.0
}
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)
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight))
if additive {
transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame)
@ -8906,11 +8922,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
insets.left += 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
if !(self.isSettings && self.state.isEditing) {
contentHeight += sectionSpacing
contentHeight += sectionSpacing + 12.0
}
for (sectionId, sectionItems) in items {