2025-09-17 20:58:42 +04:00

391 lines
25 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import TelegramStringFormatting
import WallpaperBackgroundNode
import Markdown
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatControllerInteraction
import AnimatedStickerNode
import TelegramAnimatedStickerNode
public class ChatMessageBirthdateSuggestionContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let mediaBackgroundNode: NavigationBackgroundNode
private let animationNode: AnimatedStickerNode
private let subtitleNode: TextNode
private let dayTitleNode: TextNode
private let dayValueNode: TextNode
private let monthTitleNode: TextNode
private let monthValueNode: TextNode
private let yearTitleNode: TextNode
private let yearValueNode: TextNode
private let buttonNode: HighlightTrackingButtonNode
private let buttonTitleNode: TextNode
private var absoluteRect: (CGRect, CGSize)?
required public init() {
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
self.mediaBackgroundNode.clipsToBounds = true
self.mediaBackgroundNode.cornerRadius = 27.0
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
self.dayTitleNode = TextNode()
self.dayTitleNode.isUserInteractionEnabled = false
self.dayTitleNode.displaysAsynchronously = false
self.dayValueNode = TextNode()
self.dayValueNode.isUserInteractionEnabled = false
self.dayValueNode.displaysAsynchronously = false
self.monthTitleNode = TextNode()
self.monthTitleNode.isUserInteractionEnabled = false
self.monthTitleNode.displaysAsynchronously = false
self.monthValueNode = TextNode()
self.monthValueNode.isUserInteractionEnabled = false
self.monthValueNode.displaysAsynchronously = false
self.yearTitleNode = TextNode()
self.yearTitleNode.isUserInteractionEnabled = false
self.yearTitleNode.displaysAsynchronously = false
self.yearValueNode = TextNode()
self.yearValueNode.isUserInteractionEnabled = false
self.yearValueNode.displaysAsynchronously = false
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
self.buttonTitleNode = TextNode()
self.buttonTitleNode.isUserInteractionEnabled = false
self.buttonTitleNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.dayTitleNode)
self.addSubnode(self.dayValueNode)
self.addSubnode(self.monthTitleNode)
self.addSubnode(self.monthValueNode)
self.addSubnode(self.yearTitleNode)
self.addSubnode(self.yearValueNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.buttonTitleNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.4
strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonTitleNode.alpha = 0.4
} else {
strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.buttonTitleNode.alpha = 1.0
strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
guard let item = self.item else {
return
}
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeDayTitleLayout = TextNode.asyncLayout(self.dayTitleNode)
let makeDayValueLayout = TextNode.asyncLayout(self.dayValueNode)
let makeMonthTitleLayout = TextNode.asyncLayout(self.monthTitleNode)
let makeMonthValueLayout = TextNode.asyncLayout(self.monthValueNode)
let makeYearTitleLayout = TextNode.asyncLayout(self.yearTitleNode)
let makeYearValueLayout = TextNode.asyncLayout(self.yearValueNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let width: CGFloat = 186.0
var day: Int32 = 1
var month: Int32 = 1
var year: Int32?
if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedBirthday(birthday) = action.action {
day = birthday.day
month = birthday.month
year = birthday.year
}
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let subtitleColor = primaryTextColor.withAlphaComponent(item.presentationData.theme.theme.overallDarkAppearance ? 0.7 : 0.8)
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? ""
let text: String
let fromYou = item.message.author?.id == item.context.account.peerId
if fromYou {
text = item.presentationData.strings.Conversation_SuggestedBirthdateTextYou(peerName).string
} else {
text = item.presentationData.strings.Conversation_SuggestedBirthdateText(peerName).string
}
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor)
let subtitle = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in
return nil
}), textAlignment: .center)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let titleFont = Font.regular(13.0)
let valueFont = Font.semibold(13.0)
let (dayTitleLayout, dayTitleApply) = makeDayTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_Day, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dayValueLayout, dayValueApply) = makeDayValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(day)", font: valueFont, textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (monthTitleLayout, monthTitleApply) = makeMonthTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_Month, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (monthValueLayout, monthValueApply) = makeMonthValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: stringForMonth(strings: item.presentationData.strings, month: month - 1), font: valueFont, textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (yearTitleLayout, yearTitleApply) = makeYearTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_Year, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (yearValueLayout, yearValueApply) = makeYearValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: year.flatMap { "\($0)" } ?? "", font: valueFont, textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 160.0)
if !fromYou {
backgroundSize.height += 44.0
}
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
let isFirstTime = strongSelf.item == nil
strongSelf.item = item
let mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - width) / 2.0), y: 0.0), size: backgroundSize)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate)
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
if item.presentationData.theme.theme.overallDarkAppearance {
strongSelf.dayTitleNode.layer.compositingFilter = nil
strongSelf.monthTitleNode.layer.compositingFilter = nil
strongSelf.yearTitleNode.layer.compositingFilter = nil
} else {
strongSelf.dayTitleNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.monthTitleNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.yearTitleNode.layer.compositingFilter = "overlayBlendMode"
}
let _ = subtitleApply()
let _ = dayTitleApply()
let _ = monthTitleApply()
let _ = yearTitleApply()
let _ = dayValueApply()
let _ = monthValueApply()
let _ = yearValueApply()
let _ = buttonTitleApply()
let iconSize = CGSize(width: 80.0, height: 80.0)
let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 8.0), size: iconSize)
strongSelf.animationNode.frame = animationFrame
if isFirstTime {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Cake"), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
strongSelf.animationNode.visibility = true
}
strongSelf.animationNode.updateLayout(size: iconSize)
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 96.0), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame
let titleOriginY = subtitleFrame.maxY + 11.0
let valueOriginY = titleOriginY + 19.0
let minX = mediaBackgroundFrame.minX
let maxX = mediaBackgroundFrame.maxX
let width = mediaBackgroundFrame.width
let dayColWidth = max(dayTitleLayout.size.width, dayValueLayout.size.width)
let monthColWidth = max(monthTitleLayout.size.width, monthValueLayout.size.width)
let yearColW = max(yearTitleLayout.size.width, yearValueLayout.size.width)
func centerX(inLeft left: CGFloat, right: CGFloat, contentWidth: CGFloat) -> CGFloat {
return left + floorToScreenPixels((right - left - contentWidth) * 0.5)
}
if yearValueLayout.size.width > 0.0 {
strongSelf.yearTitleNode.isHidden = false
strongSelf.yearValueNode.isHidden = false
let monthLeft = centerX(inLeft: minX, right: maxX, contentWidth: monthColWidth)
let monthRight = monthLeft + monthColWidth
let dayLeft = centerX(inLeft: minX, right: monthLeft, contentWidth: dayColWidth)
let yearLeft = centerX(inLeft: monthRight, right: maxX, contentWidth: yearColW)
strongSelf.dayTitleNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayTitleLayout.size.width) * 0.5), y: titleOriginY),
size: dayTitleLayout.size
)
strongSelf.dayValueNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayValueLayout.size.width) * 0.5), y: valueOriginY),
size: dayValueLayout.size
)
strongSelf.monthTitleNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthTitleLayout.size.width) * 0.5), y: titleOriginY),
size: monthTitleLayout.size
)
strongSelf.monthValueNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthValueLayout.size.width) * 0.5), y: valueOriginY),
size: monthValueLayout.size
)
strongSelf.yearTitleNode.frame = CGRect(
origin: CGPoint(x: yearLeft + floorToScreenPixels((yearColW - yearTitleLayout.size.width) * 0.5), y: titleOriginY),
size: yearTitleLayout.size
)
strongSelf.yearValueNode.frame = CGRect(
origin: CGPoint(x: yearLeft + floorToScreenPixels((yearColW - yearValueLayout.size.width) * 0.5), y: valueOriginY),
size: yearValueLayout.size
)
} else {
strongSelf.yearTitleNode.isHidden = true
strongSelf.yearValueNode.isHidden = true
let spacing: CGFloat = 16.0
let totalWidth = dayColWidth + monthColWidth + spacing
let dayLeft = minX + floorToScreenPixels((width - totalWidth) / 2.0)
let monthLeft = dayLeft + dayColWidth + spacing
strongSelf.dayTitleNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayTitleLayout.size.width) * 0.5), y: titleOriginY),
size: dayTitleLayout.size
)
strongSelf.dayValueNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayValueLayout.size.width) * 0.5), y: valueOriginY),
size: dayValueLayout.size
)
strongSelf.monthTitleNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthTitleLayout.size.width) * 0.5), y: titleOriginY),
size: monthTitleLayout.size
)
strongSelf.monthValueNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthValueLayout.size.width) * 0.5), y: valueOriginY),
size: monthValueLayout.size
)
}
strongSelf.buttonNode.isHidden = fromYou
strongSelf.buttonTitleNode.isHidden = fromYou
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
let buttonFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: mediaBackgroundFrame.maxY - buttonSize.height - 16.0), size: buttonSize)
strongSelf.buttonNode.frame = buttonFrame
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: floorToScreenPixels(buttonFrame.midY - buttonTitleLayout.size.height / 2.0)), size: buttonTitleLayout.size)
strongSelf.buttonTitleNode.frame = buttonTitleFrame
if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true {
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
strongSelf.mediaBackgroundNode.isHidden = true
backgroundContent.clipsToBounds = true
backgroundContent.allowsGroupOpacity = true
backgroundContent.cornerRadius = 27.0
strongSelf.mediaBackgroundContent = backgroundContent
strongSelf.insertSubnode(backgroundContent, at: 0)
}
strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame
} else {
strongSelf.mediaBackgroundNode.isHidden = false
strongSelf.mediaBackgroundContent?.removeFromSupernode()
strongSelf.mediaBackgroundContent = nil
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
})
})
})
}
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
if let mediaBackgroundContent = self.mediaBackgroundContent {
var backgroundFrame = mediaBackgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}