import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import ComponentFlow import TelegramCore import AccountContext import TelegramPresentationData import TelegramUIPreferences import TextFormat import LocalizedPeerData import UrlEscaping import TelegramStringFormatting import WallpaperBackgroundNode import ReactionSelectionNode import AnimatedStickerNode import TelegramAnimatedStickerNode import ChatControllerInteraction import ShimmerEffect import Markdown import ChatMessageBubbleContentNode import ChatMessageItemCommon import TextNodeWithEntities import GiftItemComponent private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) } public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let titleNode: TextNode private let subtitleNode: TextNodeWithEntities private let giftIcon = ComponentView() private var absoluteRect: (CGRect, CGSize)? private var isPlaying: Bool = false override public var disablesClipping: Bool { return true } override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = oldValue != .none let isVisible = self.visibility != .none if wasVisible != isVisible { self.visibilityStatus = isVisible switch self.visibility { case .none: self.subtitleNode.visibilityRect = nil case let .visible(_, subRect): var subRect = subRect subRect.origin.x = 0.0 subRect.size.width = 10000.0 self.subtitleNode.visibilityRect = subRect } } } } private var visibilityStatus: Bool? { didSet { if self.visibilityStatus != oldValue { self.updateVisibility() } } } private var fetchDisposable: Disposable? private var setupTimestamp: Double? private var cachedTonImage: (UIImage, UIColor)? required public init() { self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.subtitleNode = TextNodeWithEntities() self.subtitleNode.textNode.isUserInteractionEnabled = false self.subtitleNode.textNode.displaysAsynchronously = false super.init() self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode.textNode) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.fetchDisposable?.dispose() } 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 makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) 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 var giftSize = CGSize(width: 260.0, height: 240.0) var uniqueGift: StarGift.UniqueGift? let incoming: Bool if item.message.id.peerId == item.context.account.peerId && item.message.forwardInfo == nil { incoming = true } else { incoming = item.message.effectivelyIncoming(item.context.account.peerId) } let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText let text: String let additionalText: String var hasActionButtons = false if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined) = action.action { let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let priceString: String switch amount.currency { case .stars: priceString = "\(amount.amount) Stars" case .ton: priceString = "\(amount.amount) TON" } let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" let giftTitle: String if case let .unique(gift) = gift { giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: item.presentationData.dateTimeFormat))" uniqueGift = gift } else { giftTitle = "" } //TODO:localize if incoming { text = "**\(peerName)** offered you **\(priceString)** for your gift **\(giftTitle)**." } else { text = "You offered **\(peerName)** **\(priceString)** for their gift **\(giftTitle)**." } if isAccepted { additionalText = "This offer was accepted." } else if isDeclined { additionalText = "This offer was rejected." } else if expireDate > currentTimestamp { func textForTimeout(_ value: Int32) -> String { if value < 3600 { let minutes = value / 60 //let seconds = value % 60 //let secondsPadding = seconds < 10 ? "0" : "" return "\(minutes)m" //\(secondsPadding)\(seconds)" } else { let hours = value / 3600 let minutes = (value % 3600) / 60 let minutesPadding = minutes < 10 ? "0" : "" //let seconds = value % 60 //let secondsPadding = seconds < 10 ? "0" : "" return "\(hours)h \(minutesPadding)\(minutes)m" //:\(secondsPadding)\(seconds)" } } let delta = expireDate - currentTimestamp additionalText = "This offer expires in \(textForTimeout(delta))." if incoming { hasActionButtons = true } } else { additionalText = "This offer has expired." } } else { text = "" additionalText = "" } let titleAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: additionalText, font: Font.regular(13.0), textColor: textColor, paragraphAlignment: .center)) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), linkAttribute: { url in return ("URL", url) } ), textAlignment: .center) let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) giftSize.height = titleLayout.size.height + subtitleLayout.size.height + 162.0 let backgroundSize = CGSize(width: giftSize.width, height: giftSize.height + 4.0) return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, info in if let strongSelf = self { strongSelf.item = item let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: 0.0), size: giftSize) let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) strongSelf.updateVisibility() let _ = titleApply() let _ = subtitleApply(TextNodeWithEntities.Arguments( context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, attemptSynchronous: synchronousLoads )) let textFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 126.0), size: subtitleLayout.size) strongSelf.subtitleNode.textNode.frame = textFrame let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: textFrame.maxY + 23.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { backgroundContent.clipsToBounds = true backgroundContent.cornerRadius = 24.0 strongSelf.mediaBackgroundContent = backgroundContent strongSelf.insertSubnode(backgroundContent, at: 0) } if let backgroundContent = strongSelf.mediaBackgroundContent { animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil) backgroundContent.clipsToBounds = true if hasActionButtons { backgroundContent.cornerRadius = 0.0 if backgroundContent.view.mask == nil { backgroundContent.view.mask = UIImageView(image: generateImage(mediaBackgroundFrame.size, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) context.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height * 0.5), cornerWidth: 24.0, cornerHeight: 24.0, transform: nil)) context.addPath(CGPath(roundedRect: CGRect(x: 0, y: size.height * 0.5 - 30.0, width: size.width, height: size.height * 0.5 + 30.0), cornerWidth: 8.0, cornerHeight: 8.0, transform: nil)) context.fillPath() })) } } else { backgroundContent.view.mask = nil backgroundContent.cornerRadius = 24.0 } } if let uniqueGift { let iconSize = CGSize(width: 94.0, height: 94.0) let _ = strongSelf.giftIcon.update( transition: .immediate, component: AnyComponent(GiftItemComponent( context: item.context, theme: item.presentationData.theme.theme, strings: item.presentationData.strings, peer: nil, subject: .uniqueGift(gift: uniqueGift, price: nil), mode: .thumbnail )), environment: {}, containerSize: iconSize ) if let giftIconView = strongSelf.giftIcon.view { if giftIconView.superview == nil { // backgroundView.layer.cornerRadius = 20.0 //backgroundView.clipsToBounds = true strongSelf.view.addSubview(giftIconView) } giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize) } } if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } switch strongSelf.visibility { case .none: strongSelf.subtitleNode.visibilityRect = nil case let .visible(_, subRect): var subRect = subRect subRect.origin.x = 0.0 subRect.size.width = 10000.0 strongSelf.subtitleNode.visibilityRect = subRect } } }) }) }) } } 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 applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { } override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { } override public func unreadMessageRangeUpdated() { self.updateVisibility() } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.mediaBackgroundContent?.frame.contains(point) == true { return ChatMessageBubbleContentTapAction(content: .openMessage) } else { return ChatMessageBubbleContentTapAction(content: .none) } } private func updateVisibility() { } }