2024-06-28 19:36:15 +02:00

1304 lines
70 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import TelegramCore
import Display
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import AppBundle
import ReactionButtonListComponent
import ReactionImageComponent
import AnimationCache
import MultiAnimationRenderer
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
if let _ = layer.animation(forKey: "clockFrameAnimation") {
return
}
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.duration = duration
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
basicAnimation.beginTime = 1.0
layer.add(basicAnimation, forKey: "clockFrameAnimation")
}
public enum ChatMessageDateAndStatusOutgoingType: Equatable {
case Sent(read: Bool)
case Sending
case Failed
}
public enum ChatMessageDateAndStatusType: Equatable {
case BubbleIncoming
case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType)
case ImageIncoming
case ImageOutgoing(ChatMessageDateAndStatusOutgoingType)
case FreeIncoming
case FreeOutgoing(ChatMessageDateAndStatusOutgoingType)
}
private let reactionCountFont = Font.semibold(11.0)
private let reactionFont = Font.regular(12.0)
private final class StatusReactionNode: ASDisplayNode {
let iconView: ReactionIconView
private let iconImageDisposable = MetaDisposable()
private var theme: PresentationTheme?
private var value: MessageReaction.Reaction?
private var isSelected: Bool?
private var resolvedFile: TelegramMediaFile?
private var fileDisposable: Disposable?
private var alternativeTextView: ImmediateTextView?
override init() {
self.iconView = ReactionIconView()
super.init()
self.view.addSubview(self.iconView)
}
deinit {
self.iconImageDisposable.dispose()
self.fileDisposable?.dispose()
}
func update(context: AccountContext, type: ChatMessageDateAndStatusType, value: MessageReaction.Reaction, file: TelegramMediaFile?, fileId: Int64?, alternativeText: String, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, animated: Bool) {
if self.value != value {
self.value = value
let boundingImageSize = CGSize(width: 8.0, height: 8.0)
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingImageSize.width - boundingImageSize.width) / 2.0), y: floorToScreenPixels((boundingImageSize.height - boundingImageSize.height) / 2.0)), size: boundingImageSize)
self.iconView.frame = iconFrame
if let fileId = fileId ?? file?.fileId.id {
let animateIdle: Bool
if case .custom = value {
animateIdle = true
} else {
animateIdle = false
}
let placeholderColor: UIColor
switch type {
case .BubbleIncoming:
placeholderColor = theme.chat.message.incoming.mediaPlaceholderColor
case .BubbleOutgoing:
placeholderColor = theme.chat.message.incoming.mediaPlaceholderColor
case .ImageIncoming:
placeholderColor = UIColor(white: 1.0, alpha: 0.1)
case .ImageOutgoing:
placeholderColor = UIColor(white: 1.0, alpha: 0.1)
case .FreeIncoming:
placeholderColor = UIColor(white: 0.0, alpha: 0.1)
case .FreeOutgoing:
placeholderColor = UIColor(white: 0.0, alpha: 0.1)
}
self.iconView.update(
size: boundingImageSize,
context: context,
file: file,
fileId: fileId,
animationCache: animationCache,
animationRenderer: animationRenderer,
tintColor: nil,
placeholderColor: placeholderColor,
animateIdle: animateIdle,
reaction: value,
transition: .immediate
)
if let alternativeTextView = self.alternativeTextView {
self.alternativeTextView = nil
alternativeTextView.removeFromSuperview()
}
} else {
let alternativeTextView: ImmediateTextView
if let current = self.alternativeTextView {
alternativeTextView = current
} else {
alternativeTextView = ImmediateTextView()
alternativeTextView.insets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
self.view.addSubview(alternativeTextView)
}
alternativeTextView.attributedText = NSAttributedString(string: alternativeText, font: Font.regular(10.0), textColor: .black)
let alternativeTextSize = alternativeTextView.updateLayout(CGSize(width: 100.0, height: 100.0))
alternativeTextView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingImageSize.width - alternativeTextSize.width) / 2.0), y: floorToScreenPixels((boundingImageSize.height - alternativeTextSize.height) / 2.0)), size: alternativeTextSize)
}
}
}
}
public class ChatMessageDateAndStatusNode: ASDisplayNode {
public struct TrailingReactionSettings {
public var displayInline: Bool
public var preferAdditionalInset: Bool
public init(displayInline: Bool, preferAdditionalInset: Bool) {
self.displayInline = displayInline
self.preferAdditionalInset = preferAdditionalInset
}
}
public struct StandaloneReactionSettings {
public init() {
}
}
public enum LayoutInput {
case trailingContent(contentWidth: CGFloat?, reactionSettings: TrailingReactionSettings?)
case standalone(reactionSettings: StandaloneReactionSettings?)
public var displayInlineReactions: Bool {
switch self {
case let .trailingContent(_, reactionSettings):
if let reactionSettings = reactionSettings {
return reactionSettings.displayInline
} else {
return false
}
case let .standalone(reactionSettings):
if let _ = reactionSettings {
return true
} else {
return false
}
}
}
}
public struct Arguments {
var context: AccountContext
var presentationData: ChatPresentationData
var edited: Bool
var impressionCount: Int?
var dateText: String
var type: ChatMessageDateAndStatusType
var layoutInput: LayoutInput
var constrainedSize: CGSize
var availableReactions: AvailableReactions?
var savedMessageTags: SavedMessageTags?
var reactions: [MessageReaction]
var reactionPeers: [(MessageReaction.Reaction, EnginePeer)]
var displayAllReactionPeers: Bool
var areReactionsTags: Bool
var messageEffect: AvailableMessageEffects.MessageEffect?
var replyCount: Int
var isPinned: Bool
var hasAutoremove: Bool
var canViewReactionList: Bool
var animationCache: AnimationCache
var animationRenderer: MultiAnimationRenderer
public init(
context: AccountContext,
presentationData: ChatPresentationData,
edited: Bool,
impressionCount: Int?,
dateText: String,
type: ChatMessageDateAndStatusType,
layoutInput: LayoutInput,
constrainedSize: CGSize,
availableReactions: AvailableReactions?,
savedMessageTags: SavedMessageTags?,
reactions: [MessageReaction],
reactionPeers: [(MessageReaction.Reaction, EnginePeer)],
displayAllReactionPeers: Bool,
areReactionsTags: Bool,
messageEffect: AvailableMessageEffects.MessageEffect?,
replyCount: Int,
isPinned: Bool,
hasAutoremove: Bool,
canViewReactionList: Bool,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer
) {
self.context = context
self.presentationData = presentationData
self.edited = edited
self.impressionCount = impressionCount == 0 ? nil : impressionCount
self.dateText = dateText
self.type = type
self.layoutInput = layoutInput
self.availableReactions = availableReactions
self.savedMessageTags = savedMessageTags
self.constrainedSize = constrainedSize
self.reactions = reactions
self.reactionPeers = reactionPeers
self.displayAllReactionPeers = displayAllReactionPeers
self.areReactionsTags = areReactionsTags
self.messageEffect = messageEffect
self.replyCount = replyCount
self.isPinned = isPinned
self.hasAutoremove = hasAutoremove
self.canViewReactionList = canViewReactionList
self.animationCache = animationCache
self.animationRenderer = animationRenderer
}
}
private var backgroundNode: ASImageNode?
private var blurredBackgroundNode: NavigationBackgroundNode?
private var checkSentNode: ASImageNode?
private var checkReadNode: ASImageNode?
private var clockFrameNode: ASImageNode?
private var clockMinNode: ASImageNode?
private let dateNode: TextNode
private var impressionIcon: ASImageNode?
private var reactionNodes: [MessageReaction.Reaction: StatusReactionNode] = [:]
private let reactionButtonsContainer = ReactionButtonsAsyncLayoutContainer()
private var reactionButtonNode: HighlightTrackingButtonNode?
private var repliesIcon: ASImageNode?
private var selfExpiringIcon: ASImageNode?
private var replyCountNode: TextNode?
private var type: ChatMessageDateAndStatusType?
private var theme: ChatPresentationThemeData?
private var layoutSize: CGSize?
private var tapGestureRecognizer: UITapGestureRecognizer?
public var openReplies: (() -> Void)?
public var pressed: (() -> Void)? {
didSet {
if self.pressed != nil {
if self.tapGestureRecognizer == nil {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapGestureRecognizer = tapGestureRecognizer
self.view.addGestureRecognizer(tapGestureRecognizer)
}
} else if let tapGestureRecognizer = self.tapGestureRecognizer{
self.tapGestureRecognizer = nil
self.view.removeGestureRecognizer(tapGestureRecognizer)
}
}
}
public var reactionSelected: ((ReactionButtonAsyncNode, MessageReaction.Reaction, ContextExtractedContentContainingView?) -> Void)?
public var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)?
override public init() {
self.dateNode = TextNode()
self.dateNode.isUserInteractionEnabled = false
self.dateNode.displaysAsynchronously = false
self.dateNode.contentsScale = UIScreenScale
self.dateNode.contentMode = .topLeft
super.init()
self.addSubnode(self.dateNode)
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.pressed?()
}
}
public func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
var checkReadNode = self.checkReadNode
var checkSentNode = self.checkSentNode
var clockFrameNode = self.clockFrameNode
var clockMinNode = self.clockMinNode
var currentBackgroundNode = self.backgroundNode
var currentImpressionIcon = self.impressionIcon
var currentRepliesIcon = self.repliesIcon
let currentType = self.type
let currentTheme = self.theme
let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode)
let reactionButtonsContainer = self.reactionButtonsContainer
return { [weak self] arguments in
let dateColor: UIColor
var backgroundImage: UIImage?
var blurredBackgroundColor: (UIColor, Bool)?
var outgoingStatus: ChatMessageDateAndStatusOutgoingType?
var leftInset: CGFloat
let loadedCheckFullImage: UIImage?
let loadedCheckPartialImage: UIImage?
let clockFrameImage: UIImage?
let clockMinImage: UIImage?
var impressionImage: UIImage?
var repliesImage: UIImage?
let themeUpdated = arguments.presentationData.theme != currentTheme || arguments.type != currentType
let graphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners)
let isDefaultWallpaper = serviceMessageColorHasDefaultWallpaper(arguments.presentationData.theme.wallpaper)
let offset: CGFloat = -UIScreenPixel
let checkSize: CGFloat = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
let reactionColors: ReactionButtonComponent.Colors
switch arguments.type {
case .BubbleIncoming, .ImageIncoming, .FreeIncoming:
let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: true, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty)
reactionColors = ReactionButtonComponent.Colors(
deselectedBackground: themeColors.reactionInactiveBackground.argb,
selectedBackground: themeColors.reactionActiveBackground.argb,
deselectedForeground: themeColors.reactionInactiveForeground.argb,
selectedForeground: themeColors.reactionActiveForeground.argb,
deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb,
selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb,
deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb,
selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb,
extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb,
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
)
case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing:
let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: false, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty)
reactionColors = ReactionButtonComponent.Colors(
deselectedBackground: themeColors.reactionInactiveBackground.argb,
selectedBackground: themeColors.reactionActiveBackground.argb,
deselectedForeground: themeColors.reactionInactiveForeground.argb,
selectedForeground: themeColors.reactionActiveForeground.argb,
deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb,
selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb,
deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb,
selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb,
extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb,
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
)
}
switch arguments.type {
case .BubbleIncoming:
dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor
leftInset = 5.0
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
clockFrameImage = graphics.clockBubbleIncomingFrameImage
clockMinImage = graphics.clockBubbleIncomingMinImage
if arguments.impressionCount != nil {
impressionImage = graphics.incomingDateAndStatusImpressionIcon
}
if arguments.replyCount != 0 {
repliesImage = graphics.incomingDateAndStatusRepliesIcon
} else if arguments.isPinned {
repliesImage = graphics.incomingDateAndStatusPinnedIcon
}
case let .BubbleOutgoing(status):
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
outgoingStatus = status
leftInset = 5.0
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
clockFrameImage = graphics.clockBubbleOutgoingFrameImage
clockMinImage = graphics.clockBubbleOutgoingMinImage
if arguments.impressionCount != nil {
impressionImage = graphics.outgoingDateAndStatusImpressionIcon
}
if arguments.replyCount != 0 {
repliesImage = graphics.outgoingDateAndStatusRepliesIcon
} else if arguments.isPinned {
repliesImage = graphics.outgoingDateAndStatusPinnedIcon
}
case .ImageIncoming:
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
backgroundImage = graphics.dateAndStatusMediaBackground
leftInset = 0.0
loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(arguments.presentationData.theme.theme, size: checkSize)
loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
clockFrameImage = graphics.clockMediaFrameImage
clockMinImage = graphics.clockMediaMinImage
if arguments.impressionCount != nil {
impressionImage = graphics.mediaImpressionIcon
}
if arguments.replyCount != 0 {
repliesImage = graphics.mediaRepliesIcon
} else if arguments.isPinned {
repliesImage = graphics.mediaPinnedIcon
}
case let .ImageOutgoing(status):
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
outgoingStatus = status
backgroundImage = graphics.dateAndStatusMediaBackground
leftInset = 0.0
loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(arguments.presentationData.theme.theme, size: checkSize)
loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
clockFrameImage = graphics.clockMediaFrameImage
clockMinImage = graphics.clockMediaMinImage
if arguments.impressionCount != nil {
impressionImage = graphics.mediaImpressionIcon
}
if arguments.replyCount != 0 {
repliesImage = graphics.mediaRepliesIcon
} else if arguments.isPinned {
repliesImage = graphics.mediaPinnedIcon
}
case .FreeIncoming:
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
dateColor = serviceColor.primaryText
blurredBackgroundColor = (selectDateFillStaticColor(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), arguments.context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper))
leftInset = 0.0
loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
clockFrameImage = graphics.clockFreeFrameImage
clockMinImage = graphics.clockFreeMinImage
if arguments.impressionCount != nil {
impressionImage = graphics.freeImpressionIcon
}
if arguments.replyCount != 0 {
repliesImage = graphics.freeRepliesIcon
} else if arguments.isPinned {
repliesImage = graphics.freePinnedIcon
}
case let .FreeOutgoing(status):
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
dateColor = serviceColor.primaryText
outgoingStatus = status
blurredBackgroundColor = (selectDateFillStaticColor(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), arguments.context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper))
leftInset = 0.0
loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
clockFrameImage = graphics.clockFreeFrameImage
clockMinImage = graphics.clockFreeMinImage
if arguments.impressionCount != nil {
impressionImage = graphics.freeImpressionIcon
}
if arguments.replyCount != 0 {
repliesImage = graphics.freeRepliesIcon
} else if arguments.isPinned {
repliesImage = graphics.freePinnedIcon
}
}
var updatedDateText = arguments.dateText
if arguments.edited {
updatedDateText = "\(arguments.presentationData.strings.Conversation_MessageEditedLabel) \(updatedDateText)"
}
if let impressionCount = arguments.impressionCount {
updatedDateText = compactNumericCountString(impressionCount, decimalSeparator: arguments.presentationData.dateTimeFormat.decimalSeparator) + " " + updatedDateText
}
let dateFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
let (date, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: arguments.constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let checkOffset = floor(arguments.presentationData.fontSize.baseDisplaySize * 6.0 / 17.0)
let statusWidth: CGFloat
var checkSentFrame: CGRect?
var checkReadFrame: CGRect?
var clockPosition = CGPoint()
var impressionSize = CGSize()
var impressionWidth: CGFloat = 0.0
if let impressionImage = impressionImage {
if currentImpressionIcon == nil {
let iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displayWithoutProcessing = true
iconNode.displaysAsynchronously = false
currentImpressionIcon = iconNode
}
impressionSize = impressionImage.size
impressionWidth = impressionSize.width + 3.0
} else {
currentImpressionIcon = nil
}
var repliesIconSize = CGSize()
if let repliesImage = repliesImage {
if currentRepliesIcon == nil {
let iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displayWithoutProcessing = true
iconNode.displaysAsynchronously = false
currentRepliesIcon = iconNode
}
repliesIconSize = repliesImage.size
} else {
currentRepliesIcon = nil
}
if let outgoingStatus = outgoingStatus {
switch outgoingStatus {
case .Sending:
statusWidth = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
if checkReadNode == nil {
checkReadNode = ASImageNode()
checkReadNode?.isLayerBacked = true
checkReadNode?.displaysAsynchronously = false
checkReadNode?.displayWithoutProcessing = true
}
if checkSentNode == nil {
checkSentNode = ASImageNode()
checkSentNode?.isLayerBacked = true
checkSentNode?.displaysAsynchronously = false
checkSentNode?.displayWithoutProcessing = true
}
if clockFrameNode == nil {
clockFrameNode = ASImageNode()
clockFrameNode?.isLayerBacked = true
clockFrameNode?.displaysAsynchronously = false
clockFrameNode?.displayWithoutProcessing = true
clockFrameNode?.frame = CGRect(origin: CGPoint(), size: clockFrameImage?.size ?? CGSize())
}
if clockMinNode == nil {
clockMinNode = ASImageNode()
clockMinNode?.isLayerBacked = true
clockMinNode?.displaysAsynchronously = false
clockMinNode?.displayWithoutProcessing = true
clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize())
}
clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5 + offset)
case let .Sent(read):
let hideStatus: Bool
switch arguments.type {
case .BubbleOutgoing, .FreeOutgoing, .ImageOutgoing:
hideStatus = false
default:
hideStatus = arguments.impressionCount != nil
}
if hideStatus {
statusWidth = 0.0
checkReadNode = nil
checkSentNode = nil
clockFrameNode = nil
clockMinNode = nil
} else {
statusWidth = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
if checkReadNode == nil {
checkReadNode = ASImageNode()
checkReadNode?.isLayerBacked = true
checkReadNode?.displaysAsynchronously = false
checkReadNode?.displayWithoutProcessing = true
}
if checkSentNode == nil {
checkSentNode = ASImageNode()
checkSentNode?.isLayerBacked = true
checkSentNode?.displaysAsynchronously = false
checkSentNode?.displayWithoutProcessing = true
}
clockFrameNode = nil
clockMinNode = nil
let checkSize = loadedCheckFullImage!.size
if read {
checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize)
}
checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize)
}
case .Failed:
statusWidth = 0.0
checkReadNode = nil
checkSentNode = nil
clockFrameNode = nil
clockMinNode = nil
}
} else {
statusWidth = 0.0
checkReadNode = nil
checkSentNode = nil
clockFrameNode = nil
clockMinNode = nil
}
var backgroundInsets = UIEdgeInsets()
if let _ = backgroundImage {
if currentBackgroundNode == nil {
let backgroundNode = ASImageNode()
backgroundNode.isLayerBacked = true
backgroundNode.displayWithoutProcessing = true
backgroundNode.displaysAsynchronously = false
currentBackgroundNode = backgroundNode
}
backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0)
} else if blurredBackgroundColor != nil {
backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0)
}
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let reactionSize: CGFloat = 8.0
let reactionSpacing: CGFloat = 2.0
let reactionTrailingSpacing: CGFloat = 6.0
var reactionInset: CGFloat = 0.0
if arguments.replyCount > 0 {
let countString: String
if arguments.replyCount > 1000000 {
countString = "\(arguments.replyCount / 1000000)M"
} else if arguments.replyCount > 1000 {
countString = "\(arguments.replyCount / 1000)K"
} else {
countString = "\(arguments.replyCount)"
}
let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
replyCountLayoutAndApply = layoutAndApply
} else if arguments.isPinned {
reactionInset += 12.0
}
if arguments.messageEffect != nil {
reactionInset += 13.0
}
leftInset += reactionInset
let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
let verticalReactionsInset: CGFloat
let verticalInset: CGFloat
let resultingWidth: CGFloat
let resultingHeight: CGFloat
let reactionButtonsResult: ReactionButtonsAsyncLayoutContainer.Result
switch arguments.layoutInput {
case .standalone:
verticalReactionsInset = 0.0
verticalInset = 0.0
resultingWidth = layoutSize.width
resultingHeight = layoutSize.height
reactionButtonsResult = reactionButtonsContainer.update(
context: arguments.context,
action: { itemNode, value, sourceView in
guard let strongSelf = self else {
return
}
strongSelf.reactionSelected?(itemNode, value, sourceView)
},
reactions: [],
colors: reactionColors,
isTag: arguments.areReactionsTags,
constrainedWidth: arguments.constrainedSize.width
)
case let .trailingContent(contentWidth, reactionSettings):
if let reactionSettings = reactionSettings, !reactionSettings.displayInline {
var totalReactionCount: Int = 0
for reaction in arguments.reactions {
totalReactionCount += Int(reaction.count)
}
reactionButtonsResult = reactionButtonsContainer.update(
context: arguments.context,
action: { itemNode, value, sourceView in
guard let strongSelf = self else {
return
}
strongSelf.reactionSelected?(itemNode, value, sourceView)
},
reactions: arguments.reactions.map { reaction in
var centerAnimation: TelegramMediaFile?
var animationFileId: Int64?
switch reaction.value {
case .builtin:
if let availableReactions = arguments.availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction.value {
centerAnimation = availableReaction.centerAnimation
break
}
}
}
case let .custom(fileId):
animationFileId = fileId
}
var peers: [EnginePeer] = []
for (value, peer) in arguments.reactionPeers {
if value == reaction.value {
if !peers.contains(where: { $0.id == peer.id }) {
peers.append(peer)
}
}
}
if !arguments.displayAllReactionPeers {
if peers.count != Int(reaction.count) || arguments.reactionPeers.count != totalReactionCount {
peers.removeAll()
}
}
var title: String?
if arguments.areReactionsTags, let savedMessageTags = arguments.savedMessageTags {
for tag in savedMessageTags.tags {
if tag.reaction == reaction.value {
title = tag.title
}
}
}
return ReactionButtonsAsyncLayoutContainer.Reaction(
reaction: ReactionButtonComponent.Reaction(
value: reaction.value,
centerAnimation: centerAnimation,
animationFileId: animationFileId,
title: title
),
count: Int(reaction.count),
peers: arguments.areReactionsTags ? [] : peers,
chosenOrder: reaction.chosenOrder
)
},
colors: reactionColors,
isTag: arguments.areReactionsTags,
constrainedWidth: arguments.constrainedSize.width
)
} else {
reactionButtonsResult = reactionButtonsContainer.update(
context: arguments.context,
action: { itemNode, value, sourceView in
guard let strongSelf = self else {
return
}
strongSelf.reactionSelected?(itemNode, value, sourceView)
},
reactions: [],
colors: reactionColors,
isTag: arguments.areReactionsTags,
constrainedWidth: arguments.constrainedSize.width
)
}
var reactionButtonsSize = CGSize()
var currentRowWidth: CGFloat = 0.0
for item in reactionButtonsResult.items {
if currentRowWidth + item.size.width > arguments.constrainedSize.width {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0
}
reactionButtonsSize.height += item.size.height
currentRowWidth = 0.0
}
if !currentRowWidth.isZero {
currentRowWidth += 6.0
}
currentRowWidth += item.size.width
}
if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0
}
reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
}
if reactionButtonsSize.width.isZero {
verticalReactionsInset = 0.0
if let contentWidth {
if contentWidth + layoutSize.width > arguments.constrainedSize.width {
resultingWidth = layoutSize.width
verticalInset = 0.0
resultingHeight = layoutSize.height + verticalInset
} else {
resultingWidth = contentWidth + layoutSize.width
verticalInset = -layoutSize.height
resultingHeight = 0.0
}
} else {
resultingWidth = layoutSize.width
verticalInset = 0.0
resultingHeight = layoutSize.height + verticalInset
}
} else {
var additionalVerticalInset: CGFloat = 0.0
if let reactionSettings = reactionSettings {
if reactionSettings.preferAdditionalInset {
verticalReactionsInset = 8.0
additionalVerticalInset += 1.0
} else {
verticalReactionsInset = 3.0
}
} else {
verticalReactionsInset = 0.0
}
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0 + layoutSize.height
verticalInset = verticalReactionsInset + reactionButtonsSize.height + 3.0
} else {
resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height + additionalVerticalInset
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0
}
}
}
return (resultingWidth, { boundingWidth in
return (CGSize(width: boundingWidth, height: resultingHeight), { animation in
if let strongSelf = self {
let leftOffset = boundingWidth - layoutSize.width
strongSelf.theme = arguments.presentationData.theme
strongSelf.type = arguments.type
strongSelf.layoutSize = layoutSize
let reactionButtons = reactionButtonsResult.apply(
animation,
ReactionButtonsAsyncLayoutContainer.Arguments(
animationCache: arguments.animationCache,
animationRenderer: arguments.animationRenderer
)
)
var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset)
for item in reactionButtons.items {
if reactionButtonPosition.x + item.size.width > boundingWidth {
reactionButtonPosition.x = -1.0
reactionButtonPosition.y += item.size.height + 6.0
}
if item.node.view.superview != strongSelf.view {
assert(item.node.view.superview == nil)
strongSelf.view.addSubview(item.node.view)
item.node.view.frame = CGRect(origin: reactionButtonPosition, size: item.size)
if animation.isAnimated {
item.node.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
item.node.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
animation.animator.updateFrame(layer: item.node.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil)
}
let itemValue = item.value
let itemNode = item.node
item.node.view.isGestureEnabled = true
let canViewReactionList = arguments.canViewReactionList
item.node.view.activateAfterCompletion = !canViewReactionList
item.node.view.activated = { [weak itemNode] gesture, _ in
guard let strongSelf = self else {
return
}
guard let itemNode = itemNode else {
return
}
if let openReactionPreview = strongSelf.openReactionPreview {
openReactionPreview(gesture, itemNode.view.containerView, itemValue)
} else {
gesture.cancel()
}
}
reactionButtonPosition.x += item.size.width + 6.0
}
for node in reactionButtons.removedNodes {
if animation.isAnimated {
node.view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
node.view.removeFromSuperview()
})
} else {
node.view.removeFromSuperview()
}
}
if backgroundImage != nil {
if let currentBackgroundNode = currentBackgroundNode {
if currentBackgroundNode.supernode == nil {
strongSelf.backgroundNode = currentBackgroundNode
currentBackgroundNode.image = backgroundImage
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
} else if themeUpdated {
currentBackgroundNode.image = backgroundImage
}
}
if let backgroundNode = strongSelf.backgroundNode {
animation.animator.updateFrame(layer: backgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
}
} else {
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.removeFromSupernode()
strongSelf.backgroundNode = nil
}
}
if let blurredBackgroundColor = blurredBackgroundColor {
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, animator: animation.animator)
} else {
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
strongSelf.blurredBackgroundNode = blurredBackgroundNode
strongSelf.insertSubnode(blurredBackgroundNode, at: 0)
blurredBackgroundNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: .immediate)
}
} else if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
strongSelf.blurredBackgroundNode = nil
blurredBackgroundNode.removeFromSupernode()
}
let _ = dateApply()
if let currentImpressionIcon = currentImpressionIcon {
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
currentImpressionIcon.displaysAsynchronously = false
if currentImpressionIcon.image !== impressionImage {
currentImpressionIcon.image = impressionImage
}
if currentImpressionIcon.supernode == nil {
strongSelf.impressionIcon = currentImpressionIcon
strongSelf.addSubnode(currentImpressionIcon)
currentImpressionIcon.frame = impressionIconFrame
} else {
animation.animator.updateFrame(layer: currentImpressionIcon.layer, frame: impressionIconFrame, completion: nil)
}
} else if let impressionIcon = strongSelf.impressionIcon {
impressionIcon.removeFromSupernode()
strongSelf.impressionIcon = nil
}
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
if let clockFrameNode = clockFrameNode {
let clockPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset)
if strongSelf.clockFrameNode == nil {
strongSelf.clockFrameNode = clockFrameNode
clockFrameNode.image = clockFrameImage
strongSelf.addSubnode(clockFrameNode)
clockFrameNode.position = clockPosition
} else {
if themeUpdated {
clockFrameNode.image = clockFrameImage
}
animation.animator.updatePosition(layer: clockFrameNode.layer, position: clockPosition, completion: nil)
}
if let clockFrameNode = strongSelf.clockFrameNode {
maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0)
}
} else if let clockFrameNode = strongSelf.clockFrameNode {
clockFrameNode.removeFromSupernode()
strongSelf.clockFrameNode = nil
}
if let clockMinNode = clockMinNode {
let clockMinPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset)
if strongSelf.clockMinNode == nil {
strongSelf.clockMinNode = clockMinNode
clockMinNode.image = clockMinImage
strongSelf.addSubnode(clockMinNode)
clockMinNode.position = clockMinPosition
} else {
if themeUpdated {
clockMinNode.image = clockMinImage
}
animation.animator.updatePosition(layer: clockMinNode.layer, position: clockMinPosition, completion: nil)
}
if let clockMinNode = strongSelf.clockMinNode {
maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0)
}
} else if let clockMinNode = strongSelf.clockMinNode {
clockMinNode.removeFromSupernode()
strongSelf.clockMinNode = nil
}
if let checkSentNode = checkSentNode, let checkReadNode = checkReadNode {
var animateSentNode = false
if strongSelf.checkSentNode == nil {
checkSentNode.image = loadedCheckFullImage
strongSelf.checkSentNode = checkSentNode
strongSelf.addSubnode(checkSentNode)
animateSentNode = animation.isAnimated
} else if themeUpdated {
checkSentNode.image = loadedCheckFullImage
}
if let checkSentFrame = checkSentFrame {
let actualCheckSentFrame = checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
if checkSentNode.isHidden {
animateSentNode = animation.isAnimated
checkSentNode.isHidden = false
checkSentNode.frame = actualCheckSentFrame
} else {
animation.animator.updateFrame(layer: checkSentNode.layer, frame: actualCheckSentFrame, completion: nil)
}
} else {
checkSentNode.isHidden = true
}
var animateReadNode = false
if strongSelf.checkReadNode == nil {
animateReadNode = animation.isAnimated
checkReadNode.image = loadedCheckPartialImage
strongSelf.checkReadNode = checkReadNode
strongSelf.addSubnode(checkReadNode)
} else if themeUpdated {
checkReadNode.image = loadedCheckPartialImage
}
if let checkReadFrame = checkReadFrame {
if checkReadNode.isHidden {
animateReadNode = animation.isAnimated
checkReadNode.frame = checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
} else {
animation.animator.updateFrame(layer: checkReadNode.layer, frame: checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset), completion: nil)
}
checkReadNode.isHidden = false
} else {
checkReadNode.isHidden = true
}
if animateSentNode {
strongSelf.checkSentNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1)
}
if animateReadNode {
strongSelf.checkReadNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1)
}
} else if let checkSentNode = strongSelf.checkSentNode, let checkReadNode = strongSelf.checkReadNode {
checkSentNode.removeFromSupernode()
checkReadNode.removeFromSupernode()
strongSelf.checkSentNode = nil
strongSelf.checkReadNode = nil
}
var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left
if let messageEffect = arguments.messageEffect {
var validReactions = Set<MessageReaction.Reaction>()
do {
let node: StatusReactionNode
var animateNode = true
if let current = strongSelf.reactionNodes[.custom(messageEffect.id)] {
node = current
} else {
animateNode = false
node = StatusReactionNode()
strongSelf.reactionNodes[.custom(messageEffect.id)] = node
}
validReactions.insert(.custom(messageEffect.id))
var centerAnimation: TelegramMediaFile?
centerAnimation = messageEffect.staticIcon
node.update(
context: arguments.context,
type: arguments.type,
value: .custom(messageEffect.id),
file: centerAnimation,
fileId: centerAnimation?.fileId.id,
alternativeText: messageEffect.emoticon,
isSelected: false,
count: 0,
theme: arguments.presentationData.theme.theme,
wallpaper: arguments.presentationData.theme.wallpaper,
animationCache: arguments.animationCache,
animationRenderer: arguments.animationRenderer,
animated: false
)
if node.supernode == nil {
strongSelf.addSubnode(node)
if animation.isAnimated {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + 3.0 + UIScreenPixel + verticalInset), size: CGSize(width: reactionSize, height: reactionSize))
if animateNode {
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
} else {
node.frame = nodeFrame
}
reactionOffset += reactionSize + reactionSpacing
}
if !arguments.reactions.isEmpty {
reactionOffset += reactionTrailingSpacing
}
var removeIds: [MessageReaction.Reaction] = []
for (id, node) in strongSelf.reactionNodes {
if !validReactions.contains(id) {
removeIds.append(id)
if animation.isAnimated {
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
node?.layer.removeAllAnimations()
node?.removeFromSupernode()
})
} else {
node.removeFromSupernode()
}
}
}
for id in removeIds {
strongSelf.reactionNodes.removeValue(forKey: id)
}
} else {
var removeIds: [MessageReaction.Reaction] = []
for (id, node) in strongSelf.reactionNodes {
removeIds.append(id)
if animation.isAnimated {
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
node?.layer.removeAllAnimations()
node?.removeFromSupernode()
})
} else {
node.removeFromSupernode()
}
}
for id in removeIds {
strongSelf.reactionNodes.removeValue(forKey: id)
}
}
if let currentRepliesIcon = currentRepliesIcon {
currentRepliesIcon.displaysAsynchronously = false
if currentRepliesIcon.image !== repliesImage {
currentRepliesIcon.image = repliesImage
}
if currentRepliesIcon.supernode == nil {
strongSelf.repliesIcon = currentRepliesIcon
strongSelf.addSubnode(currentRepliesIcon)
if animation.isAnimated {
currentRepliesIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
let repliesIconFrame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - repliesIconSize.height) / 2.0)), size: repliesIconSize)
animation.animator.updateFrame(layer: currentRepliesIcon.layer, frame: repliesIconFrame, completion: nil)
reactionOffset += 9.0
} else if let repliesIcon = strongSelf.repliesIcon {
strongSelf.repliesIcon = nil
if animation.isAnimated {
repliesIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repliesIcon] _ in
repliesIcon?.removeFromSupernode()
})
} else {
repliesIcon.removeFromSupernode()
}
}
if let (layout, apply) = replyCountLayoutAndApply {
let node = apply()
if strongSelf.replyCountNode !== node {
strongSelf.replyCountNode?.removeFromSupernode()
strongSelf.addSubnode(node)
strongSelf.replyCountNode = node
if animation.isAnimated {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
let replyCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size)
animation.animator.updateFrame(layer: node.layer, frame: replyCountFrame, completion: nil)
reactionOffset += 4.0 + layout.size.width
} else if let replyCountNode = strongSelf.replyCountNode {
strongSelf.replyCountNode = nil
if animation.isAnimated {
replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in
replyCountNode?.removeFromSupernode()
})
} else {
replyCountNode.removeFromSupernode()
}
}
}
})
})
}
}
public static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)) {
let currentLayout = node?.asyncLayout()
return { arguments in
let resultNode: ChatMessageDateAndStatusNode
let resultSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))
if let node = node, let currentLayout = currentLayout {
resultNode = node
resultSuggestedWidthAndContinue = currentLayout(arguments)
} else {
resultNode = ChatMessageDateAndStatusNode()
resultSuggestedWidthAndContinue = resultNode.asyncLayout()(arguments)
}
return (resultSuggestedWidthAndContinue.0, { boundingWidth in
let (size, apply) = resultSuggestedWidthAndContinue.1(boundingWidth)
return (size, { animation in
apply(animation)
return resultNode
})
})
}
}
public func reactionView(value: MessageReaction.Reaction) -> UIView? {
for (key, button) in self.reactionButtonsContainer.buttons {
if key == value {
return button.view.iconView
}
}
return nil
}
public func messageEffectTargetView() -> UIView? {
for (_, node) in self.reactionNodes {
return node.iconView
}
return nil
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for (_, button) in self.reactionButtonsContainer.buttons {
if button.view.frame.contains(point) {
if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) {
return result
}
}
}
if self.pressed != nil {
if self.bounds.contains(point) {
return self.view
}
}
return nil
}
}
public func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool {
return false
}