mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
661 lines
33 KiB
Swift
661 lines
33 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
enum ChatMessageBackgroundMergeType {
|
|
case None, Top, Bottom, Both
|
|
|
|
init(top: Bool, bottom: Bool) {
|
|
if top && bottom {
|
|
self = .Both
|
|
} else if top {
|
|
self = .Top
|
|
} else if bottom {
|
|
self = .Bottom
|
|
} else {
|
|
self = .None
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum ChatMessageBackgroundType: Equatable {
|
|
case Incoming(ChatMessageBackgroundMergeType), Outgoing(ChatMessageBackgroundMergeType)
|
|
}
|
|
|
|
private func ==(lhs: ChatMessageBackgroundType, rhs: ChatMessageBackgroundType) -> Bool {
|
|
switch lhs {
|
|
case let .Incoming(lhsMergeType):
|
|
switch rhs {
|
|
case let .Incoming(rhsMergeType):
|
|
return lhsMergeType == rhsMergeType
|
|
case .Outgoing:
|
|
return false
|
|
}
|
|
case let .Outgoing(lhsMergeType):
|
|
switch rhs {
|
|
case .Incoming:
|
|
return false
|
|
case let .Outgoing(rhsMergeType):
|
|
return lhsMergeType == rhsMergeType
|
|
}
|
|
}
|
|
}
|
|
|
|
private let chatMessageBackgroundIncomingImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncoming")?.precomposed()
|
|
private let chatMessageBackgroundOutgoingImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoing")?.precomposed()
|
|
private let chatMessageBackgroundIncomingMergedTopImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedTop")?.precomposed()
|
|
private let chatMessageBackgroundIncomingMergedBottomImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedBottom")?.precomposed()
|
|
private let chatMessageBackgroundIncomingMergedBothImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedBoth")?.precomposed()
|
|
private let chatMessageBackgroundOutgoingMergedImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
|
|
private let chatMessageBackgroundOutgoingMergedTopImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
|
|
private let chatMessageBackgroundOutgoingMergedBottomImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
|
|
private let chatMessageBackgroundOutgoingMergedBothImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed()
|
|
|
|
class ChatMessageBackground: ASImageNode {
|
|
private var type: ChatMessageBackgroundType?
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.isLayerBacked = true
|
|
self.displaysAsynchronously = false
|
|
self.displayWithoutProcessing = true
|
|
}
|
|
|
|
fileprivate func setType(type: ChatMessageBackgroundType) {
|
|
if let currentType = self.type, currentType == type {
|
|
return
|
|
}
|
|
self.type = type
|
|
|
|
let image: UIImage?
|
|
switch type {
|
|
case let .Incoming(mergeType):
|
|
switch mergeType {
|
|
case .None:
|
|
image = chatMessageBackgroundIncomingImage
|
|
case .Top:
|
|
image = chatMessageBackgroundIncomingMergedBottomImage
|
|
case .Bottom:
|
|
image = chatMessageBackgroundIncomingMergedTopImage
|
|
case .Both:
|
|
image = chatMessageBackgroundIncomingMergedBothImage
|
|
}
|
|
case let .Outgoing(mergeType):
|
|
switch mergeType {
|
|
case .None:
|
|
image = chatMessageBackgroundOutgoingImage
|
|
case .Top:
|
|
image = chatMessageBackgroundOutgoingMergedTopImage
|
|
case .Bottom:
|
|
image = chatMessageBackgroundOutgoingMergedBottomImage
|
|
case .Both:
|
|
image = chatMessageBackgroundOutgoingMergedBothImage
|
|
}
|
|
}
|
|
self.image = image
|
|
}
|
|
}
|
|
|
|
private func contentNodeClassesForItem(_ item: ChatMessageItem) -> [AnyClass] {
|
|
var result: [AnyClass] = []
|
|
for media in item.message.media {
|
|
if let _ = media as? TelegramMediaImage {
|
|
result.append(ChatMessageMediaBubbleContentNode.self)
|
|
} else if let file = media as? TelegramMediaFile {
|
|
if file.isVideo {
|
|
result.append(ChatMessageMediaBubbleContentNode.self)
|
|
} else {
|
|
result.append(ChatMessageFileBubbleContentNode.self)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !item.message.text.isEmpty {
|
|
result.append(ChatMessageTextBubbleContentNode.self)
|
|
}
|
|
|
|
for media in item.message.media {
|
|
if let webpage = media as? TelegramMediaWebpage {
|
|
if case .Loaded = webpage.content {
|
|
result.append(ChatMessageWebpageBubbleContentNode.self)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private let nameFont: UIFont = {
|
|
if #available(iOS 8.2, *) {
|
|
return UIFont.systemFont(ofSize: 14.0, weight: UIFontWeightMedium)
|
|
} else {
|
|
return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, 14.0, nil)
|
|
}
|
|
}()
|
|
|
|
private let inlineBotPrefixFont = Font.regular(14.0)
|
|
private let inlineBotNameFont = nameFont
|
|
|
|
private let chatMessagePeerIdColors: [UIColor] = [
|
|
UIColor(0xfc5c51),
|
|
UIColor(0xfa790f),
|
|
UIColor(0x0fb297),
|
|
UIColor(0x3ca5ec),
|
|
UIColor(0x3d72ed),
|
|
UIColor(0x895dd5)
|
|
]
|
|
|
|
class ChatMessageBubbleItemNode: ChatMessageItemView {
|
|
private let backgroundNode: ChatMessageBackground
|
|
private var transitionClippingNode: ASDisplayNode?
|
|
|
|
private var nameNode: TextNode?
|
|
private var forwardInfoNode: ChatMessageForwardInfoNode?
|
|
private var replyInfoNode: ChatMessageReplyInfoNode?
|
|
|
|
private var contentNodes: [ChatMessageBubbleContentNode] = []
|
|
|
|
private var messageId: MessageId?
|
|
|
|
private var backgroundFrameTransition: (CGRect, CGRect)?
|
|
|
|
required init() {
|
|
self.backgroundNode = ChatMessageBackground()
|
|
|
|
super.init(layerBacked: false)
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
|
super.animateInsertion(currentTimestamp, duration: duration)
|
|
|
|
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
for contentNode in self.contentNodes {
|
|
contentNode.animateInsertion(currentTimestamp, duration: duration)
|
|
}
|
|
}
|
|
|
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
super.animateAdded(currentTimestamp, duration: duration)
|
|
|
|
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.nameNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.forwardInfoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.replyInfoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
for contentNode in self.contentNodes {
|
|
contentNode.animateAdded(currentTimestamp, duration: duration)
|
|
}
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
|
var currentContentClassesPropertiesAndLayouts: [(AnyClass, ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))))] = []
|
|
for contentNode in self.contentNodes {
|
|
currentContentClassesPropertiesAndLayouts.append((type(of: contentNode) as AnyClass, contentNode.properties, contentNode.asyncLayoutContent()))
|
|
}
|
|
|
|
let authorNameLayout = TextNode.asyncLayout(self.nameNode)
|
|
let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
|
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
|
|
|
|
let layoutConstants = self.layoutConstants
|
|
|
|
return { item, width, mergedTop, mergedBottom in
|
|
let message = item.message
|
|
|
|
let incoming = item.account.peerId != message.author?.id
|
|
let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroup && item.message.author != nil
|
|
|
|
let avatarInset: CGFloat = (item.peerId.isGroup && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0
|
|
|
|
let tmpWidth = width * layoutConstants.bubble.maximumWidthFillFactor
|
|
let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
|
|
|
|
var contentPropertiesAndPrepareLayouts: [(ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))))] = []
|
|
var addedContentNodes: [ChatMessageBubbleContentNode]?
|
|
|
|
let contentNodeClasses = contentNodeClassesForItem(item)
|
|
for contentNodeClass in contentNodeClasses {
|
|
var found = false
|
|
for (currentClass, currentProperties, currentLayout) in currentContentClassesPropertiesAndLayouts {
|
|
if currentClass == contentNodeClass {
|
|
contentPropertiesAndPrepareLayouts.append((currentProperties, currentLayout))
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
let contentNode = (contentNodeClass as! ChatMessageBubbleContentNode.Type).init()
|
|
contentPropertiesAndPrepareLayouts.append((contentNode.properties, contentNode.asyncLayoutContent()))
|
|
if addedContentNodes == nil {
|
|
addedContentNodes = [contentNode]
|
|
} else {
|
|
addedContentNodes!.append(contentNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
var authorNameString: String?
|
|
var inlineBotNameString: String?
|
|
var replyMessage: Message?
|
|
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? InlineBotMessageAttribute, let bot = message.peers[attribute.peerId] as? TelegramUser {
|
|
inlineBotNameString = bot.username
|
|
} else if let attribute = attribute as? ReplyMessageAttribute {
|
|
replyMessage = message.associatedMessages[attribute.messageId]
|
|
}
|
|
}
|
|
|
|
var displayHeader = true
|
|
if inlineBotNameString == nil && message.forwardInfo == nil && replyMessage == nil {
|
|
if let first = contentPropertiesAndPrepareLayouts.first, first.0.hidesSimpleAuthorHeader {
|
|
displayHeader = false
|
|
}
|
|
}
|
|
|
|
var contentPropertiesAndLayouts: [(ChatMessageBubbleContentProperties, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void)))] = []
|
|
|
|
let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
|
|
let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
|
|
|
|
let firstNodeTopPosition: ChatMessageBubbleRelativePosition
|
|
if displayHeader {
|
|
firstNodeTopPosition = .Neighbour
|
|
} else {
|
|
firstNodeTopPosition = .None(topNodeMergeStatus)
|
|
}
|
|
let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus)
|
|
|
|
var maximumNodeWidth = maximumContentWidth
|
|
let contentNodeCount = contentPropertiesAndPrepareLayouts.count
|
|
var index = 0
|
|
for (properties, prepareLayout) in contentPropertiesAndPrepareLayouts {
|
|
let topPosition: ChatMessageBubbleRelativePosition
|
|
let bottomPosition: ChatMessageBubbleRelativePosition
|
|
|
|
if index == 0 {
|
|
topPosition = firstNodeTopPosition
|
|
} else {
|
|
topPosition = .Neighbour
|
|
}
|
|
|
|
if index == contentNodeCount - 1 {
|
|
bottomPosition = lastNodeTopPosition
|
|
} else {
|
|
bottomPosition = .Neighbour
|
|
}
|
|
|
|
let (maxNodeWidth, nodeLayout) = prepareLayout(item, layoutConstants, ChatMessageBubbleContentPosition(top: topPosition, bottom: bottomPosition), CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth)
|
|
|
|
contentPropertiesAndLayouts.append((properties, nodeLayout))
|
|
index += 1
|
|
}
|
|
|
|
var headerSize = CGSize()
|
|
|
|
var nameNodeOriginY: CGFloat = 0.0
|
|
var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil })
|
|
var authorNameColor: UIColor?
|
|
|
|
var replyInfoOriginY: CGFloat = 0.0
|
|
var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil })
|
|
|
|
var forwardInfoOriginY: CGFloat = 0.0
|
|
var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode?) = (CGSize(), { nil })
|
|
|
|
if displayHeader {
|
|
if let author = message.author, displayAuthorInfo {
|
|
authorNameString = author.displayTitle
|
|
authorNameColor = chatMessagePeerIdColors[Int(author.id.id % 6)]
|
|
}
|
|
|
|
if authorNameString != nil || inlineBotNameString != nil {
|
|
if headerSize.height < CGFloat(FLT_EPSILON) {
|
|
headerSize.height += 4.0
|
|
}
|
|
|
|
let inlineBotNameColor = incoming ? UIColor(0x1195f2) : UIColor(0x00a700)
|
|
|
|
let attributedString: NSAttributedString
|
|
if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString {
|
|
let botPrefixString: NSString = " via "
|
|
let mutableString = NSMutableAttributedString(string: "\(authorNameString)\(botPrefixString)@\(inlineBotNameString)", attributes: [NSFontAttributeName: inlineBotNameFont, NSForegroundColorAttributeName: inlineBotNameColor])
|
|
mutableString.addAttributes([NSFontAttributeName: nameFont, NSForegroundColorAttributeName: authorNameColor], range: NSMakeRange(0, (authorNameString as NSString).length))
|
|
mutableString.addAttributes([NSFontAttributeName: inlineBotPrefixFont, NSForegroundColorAttributeName: inlineBotNameColor], range: NSMakeRange((authorNameString as NSString).length, botPrefixString.length))
|
|
attributedString = mutableString
|
|
} else if let authorNameString = authorNameString, let authorNameColor = authorNameColor {
|
|
attributedString = NSAttributedString(string: authorNameString, font: nameFont, textColor: authorNameColor)
|
|
} else if let inlineBotNameString = inlineBotNameString {
|
|
attributedString = NSAttributedString(string: "via @\(inlineBotNameString)", font: inlineBotNameFont, textColor: inlineBotNameColor)
|
|
} else {
|
|
attributedString = NSAttributedString(string: "", font: nameFont, textColor: UIColor.black)
|
|
}
|
|
|
|
let sizeAndApply = authorNameLayout(attributedString, nil, 1, .end, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), nil)
|
|
nameNodeSizeApply = (sizeAndApply.0.size, {
|
|
return sizeAndApply.1()
|
|
})
|
|
nameNodeOriginY = headerSize.height
|
|
headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
|
|
headerSize.height += nameNodeSizeApply.0.height
|
|
}
|
|
|
|
if let forwardInfo = message.forwardInfo {
|
|
if headerSize.height < CGFloat(FLT_EPSILON) {
|
|
headerSize.height += 4.0
|
|
}
|
|
let sizeAndApply = forwardInfoLayout(incoming, forwardInfo.source == nil ? forwardInfo.author : forwardInfo.source!, forwardInfo.source == nil ? nil : forwardInfo.author, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
forwardInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
|
|
|
|
forwardInfoOriginY = headerSize.height
|
|
headerSize.width = max(headerSize.width, forwardInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
|
|
headerSize.height += forwardInfoSizeApply.0.height
|
|
}
|
|
|
|
if let replyMessage = replyMessage {
|
|
if headerSize.height < CGFloat(FLT_EPSILON) {
|
|
headerSize.height += 6.0
|
|
} else {
|
|
headerSize.height += 2.0
|
|
}
|
|
let sizeAndApply = replyInfoLayout(incoming, replyMessage, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
|
|
|
|
replyInfoOriginY = headerSize.height
|
|
headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
|
|
headerSize.height += replyInfoSizeApply.0.height + 2.0
|
|
}
|
|
|
|
if headerSize.height > CGFloat(FLT_EPSILON) {
|
|
headerSize.height -= 3.0
|
|
}
|
|
}
|
|
|
|
var removedContentNodeIndices: [Int]?
|
|
findRemoved: for i in 0 ..< currentContentClassesPropertiesAndLayouts.count {
|
|
let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].0
|
|
for contentNodeClass in contentNodeClasses {
|
|
if currentClass == contentNodeClass {
|
|
continue findRemoved
|
|
}
|
|
}
|
|
if removedContentNodeIndices == nil {
|
|
removedContentNodeIndices = [i]
|
|
} else {
|
|
removedContentNodeIndices!.append(i)
|
|
}
|
|
}
|
|
|
|
var contentNodePropertiesAndFinalize: [(ChatMessageBubbleContentProperties, (CGFloat) -> (CGSize, () -> Void))] = []
|
|
|
|
var maxContentWidth: CGFloat = headerSize.width
|
|
for (contentNodeProperties, contentNodeLayout) in contentPropertiesAndLayouts {
|
|
let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
maxContentWidth = max(maxContentWidth, contentNodeWidth)
|
|
|
|
contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize))
|
|
}
|
|
|
|
var contentSize = CGSize(width: maxContentWidth, height: 0.0)
|
|
index = 0
|
|
var contentNodeSizesPropertiesAndApply: [(CGSize, ChatMessageBubbleContentProperties, () -> Void)] = []
|
|
for (properties, finalize) in contentNodePropertiesAndFinalize {
|
|
let (size, apply) = finalize(maxContentWidth)
|
|
contentNodeSizesPropertiesAndApply.append((size, properties, apply))
|
|
|
|
contentSize.height += size.height
|
|
|
|
if index == 0 && headerSize.height > CGFloat(FLT_EPSILON) {
|
|
contentSize.height += properties.headerSpacing
|
|
}
|
|
|
|
index += 1
|
|
}
|
|
|
|
let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(layoutConstants.bubble.minimumSize.height, headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom))
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (layoutConstants.bubble.edgeInset + avatarInset) : (width - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize)
|
|
|
|
let contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height)
|
|
|
|
let layoutSize = CGSize(width: width, height: layoutBubbleSize.height)
|
|
let layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0)
|
|
|
|
let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets)
|
|
|
|
return (layout, { [weak self] animation in
|
|
if let strongSelf = self {
|
|
strongSelf.messageId = message.id
|
|
|
|
if let nameNode = nameNodeSizeApply.1() {
|
|
strongSelf.nameNode = nameNode
|
|
if nameNode.supernode == nil {
|
|
if !nameNode.isNodeLoaded {
|
|
nameNode.isLayerBacked = true
|
|
}
|
|
strongSelf.addSubnode(nameNode)
|
|
}
|
|
nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0)
|
|
} else {
|
|
strongSelf.nameNode?.removeFromSupernode()
|
|
strongSelf.nameNode = nil
|
|
}
|
|
|
|
if let forwardInfoNode = forwardInfoSizeApply.1() {
|
|
strongSelf.forwardInfoNode = forwardInfoNode
|
|
if forwardInfoNode.supernode == nil {
|
|
strongSelf.addSubnode(forwardInfoNode)
|
|
}
|
|
forwardInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + forwardInfoOriginY), size: forwardInfoSizeApply.0)
|
|
} else {
|
|
strongSelf.forwardInfoNode?.removeFromSupernode()
|
|
strongSelf.forwardInfoNode = nil
|
|
}
|
|
|
|
if let replyInfoNode = replyInfoSizeApply.1() {
|
|
strongSelf.replyInfoNode = replyInfoNode
|
|
if replyInfoNode.supernode == nil {
|
|
strongSelf.addSubnode(replyInfoNode)
|
|
}
|
|
replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0)
|
|
} else {
|
|
strongSelf.replyInfoNode?.removeFromSupernode()
|
|
strongSelf.replyInfoNode = nil
|
|
}
|
|
|
|
if removedContentNodeIndices?.count ?? 0 != 0 || addedContentNodes?.count ?? 0 != 0 {
|
|
var updatedContentNodes = strongSelf.contentNodes
|
|
|
|
if let removedContentNodeIndices = removedContentNodeIndices {
|
|
for index in removedContentNodeIndices.reversed() {
|
|
updatedContentNodes[index].removeFromSupernode()
|
|
let _ = updatedContentNodes.remove(at: index)
|
|
}
|
|
}
|
|
|
|
if let addedContentNodes = addedContentNodes {
|
|
for contentNode in addedContentNodes {
|
|
updatedContentNodes.append(contentNode)
|
|
strongSelf.addSubnode(contentNode)
|
|
contentNode.controllerInteraction = strongSelf.controllerInteraction
|
|
}
|
|
}
|
|
|
|
strongSelf.contentNodes = updatedContentNodes
|
|
}
|
|
|
|
var contentNodeOrigin = contentOrigin
|
|
var contentNodeIndex = 0
|
|
for (size, properties, apply) in contentNodeSizesPropertiesAndApply {
|
|
apply()
|
|
if contentNodeIndex == 0 && headerSize.height > CGFloat(FLT_EPSILON) {
|
|
contentNodeOrigin.y += properties.headerSpacing
|
|
}
|
|
let contentNode = strongSelf.contentNodes[contentNodeIndex]
|
|
let contentNodeFrame = CGRect(origin: contentNodeOrigin, size: size)
|
|
let previousContentNodeFrame = contentNode.frame
|
|
contentNode.frame = contentNodeFrame
|
|
|
|
if case let .System(duration) = animation {
|
|
var animateFrame = false
|
|
var animateAlpha = false
|
|
if let addedContentNodes = addedContentNodes {
|
|
if !addedContentNodes.contains(where: { $0 === contentNode }) {
|
|
animateFrame = true
|
|
} else {
|
|
animateAlpha = true
|
|
}
|
|
} else {
|
|
animateFrame = true
|
|
}
|
|
|
|
if animateFrame {
|
|
contentNode.layer.animateFrame(from: previousContentNodeFrame, to: contentNodeFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
} else if animateAlpha {
|
|
contentNode.animateInsertionIntoBubble(duration)
|
|
var previousAlignedContentNodeFrame = contentNodeFrame
|
|
previousAlignedContentNodeFrame.origin.x += backgroundFrame.size.width - strongSelf.backgroundNode.frame.size.width
|
|
contentNode.layer.animateFrame(from: previousAlignedContentNodeFrame, to: contentNodeFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
contentNodeIndex += 1
|
|
contentNodeOrigin.y += size.height
|
|
}
|
|
|
|
let mergeType = ChatMessageBackgroundMergeType(top: mergedBottom, bottom: mergedTop)
|
|
if !incoming {
|
|
strongSelf.backgroundNode.setType(type: .Outgoing(mergeType))
|
|
} else {
|
|
strongSelf.backgroundNode.setType(type: .Incoming(mergeType))
|
|
}
|
|
|
|
if case .System = animation {
|
|
strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame)
|
|
strongSelf.enableTransitionClippingNode()
|
|
} else {
|
|
if let _ = strongSelf.backgroundFrameTransition {
|
|
strongSelf.animateFrameTransition(1.0)
|
|
strongSelf.backgroundFrameTransition = nil
|
|
}
|
|
strongSelf.backgroundNode.frame = backgroundFrame
|
|
strongSelf.disableTransitionClippingNode()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private func addContentNode(node: ChatMessageBubbleContentNode) {
|
|
if let transitionClippingNode = self.transitionClippingNode {
|
|
transitionClippingNode.addSubnode(node)
|
|
} else {
|
|
self.addSubnode(node)
|
|
}
|
|
}
|
|
|
|
private func enableTransitionClippingNode() {
|
|
if self.transitionClippingNode == nil {
|
|
let node = ASDisplayNode()
|
|
node.clipsToBounds = true
|
|
var backgroundFrame = self.backgroundNode.frame
|
|
backgroundFrame = backgroundFrame.insetBy(dx: 0.0, dy: 1.0)
|
|
node.frame = backgroundFrame
|
|
node.bounds = CGRect(origin: CGPoint(x: backgroundFrame.origin.x, y: backgroundFrame.origin.y), size: backgroundFrame.size)
|
|
for contentNode in self.contentNodes {
|
|
node.addSubnode(contentNode)
|
|
}
|
|
self.addSubnode(node)
|
|
self.transitionClippingNode = node
|
|
}
|
|
}
|
|
|
|
private func disableTransitionClippingNode() {
|
|
if let transitionClippingNode = self.transitionClippingNode {
|
|
for contentNode in self.contentNodes {
|
|
self.addSubnode(contentNode)
|
|
}
|
|
transitionClippingNode.removeFromSupernode()
|
|
self.transitionClippingNode = nil
|
|
}
|
|
}
|
|
|
|
override func animateFrameTransition(_ progress: CGFloat) {
|
|
super.animateFrameTransition(progress)
|
|
|
|
if let backgroundFrameTransition = self.backgroundFrameTransition {
|
|
let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect
|
|
self.backgroundNode.frame = backgroundFrame
|
|
|
|
if let transitionClippingNode = self.transitionClippingNode {
|
|
var fixedBackgroundFrame = backgroundFrame
|
|
fixedBackgroundFrame = fixedBackgroundFrame.insetBy(dx: 0.0, dy: 1.0)
|
|
|
|
transitionClippingNode.frame = fixedBackgroundFrame
|
|
transitionClippingNode.bounds = CGRect(origin: CGPoint(x: fixedBackgroundFrame.origin.x, y: fixedBackgroundFrame.origin.y), size: fixedBackgroundFrame.size)
|
|
|
|
if progress >= 1.0 - CGFloat(FLT_EPSILON) {
|
|
self.disableTransitionClippingNode()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .ended:
|
|
let location = recognizer.location(in: self.view)
|
|
if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) {
|
|
if let item = self.item {
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? ReplyMessageAttribute {
|
|
self.controllerInteraction?.testNavigateToMessage(item.message.id, attribute.messageId)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
//self.controllerInteraction?.testNavigateToMessage(messageId)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? {
|
|
if let item = self.item, item.message.id == id {
|
|
for contentNode in self.contentNodes {
|
|
if let result = contentNode.transitionNode(media: media) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override func updateHiddenMedia() {
|
|
if let item = self.item, let controllerInteraction = self.controllerInteraction {
|
|
for contentNode in self.contentNodes {
|
|
contentNode.updateHiddenMedia(controllerInteraction.hiddenMedia[item.message.id])
|
|
}
|
|
}
|
|
}
|
|
}
|