import Foundation import UIKit import AsyncDisplayKit import Postbox import TelegramCore import SyncCore import Display import SwiftSignalKit import TelegramPresentationData import AccountContext import AppBundle private let reactionCountFont = Font.semibold(11.0) 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") } enum ChatMessageDateAndStatusOutgoingType: Equatable { case Sent(read: Bool) case Sending case Failed } enum ChatMessageDateAndStatusType: Equatable { case BubbleIncoming case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType) case ImageIncoming case ImageOutgoing(ChatMessageDateAndStatusOutgoingType) case FreeIncoming case FreeOutgoing(ChatMessageDateAndStatusOutgoingType) } private let reactionFont = Font.regular(12.0) private final class StatusReactionNode: ASDisplayNode { let emptyImageNode: ASImageNode let selectedImageNode: ASImageNode private var theme: PresentationTheme? private var isSelected: Bool? override init() { self.emptyImageNode = ASImageNode() self.emptyImageNode.displaysAsynchronously = false self.selectedImageNode = ASImageNode() self.selectedImageNode.displaysAsynchronously = false super.init() self.addSubnode(self.emptyImageNode) self.addSubnode(self.selectedImageNode) } func update(type: ChatMessageDateAndStatusType, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) { if self.theme !== theme { self.theme = theme let emptyImage: UIImage? let selectedImage: UIImage? switch type { case .BubbleIncoming: emptyImage = PresentationResourcesChat.chatMessageLike(theme, incoming: true, isSelected: false) selectedImage = PresentationResourcesChat.chatMessageLike(theme, incoming: true, isSelected: true) case .BubbleOutgoing: emptyImage = PresentationResourcesChat.chatMessageLike(theme, incoming: false, isSelected: false) selectedImage = PresentationResourcesChat.chatMessageLike(theme, incoming: false, isSelected: true) case .ImageIncoming, .ImageOutgoing: emptyImage = PresentationResourcesChat.chatMessageMediaLike(theme, isSelected: false) selectedImage = PresentationResourcesChat.chatMessageMediaLike(theme, isSelected: true) case .FreeIncoming, .FreeOutgoing: emptyImage = PresentationResourcesChat.chatMessageFreeLike(theme, wallpaper: wallpaper, isSelected: false) selectedImage = PresentationResourcesChat.chatMessageFreeLike(theme, wallpaper: wallpaper, isSelected: true) } if let emptyImage = emptyImage, let selectedImage = selectedImage { self.emptyImageNode.image = emptyImage self.emptyImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: emptyImage.size) self.selectedImageNode.image = selectedImage self.selectedImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: selectedImage.size) } } if self.isSelected != isSelected { let wasSelected = self.isSelected self.isSelected = isSelected self.emptyImageNode.isHidden = isSelected && count <= 1 self.selectedImageNode.isHidden = !isSelected if let wasSelected = wasSelected, wasSelected, !isSelected { if let image = self.selectedImageNode.image { let leftImage = generateImage(image.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) image.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() context.clear(CGRect(origin: CGPoint(x: size.width / 2.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height))) }) let rightImage = generateImage(image.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) image.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height))) }) if let leftImage = leftImage, let rightImage = rightImage { let leftView = UIImageView() leftView.image = leftImage leftView.frame = self.selectedImageNode.frame let rightView = UIImageView() rightView.image = rightImage rightView.frame = self.selectedImageNode.frame self.view.addSubview(leftView) self.view.addSubview(rightView) let duration: Double = 0.3 leftView.layer.animateRotation(from: 0.0, to: -CGFloat.pi * 0.7, duration: duration, removeOnCompletion: false) rightView.layer.animateRotation(from: 0.0, to: CGFloat.pi * 0.7, duration: duration, removeOnCompletion: false) leftView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -6.0, y: 8.0), duration: duration, removeOnCompletion: false, additive: true) rightView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 6.0, y: 8.0), duration: duration, removeOnCompletion: false, additive: true) leftView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak leftView] _ in leftView?.removeFromSuperview() }) rightView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak rightView] _ in rightView?.removeFromSuperview() }) } } } } } } class ChatMessageDateAndStatusNode: ASDisplayNode { private var backgroundNode: ASImageNode? 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: [StatusReactionNode] = [] private var reactionCountNode: TextNode? private var reactionButtonNode: HighlightTrackingButtonNode? private var repliesIcon: ASImageNode? private var replyCountNode: TextNode? private var type: ChatMessageDateAndStatusType? private var theme: ChatPresentationThemeData? private var layoutSize: CGSize? private var tapGestureRecognizer: UITapGestureRecognizer? var openReactions: (() -> Void)? var openReplies: (() -> Void)? 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) } } } override init() { self.dateNode = TextNode() self.dateNode.isUserInteractionEnabled = false self.dateNode.displaysAsynchronously = false super.init() self.addSubnode(self.dateNode) } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.pressed?() } } func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool) -> (CGSize, (Bool) -> 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 makeReactionCountLayout = TextNode.asyncLayout(self.reactionCountNode) let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode) let previousLayoutSize = self.layoutSize return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount, isPinned in let dateColor: UIColor var backgroundImage: UIImage? 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 = presentationData.theme != currentTheme || type != currentType let graphics = PresentationResourcesChat.principalGraphics(mediaBox: context.account.postbox.mediaBox, knockoutWallpaper: context.sharedContext.immediateExperimentalUISettings.knockoutWallpaper, theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) let isDefaultWallpaper = serviceMessageColorHasDefaultWallpaper(presentationData.theme.wallpaper) let offset: CGFloat = -UIScreenPixel let checkSize: CGFloat = floor(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) switch type { case .BubbleIncoming: dateColor = presentationData.theme.theme.chat.message.incoming.secondaryTextColor leftInset = 10.0 loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(presentationData.theme.theme, size: checkSize) loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockBubbleIncomingFrameImage clockMinImage = graphics.clockBubbleIncomingMinImage if impressionCount != nil { impressionImage = graphics.incomingDateAndStatusImpressionIcon } if replyCount != 0 { repliesImage = graphics.incomingDateAndStatusRepliesIcon } else if isPinned { repliesImage = graphics.incomingDateAndStatusPinnedIcon } case let .BubbleOutgoing(status): dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor outgoingStatus = status leftInset = 10.0 loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(presentationData.theme.theme, size: checkSize) loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockBubbleOutgoingFrameImage clockMinImage = graphics.clockBubbleOutgoingMinImage if impressionCount != nil { impressionImage = graphics.outgoingDateAndStatusImpressionIcon } if replyCount != 0 { repliesImage = graphics.outgoingDateAndStatusRepliesIcon } else if isPinned { repliesImage = graphics.outgoingDateAndStatusPinnedIcon } case .ImageIncoming: dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor backgroundImage = graphics.dateAndStatusMediaBackground leftInset = 0.0 loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(presentationData.theme.theme, size: checkSize) loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockMediaFrameImage clockMinImage = graphics.clockMediaMinImage if impressionCount != nil { impressionImage = graphics.mediaImpressionIcon } if replyCount != 0 { repliesImage = graphics.mediaRepliesIcon } else if isPinned { repliesImage = graphics.mediaPinnedIcon } case let .ImageOutgoing(status): dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor outgoingStatus = status backgroundImage = graphics.dateAndStatusMediaBackground leftInset = 0.0 loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(presentationData.theme.theme, size: checkSize) loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockMediaFrameImage clockMinImage = graphics.clockMediaMinImage if impressionCount != nil { impressionImage = graphics.mediaImpressionIcon } if replyCount != 0 { repliesImage = graphics.mediaRepliesIcon } else if isPinned { repliesImage = graphics.mediaPinnedIcon } case .FreeIncoming: let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) dateColor = serviceColor.primaryText backgroundImage = graphics.dateAndStatusFreeBackground leftInset = 0.0 loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) clockFrameImage = graphics.clockFreeFrameImage clockMinImage = graphics.clockFreeMinImage if impressionCount != nil { impressionImage = graphics.freeImpressionIcon } if replyCount != 0 { repliesImage = graphics.freeRepliesIcon } else if isPinned { repliesImage = graphics.freePinnedIcon } case let .FreeOutgoing(status): let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) dateColor = serviceColor.primaryText outgoingStatus = status backgroundImage = graphics.dateAndStatusFreeBackground leftInset = 0.0 loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) clockFrameImage = graphics.clockFreeFrameImage clockMinImage = graphics.clockFreeMinImage if impressionCount != nil { impressionImage = graphics.freeImpressionIcon } if replyCount != 0 { repliesImage = graphics.freeRepliesIcon } else if isPinned { repliesImage = graphics.freePinnedIcon } } var updatedDateText = dateText if edited { updatedDateText = "\(presentationData.strings.Conversation_MessageEditedLabel) \(updatedDateText)" } if let impressionCount = impressionCount { updatedDateText = compactNumericCountString(impressionCount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + " " + updatedDateText } let dateFont = Font.regular(floor(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: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let checkOffset = floor(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 = 13.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 type { case .BubbleOutgoing, .FreeOutgoing, .ImageOutgoing: hideStatus = false default: hideStatus = impressionCount != nil } if hideStatus { statusWidth = 0.0 checkReadNode = nil checkSentNode = nil clockFrameNode = nil clockMinNode = nil } else { statusWidth = floor(floor(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) } let reactionSize: CGFloat = 14.0 var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? let reactionSpacing: CGFloat = -4.0 let reactionTrailingSpacing: CGFloat = 4.0 var reactionInset: CGFloat = 0.0 if !reactions.isEmpty { reactionInset = -1.0 + CGFloat(reactions.count) * reactionSize + CGFloat(reactions.count - 1) * reactionSpacing + reactionTrailingSpacing var count = 0 for reaction in reactions { count += Int(reaction.count) } let countString: String if count > 1000000 { countString = "\(count / 1000000)M" } else if count > 1000 { countString = "\(count / 1000)K" } else { countString = "\(count)" } let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) reactionInset += max(10.0, layoutAndApply.0.size.width) + 2.0 reactionCountLayoutAndApply = layoutAndApply } if replyCount > 0 { let countString: String if replyCount > 1000000 { countString = "\(replyCount / 1000000)M" } else if replyCount > 1000 { countString = "\(replyCount / 1000)K" } else { countString = "\(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 isPinned { reactionInset += 12.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) return (layoutSize, { [weak self] animated in if let strongSelf = self { strongSelf.theme = presentationData.theme strongSelf.type = type strongSelf.layoutSize = layoutSize 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 { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate if let previousLayoutSize = previousLayoutSize { backgroundNode.frame = backgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) } transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize)) } } else { if let backgroundNode = strongSelf.backgroundNode { backgroundNode.removeFromSupernode() strongSelf.backgroundNode = nil } } strongSelf.dateNode.displaysAsynchronously = !presentationData.isPreview let _ = dateApply() if let currentImpressionIcon = currentImpressionIcon { currentImpressionIcon.displaysAsynchronously = !presentationData.isPreview if currentImpressionIcon.image !== impressionImage { currentImpressionIcon.image = impressionImage } if currentImpressionIcon.supernode == nil { strongSelf.impressionIcon = currentImpressionIcon strongSelf.addSubnode(currentImpressionIcon) } currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize) } else if let impressionIcon = strongSelf.impressionIcon { impressionIcon.removeFromSupernode() strongSelf.impressionIcon = nil } strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset), size: date.size) if let clockFrameNode = clockFrameNode { if strongSelf.clockFrameNode == nil { strongSelf.clockFrameNode = clockFrameNode clockFrameNode.image = clockFrameImage strongSelf.addSubnode(clockFrameNode) } else if themeUpdated { clockFrameNode.image = clockFrameImage } clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y) 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 { if strongSelf.clockMinNode == nil { strongSelf.clockMinNode = clockMinNode clockMinNode.image = clockMinImage strongSelf.addSubnode(clockMinNode) } else if themeUpdated { clockMinNode.image = clockMinImage } clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y) 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 = animated } else if themeUpdated { checkSentNode.image = loadedCheckFullImage } if let checkSentFrame = checkSentFrame { if checkSentNode.isHidden { animateSentNode = animated } checkSentNode.isHidden = false checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top) } else { checkSentNode.isHidden = true } var animateReadNode = false if strongSelf.checkReadNode == nil { animateReadNode = animated checkReadNode.image = loadedCheckPartialImage strongSelf.checkReadNode = checkReadNode strongSelf.addSubnode(checkReadNode) } else if themeUpdated { checkReadNode.image = loadedCheckPartialImage } if let checkReadFrame = checkReadFrame { if checkReadNode.isHidden { animateReadNode = animated } checkReadNode.isHidden = false checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top) } 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 = leftInset - reactionInset + backgroundInsets.left for i in 0 ..< reactions.count { let node: StatusReactionNode if strongSelf.reactionNodes.count > i { node = strongSelf.reactionNodes[i] } else { node = StatusReactionNode() if strongSelf.reactionNodes.count > i { let previousNode = strongSelf.reactionNodes[i] if animated { previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in previousNode?.removeFromSupernode() }) } else { previousNode.removeFromSupernode() } strongSelf.reactionNodes[i] = node } else { strongSelf.reactionNodes.append(node) } } node.update(type: type, isSelected: reactions[i].isSelected, count: Int(reactions[i].count), theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, animated: false) if node.supernode == nil { strongSelf.addSubnode(node) if animated { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + 1.0), size: CGSize(width: reactionSize, height: reactionSize)) reactionOffset += reactionSize + reactionSpacing } if !reactions.isEmpty { reactionOffset += reactionTrailingSpacing } for _ in reactions.count ..< strongSelf.reactionNodes.count { let node = strongSelf.reactionNodes.removeLast() if animated { if let previousLayoutSize = previousLayoutSize { node.frame = node.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) } 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?.removeFromSupernode() }) } else { node.removeFromSupernode() } } if let (layout, apply) = reactionCountLayoutAndApply { let node = apply() if strongSelf.reactionCountNode !== node { strongSelf.reactionCountNode?.removeFromSupernode() strongSelf.addSubnode(node) strongSelf.reactionCountNode = node if animated { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } node.frame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset), size: layout.size) reactionOffset += 1.0 + layout.size.width } else if let reactionCountNode = strongSelf.reactionCountNode { strongSelf.reactionCountNode = nil if animated { if let previousLayoutSize = previousLayoutSize { reactionCountNode.frame = reactionCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) } reactionCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountNode] _ in reactionCountNode?.removeFromSupernode() }) } else { reactionCountNode.removeFromSupernode() } } if let currentRepliesIcon = currentRepliesIcon { currentRepliesIcon.displaysAsynchronously = !presentationData.isPreview if currentRepliesIcon.image !== repliesImage { currentRepliesIcon.image = repliesImage } if currentRepliesIcon.supernode == nil { strongSelf.repliesIcon = currentRepliesIcon strongSelf.addSubnode(currentRepliesIcon) if animated { currentRepliesIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } currentRepliesIcon.frame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + floor((date.size.height - repliesIconSize.height) / 2.0)), size: repliesIconSize) reactionOffset += 9.0 } else if let repliesIcon = strongSelf.repliesIcon { strongSelf.repliesIcon = nil if animated { if let previousLayoutSize = previousLayoutSize { repliesIcon.frame = repliesIcon.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) } 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 animated { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } node.frame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset), size: layout.size) reactionOffset += 4.0 + layout.size.width } else if let replyCountNode = strongSelf.replyCountNode { strongSelf.replyCountNode = nil if animated { if let previousLayoutSize = previousLayoutSize { replyCountNode.frame = replyCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) } replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in replyCountNode?.removeFromSupernode() }) } else { replyCountNode.removeFromSupernode() } } /*if !strongSelf.reactionNodes.isEmpty { if strongSelf.reactionButtonNode == nil { let reactionButtonNode = HighlightTrackingButtonNode() strongSelf.reactionButtonNode = reactionButtonNode strongSelf.addSubnode(reactionButtonNode) reactionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.reactionButtonPressed), forControlEvents: .touchUpInside) reactionButtonNode.highligthedChanged = { [weak strongSelf] highlighted in guard let strongSelf = strongSelf else { return } if highlighted { for itemNode in strongSelf.reactionNodes { itemNode.alpha = 0.4 } } else { for itemNode in strongSelf.reactionNodes { itemNode.alpha = 1.0 itemNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.3) } } } } strongSelf.reactionButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset - reactionInset + backgroundInsets.left - 5.0, y: backgroundInsets.top + 1.0 + offset - 5.0), size: CGSize(width: reactionOffset + 5.0 * 2.0, height: 20.0)) } else if let reactionButtonNode = strongSelf.reactionButtonNode { strongSelf.reactionButtonNode = nil reactionButtonNode.removeFromSupernode() }*/ } }) } } static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { let currentLayout = node?.asyncLayout() return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned in let resultNode: ChatMessageDateAndStatusNode let resultSizeAndApply: (CGSize, (Bool) -> Void) if let node = node, let currentLayout = currentLayout { resultNode = node resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned) } else { resultNode = ChatMessageDateAndStatusNode() resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned) } return (resultSizeAndApply.0, { animated in resultSizeAndApply.1(animated) return resultNode }) } } func reactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { for node in self.reactionNodes { return (node.emptyImageNode, node.selectedImageNode) } return nil } @objc private func reactionButtonPressed() { self.openReactions?() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let reactionButtonNode = self.reactionButtonNode { if reactionButtonNode.frame.contains(point) { return reactionButtonNode.view } } if self.pressed != nil { if self.bounds.contains(point) { return self.view } } return nil } }