Swiftgram/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift
2021-08-01 18:12:53 +02:00

424 lines
25 KiB
Swift

import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import RadialStatusNode
import AnimatedCountLabelNode
import AnimatedAvatarSetNode
final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
private let separatorNode: ASDisplayNode
private let countNode: AnimatedCountLabelNode
private let alternativeCountNode: AnimatedCountLabelNode
private let iconNode: ASImageNode
private let arrowNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
private let avatarsContext: AnimatedAvatarSetContext
private let avatarsNode: AnimatedAvatarSetNode
private let unreadIconNode: ASImageNode
private var statusNode: RadialStatusNode?
required init() {
self.separatorNode = ASDisplayNode()
self.separatorNode.isUserInteractionEnabled = false
self.countNode = AnimatedCountLabelNode()
self.alternativeCountNode = AnimatedCountLabelNode()
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.unreadIconNode = ASImageNode()
self.unreadIconNode.displaysAsynchronously = false
self.unreadIconNode.displayWithoutProcessing = true
self.unreadIconNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.isUserInteractionEnabled = false
self.avatarsContext = AnimatedAvatarSetContext()
self.avatarsNode = AnimatedAvatarSetNode()
self.avatarsNode.isUserInteractionEnabled = false
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.buttonNode.addSubnode(self.separatorNode)
self.buttonNode.addSubnode(self.countNode)
self.buttonNode.addSubnode(self.alternativeCountNode)
self.buttonNode.addSubnode(self.iconNode)
self.buttonNode.addSubnode(self.unreadIconNode)
self.buttonNode.addSubnode(self.arrowNode)
self.buttonNode.addSubnode(self.avatarsNode)
self.addSubnode(self.buttonNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
let nodes: [ASDisplayNode] = [
strongSelf.buttonNode
]
for node in nodes {
if highlighted {
node.layer.removeAnimation(forKey: "opacity")
node.alpha = 0.4
} else {
node.alpha = 1.0
node.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
guard let item = self.item else {
return
}
if item.message.id.peerId.isReplies {
item.controllerInteraction.openReplyThreadOriginalMessage(item.message)
} else {
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
}
}
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let makeCountLayout = self.countNode.asyncLayout()
let makeAlternativeCountLayout = self.alternativeCountNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
let displaySeparator: Bool
let topOffset: CGFloat
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
displaySeparator = false
topOffset = 2.0
} else {
displaySeparator = true
topOffset = 0.0
}
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let maxTextWidth = CGFloat.greatestFiniteMagnitude
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
var dateReplies = 0
var replyPeers: [Peer] = []
var hasUnseenReplies = false
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyThreadMessageAttribute {
dateReplies = Int(attribute.count)
replyPeers = attribute.latestUsers.compactMap { peerId -> Peer? in
return item.message.peers[peerId]
}
if let maxMessageId = attribute.maxMessageId, let maxReadMessageId = attribute.maxReadMessageId {
hasUnseenReplies = maxMessageId > maxReadMessageId
}
}
}
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let textFont = Font.regular(17.0)
let rawSegments: [AnimatedCountLabelNode.Segment]
let rawAlternativeSegments: [AnimatedCountLabelNode.Segment]
var accessibilityLabel = ""
if item.message.id.peerId.isReplies {
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_ViewReply, font: textFont, textColor: messageTheme.accentTextColor))]
rawAlternativeSegments = rawSegments
accessibilityLabel = item.presentationData.strings.Conversation_ViewReply
} else if dateReplies > 0 {
var commentsPart = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
if commentsPart.contains("[") && commentsPart.contains("]") {
if let startIndex = commentsPart.firstIndex(of: "["), let endIndex = commentsPart.firstIndex(of: "]") {
commentsPart.removeSubrange(startIndex ... endIndex)
}
} else {
commentsPart = commentsPart.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,. "))
}
var segments: [AnimatedCountLabelNode.Segment] = []
let textAndRanges = item.presentationData.strings.Conversation_MessageViewCommentsFormat("\(dateReplies)", commentsPart)
let rawText = textAndRanges.string
var textIndex = 0
var latestIndex = 0
for indexAndRange in textAndRanges.ranges {
var lowerSegmentIndex = indexAndRange.range.lowerBound
if indexAndRange.index != 0 {
lowerSegmentIndex = min(lowerSegmentIndex, latestIndex)
} else {
if latestIndex < indexAndRange.range.lowerBound {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex) ..< rawText.index(rawText.startIndex, offsetBy: indexAndRange.range.lowerBound)])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
}
latestIndex = indexAndRange.range.upperBound
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: lowerSegmentIndex) ..< rawText.index(rawText.startIndex, offsetBy: min(rawText.count, indexAndRange.range.upperBound))])
if indexAndRange.index == 0 {
segments.append(.number(dateReplies, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
} else {
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
}
if latestIndex < rawText.count {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex)...])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
rawSegments = segments
rawAlternativeSegments = rawSegments
accessibilityLabel = rawText
} else {
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveComment, font: textFont, textColor: messageTheme.accentTextColor))]
rawAlternativeSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveCommentShort, font: textFont, textColor: messageTheme.accentTextColor))]
accessibilityLabel = item.presentationData.strings.Conversation_MessageLeaveComment
}
let imageSize: CGFloat = 30.0
let imageSpacing: CGFloat = 20.0
var textLeftInset: CGFloat = 0.0
if replyPeers.isEmpty {
textLeftInset = 41.0
} else {
textLeftInset = 15.0 + imageSize * min(1.0, CGFloat(replyPeers.count)) + (imageSpacing) * max(0.0, min(2.0, CGFloat(replyPeers.count - 1)))
}
let textRightInset: CGFloat = 36.0
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset - textLeftInset - textRightInset), height: constrainedSize.height)
let textInsets = UIEdgeInsets()//(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
let (countLayout, countApply) = makeCountLayout(textConstrainedSize, rawSegments)
let (alternativeCountLayout, alternativeCountApply) = makeAlternativeCountLayout(textConstrainedSize, rawAlternativeSegments)
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left + textLeftInset - 2.0, y: -textInsets.top + 5.0 + topOffset), size: countLayout.size)
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top - 5.0 + UIScreenPixel)
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var suggestedBoundingWidth: CGFloat
suggestedBoundingWidth = textFrameWithoutInsets.width
suggestedBoundingWidth += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + textLeftInset + textRightInset
let iconImage: UIImage?
let iconOffset: CGPoint
if item.message.id.peerId.isReplies {
iconImage = PresentationResourcesChat.chatMessageRepliesIcon(item.presentationData.theme.theme, incoming: incoming)
iconOffset = CGPoint(x: -4.0, y: -4.0)
} else {
iconImage = PresentationResourcesChat.chatMessageCommentsIcon(item.presentationData.theme.theme, incoming: incoming)
iconOffset = CGPoint(x: 0.0, y: -1.0)
}
let arrowImage = PresentationResourcesChat.chatMessageCommentsArrowIcon(item.presentationData.theme.theme, incoming: incoming)
let unreadIconImage = PresentationResourcesChat.chatMessageCommentsUnreadDotIcon(item.presentationData.theme.theme, incoming: incoming)
return (suggestedBoundingWidth, { boundingWidth in
var boundingSize: CGSize
boundingSize = textFrameWithoutInsets.size
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height = 40.0 + topOffset
return (boundingSize, { [weak self] animation, synchronousLoad in
if let strongSelf = self {
strongSelf.item = item
let transition: ContainedViewLayoutTransition
if animation.isAnimated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.countNode.isHidden = countLayout.isTruncated
strongSelf.alternativeCountNode.isHidden = !strongSelf.countNode.isHidden
strongSelf.buttonNode.accessibilityLabel = accessibilityLabel
let _ = countApply(animation.isAnimated)
let _ = alternativeCountApply(animation.isAnimated)
let adjustedTextFrame = textFrame
if strongSelf.countNode.frame.isEmpty {
strongSelf.countNode.frame = adjustedTextFrame
} else {
transition.updateFrameAdditive(node: strongSelf.countNode, frame: adjustedTextFrame)
}
if strongSelf.alternativeCountNode.frame.isEmpty {
strongSelf.alternativeCountNode.frame = CGRect(origin: adjustedTextFrame.origin, size: alternativeCountLayout.size)
} else {
transition.updateFrameAdditive(node: strongSelf.alternativeCountNode, frame: CGRect(origin: adjustedTextFrame.origin, size: alternativeCountLayout.size))
}
let effectiveTextFrame: CGRect
if !strongSelf.alternativeCountNode.isHidden {
effectiveTextFrame = strongSelf.alternativeCountNode.frame
} else {
effectiveTextFrame = strongSelf.countNode.frame
}
if let iconImage = iconImage {
strongSelf.iconNode.image = iconImage
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: 15.0 + iconOffset.x, y: 6.0 + iconOffset.y + topOffset), size: iconImage.size)
}
if let arrowImage = arrowImage {
strongSelf.arrowNode.image = arrowImage
let arrowFrame = CGRect(origin: CGPoint(x: boundingWidth - 33.0, y: 6.0 + topOffset), size: arrowImage.size)
if strongSelf.arrowNode.frame.isEmpty {
strongSelf.arrowNode.frame = arrowFrame
} else {
transition.updateFrameAdditive(node: strongSelf.arrowNode, frame: arrowFrame)
}
if let unreadIconImage = unreadIconImage {
strongSelf.unreadIconNode.image = unreadIconImage
let unreadIconFrame = CGRect(origin: CGPoint(x: effectiveTextFrame.maxX + 4.0, y: effectiveTextFrame.minY + floor((effectiveTextFrame.height - unreadIconImage.size.height) / 2.0) + 1.0), size: unreadIconImage.size)
if strongSelf.unreadIconNode.frame.isEmpty {
strongSelf.unreadIconNode.frame = unreadIconFrame
} else {
transition.updateFrameAdditive(node: strongSelf.unreadIconNode, frame: unreadIconFrame)
}
}
}
if strongSelf.unreadIconNode.alpha.isZero != !hasUnseenReplies {
transition.updateAlpha(node: strongSelf.unreadIconNode, alpha: hasUnseenReplies ? 1.0 : 0.0)
if hasUnseenReplies {
strongSelf.unreadIconNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0)
}
}
let hasActivity = item.controllerInteraction.currentMessageWithLoadingReplyThread == item.message.id
if hasActivity {
strongSelf.arrowNode.isHidden = true
let statusNode: RadialStatusNode
if let current = strongSelf.statusNode {
statusNode = current
} else {
statusNode = RadialStatusNode(backgroundNodeColor: .clear)
strongSelf.statusNode = statusNode
strongSelf.buttonNode.addSubnode(statusNode)
}
let statusSize = CGSize(width: 20.0, height: 20.0)
let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - 11.0, y: 8.0 + topOffset), size: statusSize)
if statusNode.frame.isEmpty {
statusNode.frame = statusFrame
} else {
transition.updateFrameAdditive(node: statusNode, frame: statusFrame)
}
statusNode.transitionToState(.progress(color: messageTheme.accentTextColor, lineWidth: 1.5, value: nil, cancelEnabled: false, animateRotation: true), animated: false, synchronous: false, completion: {})
} else {
strongSelf.arrowNode.isHidden = false
if let statusNode = strongSelf.statusNode {
strongSelf.statusNode = nil
statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.3, removeOnCompletion: false, completion: { [weak statusNode] _ in
statusNode?.removeFromSupernode()
})
strongSelf.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.3)
}
}
let avatarContent = strongSelf.avatarsContext.update(peers: replyPeers.map(EnginePeer.init), animated: animation.isAnimated)
let avatarsSize = strongSelf.avatarsNode.update(context: item.context, content: avatarContent, animated: animation.isAnimated, synchronousLoad: synchronousLoad)
let iconAlpha: CGFloat = avatarsSize.width.isZero ? 1.0 : 0.0
if iconAlpha.isZero != strongSelf.iconNode.alpha.isZero {
transition.updateAlpha(node: strongSelf.iconNode, alpha: iconAlpha)
if animation.isAnimated {
if iconAlpha.isZero {
} else {
strongSelf.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
}
let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: 3.0 + topOffset), size: avatarsSize)
strongSelf.avatarsNode.frame = avatarsFrame
//strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
//strongSelf.avatarsNode.update(context: item.context, peers: replyPeers, synchronousLoad: synchronousLoad, imageSize: imageSize, imageSpacing: imageSpacing, borderWidth: 2.0 - UIScreenPixel)
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
strongSelf.separatorNode.isHidden = !displaySeparator
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.strokeInsets.left, y: -3.0), size: CGSize(width: boundingWidth - layoutConstants.bubble.strokeInsets.left - layoutConstants.bubble.strokeInsets.right, height: UIScreenPixel))
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: boundingSize.height))
strongSelf.buttonNode.isUserInteractionEnabled = item.message.id.namespace == Namespaces.Message.Cloud
strongSelf.buttonNode.alpha = item.message.id.namespace == Namespaces.Message.Cloud ? 1.0 : 0.5
}
})
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .ignore
}
return .none
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.buttonNode.isUserInteractionEnabled && self.buttonNode.frame.contains(point) {
return self.buttonNode.view
}
return nil
}
}