mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1382 lines
88 KiB
Swift
1382 lines
88 KiB
Swift
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 InvisibleInkDustNode
|
|
import PeerInfoCoverComponent
|
|
|
|
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 ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|
private let labelNode: TextNode
|
|
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
|
private let backgroundMaskNode: ASImageNode
|
|
private var linkHighlightingNode: LinkHighlightingNode?
|
|
|
|
private let patternView = ComponentView<Empty>()
|
|
private let mediaBackgroundMaskNode: ASImageNode
|
|
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
|
|
private let titleNode: TextNode
|
|
private let subtitleNode: TextNodeWithEntities
|
|
private var spoilerSubtitleNode: TextNodeWithEntities?
|
|
private let textClippingNode: ASDisplayNode
|
|
private var dustNode: InvisibleInkDustNode?
|
|
private let placeholderNode: StickerShimmerEffectNode
|
|
private let animationNode: AnimatedStickerNode
|
|
|
|
private let modelTitleTextNode: TextNode
|
|
private let modelValueTextNode: TextNode
|
|
private let backdropTitleTextNode: TextNode
|
|
private let backdropValueTextNode: TextNode
|
|
private let symbolTitleTextNode: TextNode
|
|
private let symbolValueTextNode: TextNode
|
|
|
|
private let ribbonBackgroundNode: ASImageNode
|
|
private let ribbonTextNode: TextNode
|
|
|
|
private var shimmerEffectNode: ShimmerEffectForegroundNode?
|
|
private let buttonNode: HighlightTrackingButtonNode
|
|
private let buttonStarsNode: PremiumStarsNode
|
|
private let buttonContentNode: ASDisplayNode
|
|
private let buttonTitleNode: TextNode
|
|
private var buttonIconNode: DefaultAnimatedStickerNodeImpl?
|
|
|
|
private let moreTextNode: TextNode
|
|
|
|
private var maskView: UIImageView?
|
|
private var maskOverlayView: UIView?
|
|
|
|
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
|
|
private var absoluteRect: (CGRect, CGSize)?
|
|
|
|
private var isPlaying: Bool = false
|
|
|
|
private var isExpanded: Bool = false
|
|
private var appliedIsExpanded: Bool = false
|
|
|
|
private var isStarGift = false
|
|
|
|
private var currentProgressDisposable: Disposable?
|
|
|
|
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?
|
|
|
|
required public init() {
|
|
self.labelNode = TextNode()
|
|
self.labelNode.isUserInteractionEnabled = false
|
|
self.labelNode.displaysAsynchronously = false
|
|
|
|
self.backgroundMaskNode = ASImageNode()
|
|
|
|
self.mediaBackgroundMaskNode = ASImageNode()
|
|
|
|
self.titleNode = TextNode()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
self.titleNode.displaysAsynchronously = false
|
|
|
|
self.subtitleNode = TextNodeWithEntities()
|
|
self.subtitleNode.textNode.isUserInteractionEnabled = false
|
|
self.subtitleNode.textNode.displaysAsynchronously = false
|
|
|
|
self.textClippingNode = ASDisplayNode()
|
|
self.textClippingNode.clipsToBounds = true
|
|
|
|
self.modelTitleTextNode = TextNode()
|
|
self.modelTitleTextNode.isUserInteractionEnabled = false
|
|
self.modelTitleTextNode.displaysAsynchronously = false
|
|
self.modelValueTextNode = TextNode()
|
|
self.modelValueTextNode.isUserInteractionEnabled = false
|
|
self.modelValueTextNode.displaysAsynchronously = false
|
|
self.backdropTitleTextNode = TextNode()
|
|
self.backdropTitleTextNode.isUserInteractionEnabled = false
|
|
self.backdropTitleTextNode.displaysAsynchronously = false
|
|
self.backdropValueTextNode = TextNode()
|
|
self.backdropValueTextNode.isUserInteractionEnabled = false
|
|
self.backdropValueTextNode.displaysAsynchronously = false
|
|
self.symbolTitleTextNode = TextNode()
|
|
self.symbolTitleTextNode.isUserInteractionEnabled = false
|
|
self.symbolTitleTextNode.displaysAsynchronously = false
|
|
self.symbolValueTextNode = TextNode()
|
|
self.symbolValueTextNode.isUserInteractionEnabled = false
|
|
self.symbolValueTextNode.displaysAsynchronously = false
|
|
|
|
self.buttonNode = HighlightTrackingButtonNode()
|
|
self.buttonNode.clipsToBounds = true
|
|
self.buttonNode.cornerRadius = 17.0
|
|
|
|
self.placeholderNode = StickerShimmerEffectNode()
|
|
self.placeholderNode.isUserInteractionEnabled = false
|
|
self.placeholderNode.alpha = 0.75
|
|
|
|
self.animationNode = DefaultAnimatedStickerNodeImpl()
|
|
|
|
self.buttonStarsNode = PremiumStarsNode()
|
|
|
|
self.buttonContentNode = ASDisplayNode()
|
|
self.buttonContentNode.isUserInteractionEnabled = false
|
|
|
|
self.buttonTitleNode = TextNode()
|
|
self.buttonTitleNode.displaysAsynchronously = false
|
|
|
|
self.ribbonBackgroundNode = ASImageNode()
|
|
self.ribbonBackgroundNode.displaysAsynchronously = false
|
|
|
|
self.ribbonTextNode = TextNode()
|
|
self.ribbonTextNode.isUserInteractionEnabled = false
|
|
self.ribbonTextNode.displaysAsynchronously = false
|
|
|
|
self.moreTextNode = TextNode()
|
|
self.moreTextNode.isUserInteractionEnabled = false
|
|
self.moreTextNode.displaysAsynchronously = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.labelNode)
|
|
|
|
self.addSubnode(self.titleNode)
|
|
self.addSubnode(self.textClippingNode)
|
|
self.textClippingNode.addSubnode(self.subtitleNode.textNode)
|
|
self.addSubnode(self.placeholderNode)
|
|
self.addSubnode(self.animationNode)
|
|
self.addSubnode(self.moreTextNode)
|
|
|
|
self.addSubnode(self.buttonNode)
|
|
self.buttonNode.addSubnode(self.buttonStarsNode)
|
|
self.buttonNode.addSubnode(self.buttonContentNode)
|
|
|
|
self.buttonContentNode.addSubnode(self.buttonTitleNode)
|
|
|
|
self.addSubnode(self.ribbonBackgroundNode)
|
|
self.addSubnode(self.ribbonTextNode)
|
|
|
|
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.buttonNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.buttonNode.alpha = 1.0
|
|
strongSelf.buttonNode.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")
|
|
}
|
|
|
|
deinit {
|
|
self.fetchDisposable?.dispose()
|
|
self.currentProgressDisposable?.dispose()
|
|
}
|
|
|
|
override public func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.maskView = UIImageView()
|
|
|
|
let maskOverlayView = UIView()
|
|
maskOverlayView.alpha = 0.0
|
|
maskOverlayView.backgroundColor = .white
|
|
self.maskOverlayView = maskOverlayView
|
|
|
|
self.maskView?.addSubview(maskOverlayView)
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
guard let item = self.item else {
|
|
return
|
|
}
|
|
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress()))
|
|
}
|
|
|
|
private func expandPressed() {
|
|
self.isExpanded = !self.isExpanded
|
|
guard let item = self.item else{
|
|
return
|
|
}
|
|
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
|
|
}
|
|
|
|
private func makeProgress() -> Promise<Bool> {
|
|
let progress = Promise<Bool>()
|
|
self.currentProgressDisposable?.dispose()
|
|
self.currentProgressDisposable = (progress.get()
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.displayProgress = hasProgress
|
|
})
|
|
return progress
|
|
}
|
|
|
|
private var displayProgress = false {
|
|
didSet {
|
|
if self.displayProgress != oldValue {
|
|
if self.displayProgress {
|
|
self.startShimmering()
|
|
} else {
|
|
self.stopShimmering()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startShimmering() {
|
|
let shimmerEffectNode: ShimmerEffectForegroundNode
|
|
if let current = self.shimmerEffectNode {
|
|
shimmerEffectNode = current
|
|
} else {
|
|
shimmerEffectNode = ShimmerEffectForegroundNode()
|
|
shimmerEffectNode.cornerRadius = 17.0
|
|
self.buttonNode.insertSubnode(shimmerEffectNode, at: 0)
|
|
self.shimmerEffectNode = shimmerEffectNode
|
|
}
|
|
|
|
shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
let backgroundFrame = self.buttonNode.frame
|
|
shimmerEffectNode.frame = CGRect(origin: .zero, size: backgroundFrame.size)
|
|
shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size)
|
|
shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.15), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
|
|
}
|
|
|
|
private func stopShimmering() {
|
|
guard let shimmerEffectNode = self.shimmerEffectNode else {
|
|
return
|
|
}
|
|
self.shimmerEffectNode = nil
|
|
shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak shimmerEffectNode] _ in
|
|
shimmerEffectNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
private func removePlaceholder(animated: Bool) {
|
|
self.placeholderNode.alpha = 0.0
|
|
if !animated {
|
|
self.placeholderNode.removeFromSupernode()
|
|
} else {
|
|
self.placeholderNode.layer.animateAlpha(from: self.placeholderNode.alpha, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
|
self?.placeholderNode.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
|
|
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 makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
|
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
|
|
let makeSpoilerSubtitleLayout = TextNodeWithEntities.asyncLayout(self.spoilerSubtitleNode)
|
|
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
|
|
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
|
|
let makeMeasureTextLayout = TextNode.asyncLayout(nil)
|
|
let makeMoreTextLayout = TextNode.asyncLayout(self.moreTextNode)
|
|
|
|
let makeModelTitleLayout = TextNode.asyncLayout(self.modelTitleTextNode)
|
|
let makeModelValueLayout = TextNode.asyncLayout(self.modelValueTextNode)
|
|
let makeBackdropTitleLayout = TextNode.asyncLayout(self.backdropTitleTextNode)
|
|
let makeBackdropValueLayout = TextNode.asyncLayout(self.backdropValueTextNode)
|
|
let makeSymbolTitleLayout = TextNode.asyncLayout(self.symbolTitleTextNode)
|
|
let makeSymbolValueLayout = TextNode.asyncLayout(self.symbolValueTextNode)
|
|
|
|
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
|
|
|
|
let currentIsExpanded = self.isExpanded
|
|
|
|
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: 220.0, height: 240.0)
|
|
|
|
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 attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId)
|
|
|
|
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
|
|
|
var months: Int32 = 3
|
|
var animationName: String = ""
|
|
var animationFile: TelegramMediaFile?
|
|
var title = item.presentationData.strings.Notification_PremiumGift_Title
|
|
var text = ""
|
|
var subtitleColor = primaryTextColor
|
|
var entities: [MessageTextEntity] = []
|
|
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
|
|
var buttonIcon: String?
|
|
var ribbonTitle = ""
|
|
var textSpacing: CGFloat = 0.0
|
|
var isStarGift = false
|
|
|
|
var modelTitle: String?
|
|
var modelValue: String?
|
|
var backdropTitle: String?
|
|
var backdropValue: String?
|
|
var symbolTitle: String?
|
|
var symbolValue: String?
|
|
var uniqueBackgroundColor: UIColor?
|
|
var uniqueSecondBackgroundColor: UIColor?
|
|
var uniquePatternColor: UIColor?
|
|
var uniquePatternFile: TelegramMediaFile?
|
|
|
|
let isStoryEntity = item.message.id.id == -1
|
|
var hasServiceMessage = !isStoryEntity
|
|
|
|
for media in item.message.media {
|
|
if let action = media as? TelegramMediaAction {
|
|
switch action.action {
|
|
case let .giftPremium(_, _, monthsValue, _, _, giftText, giftEntities):
|
|
months = monthsValue
|
|
if months == 12 {
|
|
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
|
|
} else {
|
|
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
|
|
}
|
|
if let giftText, !giftText.isEmpty {
|
|
text = giftText
|
|
entities = giftEntities ?? []
|
|
} else {
|
|
text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription
|
|
}
|
|
case let .giftStars(_, _, count, _, _, _):
|
|
if count <= 1000 {
|
|
months = 3
|
|
} else if count < 2500 {
|
|
months = 6
|
|
} else {
|
|
months = 12
|
|
}
|
|
var peerName = ""
|
|
if let peer = item.message.peers[item.message.id.peerId] {
|
|
peerName = EnginePeer(peer).compactDisplayTitle
|
|
}
|
|
title = item.presentationData.strings.Notification_StarsGift_Title(Int32(count))
|
|
text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string
|
|
case let .prizeStars(count, _, channelId, _, _):
|
|
if count <= 1000 {
|
|
months = 3
|
|
} else if count < 2500 {
|
|
months = 6
|
|
} else {
|
|
months = 12
|
|
}
|
|
var peerName = ""
|
|
if let channelId, let channel = item.message.peers[channelId] {
|
|
peerName = EnginePeer(channel).compactDisplayTitle
|
|
}
|
|
title = item.presentationData.strings.Notification_StarsGiveaway_Title
|
|
text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(count))).string
|
|
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities):
|
|
if channelId == nil {
|
|
months = monthsValue
|
|
if months == 12 {
|
|
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
|
|
} else {
|
|
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
|
|
}
|
|
if let giftText, !giftText.isEmpty {
|
|
text = giftText
|
|
entities = giftEntities ?? []
|
|
} else {
|
|
text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription
|
|
}
|
|
if item.message.author?.id != item.context.account.peerId {
|
|
buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift
|
|
}
|
|
} else {
|
|
giftSize.width += 34.0
|
|
textSpacing += 13.0
|
|
|
|
if unclaimed {
|
|
title = item.presentationData.strings.Notification_PremiumPrize_Unclaimed
|
|
} else {
|
|
title = item.presentationData.strings.Notification_PremiumPrize_Title
|
|
}
|
|
var peerName = ""
|
|
if let channelId, let channel = item.message.peers[channelId] {
|
|
peerName = EnginePeer(channel).compactDisplayTitle
|
|
}
|
|
if unclaimed {
|
|
text = item.presentationData.strings.Notification_PremiumPrize_UnclaimedText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string
|
|
} else if fromGiveaway {
|
|
text = item.presentationData.strings.Notification_PremiumPrize_GiveawayText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string
|
|
} else {
|
|
text = item.presentationData.strings.Notification_PremiumPrize_GiftText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string
|
|
}
|
|
|
|
months = monthsValue
|
|
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
|
|
hasServiceMessage = false
|
|
}
|
|
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, _, _):
|
|
if case let .generic(gift) = gift {
|
|
isStarGift = true
|
|
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
|
|
|
let isSelfGift = item.message.id.peerId == item.context.account.peerId
|
|
let isChannelGift = item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel
|
|
if isSelfGift {
|
|
title = item.presentationData.strings.Notification_StarGift_Self_Title
|
|
} else {
|
|
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
|
|
}
|
|
if let giftText, !giftText.isEmpty {
|
|
text = giftText
|
|
entities = giftEntities ?? []
|
|
} else {
|
|
if isRefunded {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Refunded
|
|
} else if upgraded {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Upgraded
|
|
} else if incoming {
|
|
if converted {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Converted(item.presentationData.strings.Notification_StarGift_Subtitle_Converted_Stars(Int32(convertStars ?? 0))).string
|
|
} else if upgradeStars != nil {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Upgrade
|
|
} else if isSelfGift && canUpgrade {
|
|
text = item.presentationData.strings.Notification_StarsGift_Subtitle_Self
|
|
} else if savedToProfile {
|
|
if let convertStars {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Displaying(item.presentationData.strings.Notification_StarGift_Subtitle_Displaying_Stars(Int32(convertStars))).string
|
|
} else {
|
|
text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle_Displaying
|
|
}
|
|
} else {
|
|
if let convertStars, convertStars > 0 {
|
|
if isChannelGift {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Channel(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string
|
|
} else {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string
|
|
}
|
|
} else {
|
|
text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle
|
|
}
|
|
}
|
|
} else {
|
|
var peerName = ""
|
|
if let peer = item.message.peers[item.message.id.peerId] {
|
|
peerName = EnginePeer(peer).compactDisplayTitle
|
|
}
|
|
if peerName.isEmpty {
|
|
if let convertStars, convertStars > 0 {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string
|
|
} else {
|
|
text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle
|
|
}
|
|
} else {
|
|
if upgradeStars != nil {
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Upgrade_Other(peerName).string
|
|
} else if let convertStars, convertStars > 0 {
|
|
let formattedString = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars)))
|
|
text = formattedString.string
|
|
if let starsRange = formattedString.ranges.last {
|
|
entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
animationFile = gift.file
|
|
if let availability = gift.availability {
|
|
let availabilityString: String
|
|
if availability.total > 9999 {
|
|
availabilityString = compactNumericCountString(Int(availability.total))
|
|
} else {
|
|
availabilityString = "\(availability.total)"
|
|
}
|
|
ribbonTitle = item.presentationData.strings.Notification_StarGift_OneOf(availabilityString).string
|
|
}
|
|
if incoming || item.presentationData.isPreview, let upgradeStars, upgradeStars > 0, !upgraded {
|
|
buttonTitle = item.presentationData.strings.Notification_StarGift_Unpack
|
|
buttonIcon = "GiftUnpack"
|
|
} else {
|
|
buttonTitle = item.presentationData.strings.Notification_StarGift_View
|
|
}
|
|
}
|
|
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded):
|
|
if case let .unique(uniqueGift) = gift {
|
|
isStarGift = true
|
|
let authorName: String
|
|
if isUpgrade {
|
|
if item.message.author?.id == item.context.account.peerId {
|
|
authorName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
|
} else {
|
|
authorName = item.associatedData.accountPeer?.compactDisplayTitle ?? ""
|
|
}
|
|
} else {
|
|
authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
|
}
|
|
title = isStoryEntity ? uniqueGift.title : item.presentationData.strings.Notification_StarGift_Title(authorName).string
|
|
text = isStoryEntity ? "**Collectible #\(uniqueGift.number)**" : "**\(uniqueGift.title) #\(uniqueGift.number)**"
|
|
ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift
|
|
buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View
|
|
modelTitle = item.presentationData.strings.Notification_StarGift_Model
|
|
backdropTitle = item.presentationData.strings.Notification_StarGift_Backdrop
|
|
symbolTitle = item.presentationData.strings.Notification_StarGift_Symbol
|
|
|
|
for attribute in uniqueGift.attributes {
|
|
switch attribute {
|
|
case let .model(name, file, _):
|
|
modelValue = name
|
|
animationFile = file
|
|
case let .backdrop(name, innerColor, outerColor, patternColor, _, _):
|
|
uniqueBackgroundColor = UIColor(rgb: UInt32(bitPattern: outerColor))
|
|
uniqueSecondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColor))
|
|
uniquePatternColor = UIColor(rgb: UInt32(bitPattern: patternColor))
|
|
backdropValue = name
|
|
subtitleColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
|
|
case let .pattern(name, file, _):
|
|
symbolValue = name
|
|
uniquePatternFile = file
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
} else if isRefunded, case let .generic(gift) = gift {
|
|
isStarGift = true
|
|
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
|
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
|
|
text = item.presentationData.strings.Notification_StarGift_Subtitle_Refunded
|
|
animationFile = gift.file
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
switch months {
|
|
case 12:
|
|
animationName = "Gift12"
|
|
case 6:
|
|
animationName = "Gift6"
|
|
case 3:
|
|
animationName = "Gift3"
|
|
default:
|
|
animationName = "Gift3"
|
|
}
|
|
|
|
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (moreLayout, moreApply) = makeMoreTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_More, font: Font.semibold(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let attributedText: NSAttributedString
|
|
if !entities.isEmpty {
|
|
attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil)
|
|
} else {
|
|
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: subtitleColor),
|
|
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: subtitleColor),
|
|
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: subtitleColor),
|
|
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()))
|
|
|
|
let (_, spoilerSubtitleApply) = makeSpoilerSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets(), displaySpoilers: true))
|
|
|
|
var canExpand = false
|
|
var clippedTextHeight: CGFloat = subtitleLayout.size.height
|
|
if subtitleLayout.numberOfLines > 4 {
|
|
let (measuredTextLayout, _) = makeMeasureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
canExpand = true
|
|
if !currentIsExpanded {
|
|
clippedTextHeight = measuredTextLayout.size.height
|
|
}
|
|
}
|
|
|
|
let infoConstrainedSize = CGSize(width: (giftSize.width - 32.0) * 0.7, height: CGFloat.greatestFiniteMagnitude)
|
|
let modelTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
if let modelTitle {
|
|
modelTitleLayoutAndApply = makeModelTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: modelTitle, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
} else {
|
|
modelTitleLayoutAndApply = nil
|
|
}
|
|
let modelValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
if let modelValue {
|
|
modelValueLayoutAndApply = makeModelValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: modelValue, font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
} else {
|
|
modelValueLayoutAndApply = nil
|
|
}
|
|
|
|
let backdropTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
if let backdropTitle {
|
|
backdropTitleLayoutAndApply = makeBackdropTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: backdropTitle, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
} else {
|
|
backdropTitleLayoutAndApply = nil
|
|
}
|
|
let backdropValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
if let backdropValue {
|
|
backdropValueLayoutAndApply = makeBackdropValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: backdropValue, font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
} else {
|
|
backdropValueLayoutAndApply = nil
|
|
}
|
|
|
|
let symbolTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
if let symbolTitle {
|
|
symbolTitleLayoutAndApply = makeSymbolTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: symbolTitle, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
} else {
|
|
symbolTitleLayoutAndApply = nil
|
|
}
|
|
let symbolValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
if let symbolValue {
|
|
symbolValueLayoutAndApply = makeSymbolValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: symbolValue, font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
} else {
|
|
symbolValueLayoutAndApply = nil
|
|
}
|
|
|
|
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
giftSize.height = titleLayout.size.height + textSpacing + clippedTextHeight + 164.0
|
|
|
|
if let _ = modelTitle {
|
|
giftSize.height += 70.0
|
|
}
|
|
|
|
if !buttonTitle.isEmpty {
|
|
giftSize.height += 48.0
|
|
} else if isStoryEntity {
|
|
giftSize.height += 12.0
|
|
}
|
|
|
|
var labelRects = labelLayout.linesRects()
|
|
if labelRects.count > 1 {
|
|
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
|
|
for i in 0 ..< sortedIndices.count {
|
|
let index = sortedIndices[i]
|
|
for j in -1 ... 1 {
|
|
if j != 0 && index + j >= 0 && index + j < sortedIndices.count {
|
|
if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 {
|
|
labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width)
|
|
labelRects[index].size.width = labelRects[index + j].size.width
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for i in 0 ..< labelRects.count {
|
|
labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0))
|
|
labelRects[i].size.height = 20.0
|
|
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
|
|
}
|
|
|
|
let backgroundMaskImage: (CGPoint, UIImage)?
|
|
var backgroundMaskUpdated = false
|
|
if hasServiceMessage {
|
|
if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects {
|
|
backgroundMaskImage = (currentOffset, currentImage)
|
|
} else {
|
|
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 10.0, outerRadius: 10.0, rects: labelRects, useModernPathCalculation: false)
|
|
backgroundMaskUpdated = true
|
|
}
|
|
} else {
|
|
backgroundMaskImage = nil
|
|
}
|
|
|
|
var backgroundSize = giftSize
|
|
if hasServiceMessage {
|
|
backgroundSize.height += labelLayout.size.height + 18.0
|
|
} else {
|
|
backgroundSize.height += 4.0
|
|
}
|
|
|
|
return (backgroundSize.width, { boundingWidth in
|
|
return (backgroundSize, { [weak self] animation, synchronousLoads, info in
|
|
if let strongSelf = self {
|
|
let isFirstTime = strongSelf.item == nil
|
|
|
|
if strongSelf.appliedIsExpanded != currentIsExpanded {
|
|
strongSelf.appliedIsExpanded = currentIsExpanded
|
|
info?.setInvertOffsetDirection()
|
|
|
|
if let maskOverlayView = strongSelf.maskOverlayView {
|
|
animation.transition.updateAlpha(layer: maskOverlayView.layer, alpha: currentIsExpanded ? 1.0 : 0.0)
|
|
}
|
|
}
|
|
|
|
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance && uniquePatternFile == nil ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
|
|
|
|
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 12.0 : 0.0), size: giftSize)
|
|
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
|
|
|
|
var iconSize = CGSize(width: 160.0, height: 160.0)
|
|
var iconOffset: CGFloat = 0.0
|
|
if let _ = animationFile {
|
|
iconSize = CGSize(width: 120.0, height: 120.0)
|
|
iconOffset = 32.0
|
|
}
|
|
let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize)
|
|
strongSelf.animationNode.frame = animationFrame
|
|
strongSelf.animationNode.isHidden = isStoryEntity
|
|
|
|
strongSelf.buttonNode.isHidden = buttonTitle.isEmpty
|
|
strongSelf.buttonTitleNode.isHidden = buttonTitle.isEmpty
|
|
|
|
if strongSelf.item == nil && !isStoryEntity {
|
|
strongSelf.animationNode.started = { [weak self] in
|
|
if let strongSelf = self {
|
|
let current = CACurrentMediaTime()
|
|
if let setupTimestamp = strongSelf.setupTimestamp, current - setupTimestamp > 0.3 {
|
|
if !strongSelf.placeholderNode.alpha.isZero {
|
|
strongSelf.animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
strongSelf.removePlaceholder(animated: true)
|
|
}
|
|
} else {
|
|
strongSelf.removePlaceholder(animated: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
strongSelf.animationNode.autoplay = true
|
|
|
|
if let file = animationFile {
|
|
strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, isVideo: file.mimeType == "video/webm"), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
|
if strongSelf.fetchDisposable == nil {
|
|
strongSelf.fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: item.context.account.postbox, userLocation: .other, fileReference: .message(message: MessageReference(item.message), media: file), resource: file.resource).start()
|
|
}
|
|
|
|
if let immediateThumbnailData = file.immediateThumbnailData {
|
|
let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper)
|
|
strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: overlayColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: animationFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency)
|
|
}
|
|
} else if animationName.hasPrefix("Gift") {
|
|
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
|
|
}
|
|
}
|
|
strongSelf.item = item
|
|
strongSelf.isStarGift = isStarGift
|
|
|
|
strongSelf.updateVisibility()
|
|
|
|
strongSelf.labelNode.isHidden = !hasServiceMessage
|
|
|
|
strongSelf.buttonNode.backgroundColor = overlayColor
|
|
|
|
strongSelf.animationNode.updateLayout(size: iconSize)
|
|
strongSelf.placeholderNode.frame = animationFrame
|
|
|
|
let _ = labelApply()
|
|
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 _ = buttonTitleApply()
|
|
let _ = ribbonTextApply()
|
|
let _ = moreApply()
|
|
|
|
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size)
|
|
strongSelf.labelNode.frame = labelFrame
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
|
|
strongSelf.titleNode.frame = titleFrame
|
|
|
|
let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight))
|
|
|
|
let subtitleFrame = CGRect(origin: .zero, size: subtitleLayout.size)
|
|
strongSelf.subtitleNode.textNode.frame = subtitleFrame
|
|
|
|
if isFirstTime {
|
|
strongSelf.textClippingNode.frame = clippingTextFrame
|
|
} else {
|
|
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil)
|
|
}
|
|
if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView {
|
|
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil)
|
|
animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil)
|
|
}
|
|
animation.animator.updateFrame(layer: strongSelf.moreTextNode.layer, frame: CGRect(origin: CGPoint(x: clippingTextFrame.maxX - moreLayout.size.width, y: clippingTextFrame.maxY - moreLayout.size.height), size: moreLayout.size), completion: nil)
|
|
|
|
if !subtitleLayout.spoilers.isEmpty {
|
|
let spoilerSubtitleNode = spoilerSubtitleApply(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
|
|
))
|
|
if strongSelf.spoilerSubtitleNode == nil {
|
|
spoilerSubtitleNode.textNode.alpha = 0.0
|
|
spoilerSubtitleNode.textNode.isUserInteractionEnabled = false
|
|
strongSelf.spoilerSubtitleNode = spoilerSubtitleNode
|
|
|
|
strongSelf.textClippingNode.addSubnode(spoilerSubtitleNode.textNode)
|
|
}
|
|
spoilerSubtitleNode.textNode.frame = subtitleFrame
|
|
|
|
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
|
|
|
let dustNode: InvisibleInkDustNode
|
|
if let current = strongSelf.dustNode {
|
|
dustNode = current
|
|
} else {
|
|
dustNode = InvisibleInkDustNode(textNode: spoilerSubtitleNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
|
|
strongSelf.dustNode = dustNode
|
|
strongSelf.textClippingNode.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode)
|
|
}
|
|
dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0)
|
|
dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
|
} else if let dustNode = strongSelf.dustNode {
|
|
dustNode.removeFromSupernode()
|
|
strongSelf.dustNode = nil
|
|
}
|
|
|
|
let attributeSpacing: CGFloat = 6.0
|
|
let attributeVerticalSpacing: CGFloat = 22.0
|
|
var attributeMidpoints: [CGFloat] = []
|
|
|
|
func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?) {
|
|
if let titleLayout, let valueLayout {
|
|
let totalWidth = titleLayout.size.width + attributeSpacing + valueLayout.size.width
|
|
let titleOffset = titleLayout.size.width + attributeSpacing / 2.0
|
|
let midpoint = (mediaBackgroundFrame.width - totalWidth) / 2.0 + titleOffset
|
|
attributeMidpoints.append(midpoint)
|
|
}
|
|
}
|
|
appendAttributeMidpoint(titleLayout: modelTitleLayoutAndApply?.0, valueLayout: modelValueLayoutAndApply?.0)
|
|
appendAttributeMidpoint(titleLayout: backdropTitleLayoutAndApply?.0, valueLayout: backdropValueLayoutAndApply?.0)
|
|
appendAttributeMidpoint(titleLayout: symbolTitleLayoutAndApply?.0, valueLayout: symbolValueLayoutAndApply?.0)
|
|
|
|
let middleX = attributeMidpoints.isEmpty ? mediaBackgroundFrame.width / 2.0 : attributeMidpoints.reduce(0, +) / CGFloat(attributeMidpoints.count)
|
|
|
|
let titleMaxX: CGFloat = mediaBackgroundFrame.minX + middleX - attributeSpacing / 2.0
|
|
let valueMinX: CGFloat = mediaBackgroundFrame.minX + middleX + attributeSpacing / 2.0
|
|
|
|
func positionAttributeNodes(
|
|
titleTextNode: TextNode,
|
|
valueTextNode: TextNode,
|
|
titleLayoutAndApply: (TextNodeLayout, () -> TextNode)?,
|
|
valueLayoutAndApply: (TextNodeLayout, () -> TextNode)?,
|
|
yOffset: CGFloat
|
|
) {
|
|
if let (titleLayout, titleApply) = titleLayoutAndApply {
|
|
if titleTextNode.supernode == nil {
|
|
strongSelf.addSubnode(titleTextNode)
|
|
}
|
|
let _ = titleApply()
|
|
titleTextNode.frame = CGRect(
|
|
origin: CGPoint(x: titleMaxX - titleLayout.size.width, y: clippingTextFrame.maxY + yOffset),
|
|
size: titleLayout.size
|
|
)
|
|
}
|
|
if let (valueLayout, valueApply) = valueLayoutAndApply {
|
|
if valueTextNode.supernode == nil {
|
|
strongSelf.addSubnode(valueTextNode)
|
|
}
|
|
let _ = valueApply()
|
|
valueTextNode.frame = CGRect(
|
|
origin: CGPoint(x: valueMinX, y: clippingTextFrame.maxY + yOffset),
|
|
size: valueLayout.size
|
|
)
|
|
}
|
|
}
|
|
|
|
positionAttributeNodes(
|
|
titleTextNode: strongSelf.modelTitleTextNode,
|
|
valueTextNode: strongSelf.modelValueTextNode,
|
|
titleLayoutAndApply: modelTitleLayoutAndApply,
|
|
valueLayoutAndApply: modelValueLayoutAndApply,
|
|
yOffset: 10.0
|
|
)
|
|
positionAttributeNodes(
|
|
titleTextNode: strongSelf.backdropTitleTextNode,
|
|
valueTextNode: strongSelf.backdropValueTextNode,
|
|
titleLayoutAndApply: backdropTitleLayoutAndApply,
|
|
valueLayoutAndApply: backdropValueLayoutAndApply,
|
|
yOffset: 10.0 + attributeVerticalSpacing
|
|
)
|
|
positionAttributeNodes(
|
|
titleTextNode: strongSelf.symbolTitleTextNode,
|
|
valueTextNode: strongSelf.symbolValueTextNode,
|
|
titleLayoutAndApply: symbolTitleLayoutAndApply,
|
|
valueLayoutAndApply: symbolValueLayoutAndApply,
|
|
yOffset: 10.0 + attributeVerticalSpacing * 2
|
|
)
|
|
|
|
var buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
|
|
var buttonOriginY = clippingTextFrame.maxY + 10.0
|
|
if modelTitleLayoutAndApply != nil {
|
|
buttonOriginY = clippingTextFrame.maxY + 80.0
|
|
}
|
|
strongSelf.buttonTitleNode.frame = CGRect(origin: CGPoint(x: 19.0, y: 8.0), size: buttonTitleLayout.size)
|
|
|
|
if let buttonIcon {
|
|
buttonSize.width += 15.0
|
|
|
|
let buttonIconNode: DefaultAnimatedStickerNodeImpl
|
|
if let current = strongSelf.buttonIconNode {
|
|
buttonIconNode = current
|
|
} else {
|
|
if animation.isAnimated {
|
|
if let snapshotView = strongSelf.buttonContentNode.view.snapshotView(afterScreenUpdates: false) {
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
snapshotView.removeFromSuperview()
|
|
})
|
|
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
|
strongSelf.buttonNode.view.addSubview(snapshotView)
|
|
}
|
|
strongSelf.buttonContentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
strongSelf.buttonContentNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
buttonIconNode = DefaultAnimatedStickerNodeImpl()
|
|
buttonIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: buttonIcon), width: 60, height: 60, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
|
strongSelf.buttonContentNode.addSubnode(buttonIconNode)
|
|
strongSelf.buttonIconNode = buttonIconNode
|
|
buttonIconNode.playLoop()
|
|
}
|
|
let iconSize = CGSize(width: 20.0, height: 20.0)
|
|
buttonIconNode.frame = CGRect(origin: CGPoint(x: buttonSize.width - iconSize.width - 13.0, y: 7.0), size: iconSize)
|
|
buttonIconNode.updateLayout(size: iconSize)
|
|
buttonIconNode.visibility = strongSelf.visibilityStatus == true
|
|
buttonIconNode.dynamicColor = primaryTextColor
|
|
} else if let buttonIconNode = strongSelf.buttonIconNode {
|
|
if animation.isAnimated {
|
|
if let snapshotView = strongSelf.buttonContentNode.view.snapshotView(afterScreenUpdates: false) {
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
snapshotView.removeFromSuperview()
|
|
})
|
|
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
|
strongSelf.buttonNode.view.addSubview(snapshotView)
|
|
}
|
|
strongSelf.buttonContentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
strongSelf.buttonContentNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
strongSelf.buttonIconNode = nil
|
|
buttonIconNode.removeFromSupernode()
|
|
}
|
|
|
|
animation.animator.updateFrame(layer: strongSelf.buttonNode.layer, frame: CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: buttonOriginY), size: buttonSize), completion: nil)
|
|
strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize)
|
|
strongSelf.buttonContentNode.frame = CGRect(origin: .zero, size: buttonSize)
|
|
|
|
if ribbonTextLayout.size.width > 0.0 {
|
|
if strongSelf.ribbonBackgroundNode.image == nil {
|
|
if let uniqueBackgroundColor {
|
|
let colors = [
|
|
uniqueBackgroundColor.withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89),
|
|
uniqueBackgroundColor.withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04)
|
|
]
|
|
strongSelf.ribbonBackgroundNode.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: colors, direction: .mirroredDiagonal)
|
|
} else {
|
|
strongSelf.ribbonBackgroundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/GiftRibbon"), color: overlayColor)
|
|
}
|
|
}
|
|
if let ribbonImage = strongSelf.ribbonBackgroundNode.image {
|
|
var ribbonFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.maxX - ribbonImage.size.width + 2.0, y: mediaBackgroundFrame.minY - 2.0), size: ribbonImage.size)
|
|
if let _ = uniqueBackgroundColor {
|
|
ribbonFrame = ribbonFrame.offsetBy(dx: -4.0, dy: 4.0)
|
|
}
|
|
strongSelf.ribbonBackgroundNode.frame = ribbonFrame
|
|
|
|
strongSelf.ribbonTextNode.transform = CATransform3DMakeRotation(.pi / 4.0, 0.0, 0.0, 1.0)
|
|
strongSelf.ribbonTextNode.bounds = CGRect(origin: .zero, size: ribbonTextLayout.size)
|
|
strongSelf.ribbonTextNode.position = ribbonFrame.center.offsetBy(dx: 7.0, dy: -6.0)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if ribbonTextLayout.size.width > 0.0, uniqueBackgroundColor == nil {
|
|
let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0)
|
|
backgroundContent.frame = backgroundMaskFrame
|
|
animation.animator.updateFrame(layer: backgroundContent.layer, frame: backgroundMaskFrame, completion: nil)
|
|
backgroundContent.cornerRadius = 0.0
|
|
|
|
if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size {
|
|
strongSelf.mediaBackgroundMaskNode.image = generateImage(backgroundMaskFrame.size, contextGenerator: { size, context in
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
context.clear(bounds)
|
|
|
|
context.setFillColor(UIColor.black.cgColor)
|
|
context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: 2.0, dy: 2.0), cornerRadius: 24.0).cgPath)
|
|
context.fillPath()
|
|
|
|
if let ribbonImage = UIImage(bundleImageName: "Chat/Message/GiftRibbon"), let cgImage = ribbonImage.cgImage {
|
|
context.draw(cgImage, in: CGRect(origin: CGPoint(x: bounds.width - ribbonImage.size.width, y: bounds.height - ribbonImage.size.height), size: ribbonImage.size), byTiling: false)
|
|
}
|
|
})
|
|
}
|
|
backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view
|
|
strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size)
|
|
} else {
|
|
animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil)
|
|
backgroundContent.clipsToBounds = true
|
|
backgroundContent.cornerRadius = 24.0
|
|
backgroundContent.view.mask = nil
|
|
}
|
|
}
|
|
|
|
if let uniqueBackgroundColor, let uniqueSecondBackgroundColor, let uniquePatternColor, let uniquePatternFile {
|
|
let patternInset: CGFloat = 4.0
|
|
let patternSize = CGSize(width: mediaBackgroundFrame.width - patternInset * 2.0, height: mediaBackgroundFrame.height - patternInset * 2.0)
|
|
let files: [Int64: TelegramMediaFile] = [uniquePatternFile.fileId.id: uniquePatternFile]
|
|
let _ = strongSelf.patternView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(PeerInfoCoverComponent(
|
|
context: item.context,
|
|
subject: .custom(uniqueBackgroundColor, uniqueSecondBackgroundColor, uniquePatternColor, uniquePatternFile.fileId.id),
|
|
files: files,
|
|
isDark: false,
|
|
avatarCenter: CGPoint(x: patternSize.width / 2.0, y: 104.0),
|
|
avatarScale: 1.0,
|
|
defaultHeight: patternSize.height,
|
|
avatarTransitionFraction: 0.0,
|
|
patternTransitionFraction: 0.0
|
|
)),
|
|
environment: {},
|
|
containerSize: patternSize
|
|
)
|
|
if let backgroundView = strongSelf.patternView.view {
|
|
if backgroundView.superview == nil {
|
|
backgroundView.layer.cornerRadius = 20.0
|
|
backgroundView.clipsToBounds = true
|
|
strongSelf.view.insertSubview(backgroundView, belowSubview: strongSelf.titleNode.view)
|
|
}
|
|
backgroundView.frame = CGRect(origin: .zero, size: patternSize).offsetBy(dx: mediaBackgroundFrame.minX + patternInset, dy: mediaBackgroundFrame.minY + patternInset)
|
|
}
|
|
}
|
|
|
|
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
|
|
if let (offset, image) = backgroundMaskImage {
|
|
if strongSelf.backgroundNode == nil {
|
|
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
|
strongSelf.backgroundNode = backgroundNode
|
|
strongSelf.insertSubnode(backgroundNode, at: 0)
|
|
}
|
|
}
|
|
|
|
if backgroundMaskUpdated, let backgroundNode = strongSelf.backgroundNode {
|
|
if labelRects.count == 1 {
|
|
backgroundNode.clipsToBounds = true
|
|
backgroundNode.cornerRadius = labelRects[0].height / 2.0
|
|
backgroundNode.view.mask = nil
|
|
} else {
|
|
backgroundNode.clipsToBounds = false
|
|
backgroundNode.cornerRadius = 0.0
|
|
backgroundNode.view.mask = strongSelf.backgroundMaskNode.view
|
|
}
|
|
}
|
|
|
|
if let backgroundNode = strongSelf.backgroundNode {
|
|
backgroundNode.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size)
|
|
}
|
|
strongSelf.backgroundMaskNode.image = image
|
|
strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size)
|
|
|
|
strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects)
|
|
}
|
|
if let (rect, size) = strongSelf.absoluteRect {
|
|
strongSelf.updateAbsoluteRect(rect, within: size)
|
|
}
|
|
|
|
if canExpand, let maskView = strongSelf.maskView {
|
|
if maskView.image == nil {
|
|
maskView.image = generateMaskImage()
|
|
}
|
|
strongSelf.textClippingNode.view.mask = strongSelf.maskView
|
|
|
|
animation.animator.updateAlpha(layer: strongSelf.moreTextNode.layer, alpha: strongSelf.isExpanded ? 0.0 : 1.0, completion: nil)
|
|
} else {
|
|
strongSelf.textClippingNode.view.mask = nil
|
|
strongSelf.moreTextNode.alpha = 0.0
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize)
|
|
|
|
if let backgroundNode = self.backgroundNode {
|
|
var backgroundFrame = backgroundNode.frame
|
|
backgroundFrame.origin.x += rect.minX
|
|
backgroundFrame.origin.y += rect.minY
|
|
backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
|
|
if let backgroundNode = self.backgroundNode {
|
|
backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration)
|
|
}
|
|
}
|
|
|
|
override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
|
if let backgroundNode = self.backgroundNode {
|
|
backgroundNode.offsetSpring(value: value, duration: duration, damping: damping)
|
|
}
|
|
}
|
|
|
|
override public func updateTouchesAtPoint(_ point: CGPoint?) {
|
|
if let item = self.item {
|
|
var rects: [(CGRect, CGRect)]?
|
|
let textNodeFrame = self.labelNode.frame
|
|
if let point = point {
|
|
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) {
|
|
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.labelNode.lineAndAttributeRects(name: name, at: index)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let rects = rects {
|
|
var mappedRects: [CGRect] = []
|
|
for i in 0 ..< rects.count {
|
|
let lineRect = rects[i].0
|
|
var itemRect = rects[i].1
|
|
itemRect.origin.x = floor((textNodeFrame.size.width - lineRect.width) / 2.0) + itemRect.origin.x
|
|
mappedRects.append(itemRect)
|
|
}
|
|
|
|
let linkHighlightingNode: LinkHighlightingNode
|
|
if let current = self.linkHighlightingNode {
|
|
linkHighlightingNode = current
|
|
} else {
|
|
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
|
linkHighlightingNode = LinkHighlightingNode(color: serviceColor.linkHighlight)
|
|
linkHighlightingNode.inset = 2.5
|
|
self.linkHighlightingNode = linkHighlightingNode
|
|
self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode)
|
|
}
|
|
linkHighlightingNode.frame = self.labelNode.frame.offsetBy(dx: 0.0, dy: 1.5)
|
|
linkHighlightingNode.updateRects(mappedRects)
|
|
} 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()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
|
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - self.labelNode.frame.minX, y: point.y - self.labelNode.frame.minY - 10.0)), gesture == .tap {
|
|
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
var concealed = true
|
|
if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
|
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
|
}
|
|
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
|
|
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
|
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
|
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
|
|
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
|
|
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
|
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
|
|
}
|
|
}
|
|
|
|
if let (_, attributes) = self.subtitleNode.textNode.attributesAtPoint(CGPoint(x: point.x - self.textClippingNode.frame.minX, y: point.y - self.textClippingNode.frame.minY)), gesture == .tap {
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], let dustNode = self.dustNode, !dustNode.isRevealed {
|
|
return ChatMessageBubbleContentTapAction(content: .none)
|
|
}
|
|
}
|
|
|
|
if self.buttonNode.frame.contains(point) {
|
|
return ChatMessageBubbleContentTapAction(content: .ignore)
|
|
} else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero {
|
|
return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in
|
|
self?.expandPressed()
|
|
}))
|
|
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
|
|
return ChatMessageBubbleContentTapAction(content: .openMessage)
|
|
} else if self.mediaBackgroundContent?.frame.contains(point) == true {
|
|
return ChatMessageBubbleContentTapAction(content: .openMessage)
|
|
} else {
|
|
return ChatMessageBubbleContentTapAction(content: .none)
|
|
}
|
|
}
|
|
|
|
override public func unreadMessageRangeUpdated() {
|
|
self.updateVisibility()
|
|
}
|
|
|
|
private var internalPlayedOnce = false
|
|
private func updateVisibility() {
|
|
guard let item = self.item else {
|
|
return
|
|
}
|
|
|
|
let isPlaying = self.visibilityStatus == true
|
|
if self.isPlaying != isPlaying {
|
|
self.isPlaying = isPlaying
|
|
self.animationNode.visibility = isPlaying
|
|
}
|
|
self.buttonIconNode?.visibility = isPlaying
|
|
|
|
if isPlaying && self.setupTimestamp == nil {
|
|
self.setupTimestamp = CACurrentMediaTime()
|
|
}
|
|
|
|
if isPlaying {
|
|
var alreadySeen = true
|
|
|
|
if item.message.flags.contains(.Incoming) {
|
|
if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] {
|
|
if unreadRange.contains(item.message.id.id) {
|
|
alreadySeen = false
|
|
}
|
|
}
|
|
} else {
|
|
if item.controllerInteraction.playNextOutgoingGift && !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
|
alreadySeen = false
|
|
}
|
|
}
|
|
|
|
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) && !self.internalPlayedOnce {
|
|
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
|
self.animationNode.playOnce()
|
|
self.internalPlayedOnce = true
|
|
|
|
Queue.mainQueue().after(0.05) {
|
|
if let itemNode = self.itemNode, let supernode = itemNode.supernode {
|
|
supernode.addSubnode(itemNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !alreadySeen && self.animationNode.isPlaying {
|
|
item.controllerInteraction.playNextOutgoingGift = false
|
|
|
|
Queue.mainQueue().after(self.isStarGift ? 0.1 : 1.0) {
|
|
item.controllerInteraction.animateDiceSuccess(false, true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func generateMaskImage() -> UIImage? {
|
|
return generateImage(CGSize(width: 100.0, height: 30.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
context.fill(CGRect(origin: .zero, size: size))
|
|
|
|
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
|
let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
|
|
|
context.setBlendMode(.copy)
|
|
context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 12.0), size: CGSize(width: 130.0, height: 18.0)))
|
|
context.drawLinearGradient(gradient, start: CGPoint(x: 30.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
|
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 18.0, right: 70.0))
|
|
}
|