mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
977 lines
52 KiB
Swift
977 lines
52 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
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 || (file.isAnimated && file.dimensions != nil) {
|
|
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 selectionNode: ChatMessageSelectionNode?
|
|
|
|
private var nameNode: TextNode?
|
|
private var forwardInfoNode: ChatMessageForwardInfoNode?
|
|
private var replyInfoNode: ChatMessageReplyInfoNode?
|
|
|
|
private var contentNodes: [ChatMessageBubbleContentNode] = []
|
|
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
|
|
|
private var messageId: MessageId?
|
|
private var messageStableId: UInt32?
|
|
private var backgroundType: ChatMessageBackgroundType?
|
|
private var highlightedState: Bool = false
|
|
|
|
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, short: Bool) {
|
|
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
|
|
|
for node in self.subnodes {
|
|
if node !== self.accessoryItemNode {
|
|
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
for contentNode in self.contentNodes {
|
|
//contentNode.animateInsertion(currentTimestamp, duration: duration)
|
|
}
|
|
}
|
|
|
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
super.animateRemoved(currentTimestamp, duration: duration)
|
|
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
|
|
for contentNode in self.contentNodes {
|
|
//contentNode.animateRemoved(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()
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
recognizer.tapActionAtPoint = { [weak self] point in
|
|
if let strongSelf = self {
|
|
if let avatarNode = strongSelf.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) {
|
|
return .waitForSingleTap
|
|
}
|
|
|
|
if let nameNode = strongSelf.nameNode, nameNode.frame.contains(point) {
|
|
if let item = strongSelf.item {
|
|
for attribute in item.message.attributes {
|
|
if let _ = attribute as? InlineBotMessageAttribute {
|
|
return .waitForSingleTap
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let replyInfoNode = strongSelf.replyInfoNode, replyInfoNode.frame.contains(point) {
|
|
return .waitForSingleTap
|
|
}
|
|
if let forwardInfoNode = strongSelf.forwardInfoNode, forwardInfoNode.frame.contains(point) {
|
|
return .waitForSingleTap
|
|
}
|
|
for contentNode in strongSelf.contentNodes {
|
|
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY))
|
|
switch tapAction {
|
|
case .none:
|
|
break
|
|
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage:
|
|
return .waitForSingleTap
|
|
case .holdToPreviewSecretMedia:
|
|
return .waitForHold
|
|
}
|
|
}
|
|
}
|
|
|
|
return .waitForDoubleTap
|
|
}
|
|
self.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
|
var currentContentClassesPropertiesAndLayouts: [(AnyClass, ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> 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 actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
|
|
|
let layoutConstants = self.layoutConstants
|
|
|
|
return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in
|
|
let message = item.message
|
|
let incoming = item.message.effectivelyIncoming
|
|
|
|
let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroupOrChannel && item.message.author != nil
|
|
|
|
let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && 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, (ListViewItemUpdateAnimation) -> 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?
|
|
var replyMarkup: ReplyMarkupMessageAttribute?
|
|
|
|
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]
|
|
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
|
replyMarkup = attribute
|
|
}
|
|
}
|
|
|
|
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, (ListViewItemUpdateAnimation) -> 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.ulpOfOne {
|
|
headerSize.height += 4.0
|
|
}
|
|
|
|
let inlineBotNameColor = incoming ? UIColor(0x007ee5) : 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), .natural, 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.ulpOfOne {
|
|
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.ulpOfOne {
|
|
headerSize.height += 6.0
|
|
} else {
|
|
headerSize.height += 2.0
|
|
}
|
|
let sizeAndApply = replyInfoLayout(item.account, .bubble(incoming: 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.ulpOfOne {
|
|
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, (ListViewItemUpdateAnimation) -> 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 actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
|
if let replyMarkup = replyMarkup {
|
|
let (minWidth, buttonsLayout) = actionButtonsLayout(replyMarkup, maximumNodeWidth)
|
|
maxContentWidth = max(maxContentWidth, minWidth)
|
|
actionButtonsFinalize = buttonsLayout
|
|
}
|
|
|
|
var contentSize = CGSize(width: maxContentWidth, height: 0.0)
|
|
index = 0
|
|
var contentNodeSizesPropertiesAndApply: [(CGSize, ChatMessageBubbleContentProperties, (ListViewItemUpdateAnimation) -> 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.ulpOfOne {
|
|
contentSize.height += properties.headerSpacing
|
|
}
|
|
|
|
index += 1
|
|
}
|
|
|
|
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
|
if let actionButtonsFinalize = actionButtonsFinalize {
|
|
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
|
}
|
|
|
|
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)
|
|
|
|
var layoutSize = CGSize(width: width, height: layoutBubbleSize.height)
|
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
|
layoutSize.height += actionButtonsSizeAndApply.0.height
|
|
}
|
|
|
|
var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0)
|
|
if dateHeaderAtBottom {
|
|
layoutInsets.top += layoutConstants.timestampHeaderHeight
|
|
}
|
|
|
|
let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets)
|
|
|
|
return (layout, { [weak self] animation in
|
|
if let strongSelf = self {
|
|
strongSelf.messageId = message.id
|
|
strongSelf.messageStableId = message.stableId
|
|
|
|
let mergeType = ChatMessageBackgroundMergeType(top: mergedBottom, bottom: mergedTop)
|
|
let backgroundType: ChatMessageBackgroundType
|
|
if !incoming {
|
|
backgroundType = .Outgoing(mergeType)
|
|
} else {
|
|
backgroundType = .Incoming(mergeType)
|
|
}
|
|
strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState)
|
|
|
|
strongSelf.backgroundType = backgroundType
|
|
|
|
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
|
|
var animateFrame = true
|
|
if forwardInfoNode.supernode == nil {
|
|
strongSelf.addSubnode(forwardInfoNode)
|
|
animateFrame = false
|
|
}
|
|
let previousForwardInfoNodeFrame = forwardInfoNode.frame
|
|
forwardInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + forwardInfoOriginY), size: forwardInfoSizeApply.0)
|
|
if case let .System(duration) = animation {
|
|
if animateFrame {
|
|
forwardInfoNode.layer.animateFrame(from: previousForwardInfoNodeFrame, to: forwardInfoNode.frame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
} else {
|
|
strongSelf.forwardInfoNode?.removeFromSupernode()
|
|
strongSelf.forwardInfoNode = nil
|
|
}
|
|
|
|
if let replyInfoNode = replyInfoSizeApply.1() {
|
|
strongSelf.replyInfoNode = replyInfoNode
|
|
var animateFrame = true
|
|
if replyInfoNode.supernode == nil {
|
|
strongSelf.addSubnode(replyInfoNode)
|
|
animateFrame = false
|
|
}
|
|
let previousReplyInfoNodeFrame = replyInfoNode.frame
|
|
replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0)
|
|
if case let .System(duration) = animation {
|
|
if animateFrame {
|
|
replyInfoNode.layer.animateFrame(from: previousReplyInfoNodeFrame, to: replyInfoNode.frame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
} 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(animation)
|
|
if contentNodeIndex == 0 && headerSize.height > CGFloat.ulpOfOne {
|
|
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
|
|
}
|
|
|
|
if case .System = animation {
|
|
if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) {
|
|
strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame)
|
|
strongSelf.enableTransitionClippingNode()
|
|
}
|
|
} else {
|
|
if let _ = strongSelf.backgroundFrameTransition {
|
|
strongSelf.animateFrameTransition(1.0, backgroundFrame.size.height)
|
|
strongSelf.backgroundFrameTransition = nil
|
|
}
|
|
strongSelf.backgroundNode.frame = backgroundFrame
|
|
strongSelf.disableTransitionClippingNode()
|
|
}
|
|
let offset: CGFloat = incoming ? 42.0 : 0.0
|
|
strongSelf.selectionNode?.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: width, height: layout.size.height))
|
|
|
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
|
var animated = false
|
|
if let _ = strongSelf.actionButtonsNode {
|
|
if case .System = animation {
|
|
animated = true
|
|
}
|
|
}
|
|
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
|
let previousFrame = actionButtonsNode.frame
|
|
let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
|
|
actionButtonsNode.frame = actionButtonsFrame
|
|
if actionButtonsNode !== strongSelf.actionButtonsNode {
|
|
strongSelf.actionButtonsNode = actionButtonsNode
|
|
actionButtonsNode.buttonPressed = { button in
|
|
if let strongSelf = self {
|
|
strongSelf.performMessageButtonAction(button: button)
|
|
}
|
|
}
|
|
strongSelf.addSubnode(actionButtonsNode)
|
|
} else {
|
|
if case let .System(duration) = animation {
|
|
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
} else if let actionButtonsNode = strongSelf.actionButtonsNode {
|
|
actionButtonsNode.removeFromSupernode()
|
|
strongSelf.actionButtonsNode = nil
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
if let forwardInfoNode = self.forwardInfoNode {
|
|
node.addSubnode(forwardInfoNode)
|
|
}
|
|
if let replyInfoNode = self.replyInfoNode {
|
|
node.addSubnode(replyInfoNode)
|
|
}
|
|
for contentNode in self.contentNodes {
|
|
node.addSubnode(contentNode)
|
|
}
|
|
self.addSubnode(node)
|
|
self.transitionClippingNode = node
|
|
}
|
|
}
|
|
|
|
private func disableTransitionClippingNode() {
|
|
if let transitionClippingNode = self.transitionClippingNode {
|
|
if let forwardInfoNode = self.forwardInfoNode {
|
|
self.addSubnode(forwardInfoNode)
|
|
}
|
|
if let replyInfoNode = self.replyInfoNode {
|
|
self.addSubnode(replyInfoNode)
|
|
}
|
|
for contentNode in self.contentNodes {
|
|
self.addSubnode(contentNode)
|
|
}
|
|
transitionClippingNode.removeFromSupernode()
|
|
self.transitionClippingNode = nil
|
|
}
|
|
}
|
|
|
|
override func shouldAnimateHorizontalFrameTransition() -> Bool {
|
|
if let _ = self.backgroundFrameTransition {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
|
super.animateFrameTransition(progress, currentValue)
|
|
|
|
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.ulpOfOne {
|
|
self.disableTransitionClippingNode()
|
|
}
|
|
}
|
|
|
|
if CGFloat(1.0).isLessThanOrEqualTo(progress) {
|
|
self.backgroundFrameTransition = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture {
|
|
if let item = self.item, item.message.containsSecretMedia {
|
|
self.controllerInteraction?.openSecretMessagePreview(item.message.id)
|
|
}
|
|
}
|
|
case .ended:
|
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
switch gesture {
|
|
case .tap:
|
|
if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) {
|
|
if let item = self.item, let author = item.message.author {
|
|
self.controllerInteraction?.openPeer(author.id, .info, item.message.id)
|
|
}
|
|
return
|
|
}
|
|
|
|
if let nameNode = self.nameNode, nameNode.frame.contains(location) {
|
|
if let item = self.item {
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? InlineBotMessageAttribute, let botPeer = item.message.peers[attribute.peerId], let addressName = botPeer.addressName {
|
|
self.controllerInteraction?.updateInputState { textInputState in
|
|
return ChatTextInputState(inputText: "@" + addressName + " ")
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
} else 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?.navigateToMessage(item.message.id, attribute.messageId)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) {
|
|
if let item = self.item, let forwardInfo = item.message.forwardInfo {
|
|
if let sourceMessageId = forwardInfo.sourceMessageId {
|
|
self.controllerInteraction?.navigateToMessage(item.message.id, sourceMessageId)
|
|
} else {
|
|
self.controllerInteraction?.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil), nil)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
var foundTapAction = false
|
|
loop: for contentNode in self.contentNodes {
|
|
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY))
|
|
switch tapAction {
|
|
case .none:
|
|
break
|
|
case let .url(url):
|
|
foundTapAction = true
|
|
if let controllerInteraction = self.controllerInteraction {
|
|
controllerInteraction.openUrl(url)
|
|
}
|
|
break loop
|
|
case let .peerMention(peerId):
|
|
foundTapAction = true
|
|
if let controllerInteraction = self.controllerInteraction {
|
|
controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil)
|
|
}
|
|
break loop
|
|
case let .textMention(name):
|
|
foundTapAction = true
|
|
if let controllerInteraction = self.controllerInteraction {
|
|
controllerInteraction.openPeerMention(name)
|
|
}
|
|
break loop
|
|
case let .botCommand(command):
|
|
foundTapAction = true
|
|
if let item = self.item, let controllerInteraction = self.controllerInteraction {
|
|
controllerInteraction.sendBotCommand(item.message.id, command)
|
|
}
|
|
break loop
|
|
case let .hashtag(peerName, hashtag):
|
|
foundTapAction = true
|
|
if let controllerInteraction = self.controllerInteraction {
|
|
controllerInteraction.openHashtag(peerName, hashtag)
|
|
}
|
|
break loop
|
|
case .instantPage:
|
|
foundTapAction = true
|
|
if let item = self.item, let controllerInteraction = self.controllerInteraction {
|
|
controllerInteraction.openInstantPage(item.message.id)
|
|
}
|
|
break loop
|
|
case .holdToPreviewSecretMedia:
|
|
foundTapAction = true
|
|
break
|
|
}
|
|
}
|
|
if !foundTapAction {
|
|
self.controllerInteraction?.clickThroughMessage()
|
|
}
|
|
case .longTap, .doubleTap:
|
|
if let item = self.item, self.backgroundNode.frame.contains(location) {
|
|
self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.backgroundNode.frame)
|
|
}
|
|
case .hold:
|
|
if let item = self.item, item.message.containsSecretMedia {
|
|
self.controllerInteraction?.closeSecretMessagePreview()
|
|
}
|
|
}
|
|
}
|
|
case .cancelled:
|
|
if let item = self.item, item.message.containsSecretMedia {
|
|
self.controllerInteraction?.closeSecretMessagePreview()
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) {
|
|
return self.view
|
|
}
|
|
|
|
if let selectionNode = self.selectionNode {
|
|
if selectionNode.frame.offsetBy(dx: 42.0, dy: 0.0).contains(point) {
|
|
return selectionNode.view
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if !self.backgroundNode.frame.contains(point) {
|
|
if self.actionButtonsNode == nil || !self.actionButtonsNode!.frame.contains(point) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
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])
|
|
}
|
|
}
|
|
}
|
|
|
|
override func updateSelectionState(animated: Bool) {
|
|
guard let controllerInteraction = self.controllerInteraction else {
|
|
return
|
|
}
|
|
|
|
if let selectionState = controllerInteraction.selectionState {
|
|
var selected = false
|
|
var incoming = true
|
|
if let item = self.item {
|
|
selected = selectionState.selectedIds.contains(item.message.id)
|
|
incoming = item.message.effectivelyIncoming
|
|
}
|
|
let offset: CGFloat = incoming ? 42.0 : 0.0
|
|
|
|
if let selectionNode = self.selectionNode {
|
|
selectionNode.updateSelected(selected, animated: false)
|
|
selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height))
|
|
self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0);
|
|
} else {
|
|
let selectionNode = ChatMessageSelectionNode(toggle: { [weak self] in
|
|
if let strongSelf = self, let item = strongSelf.item {
|
|
strongSelf.controllerInteraction?.toggleMessageSelection(item.message.id)
|
|
}
|
|
})
|
|
|
|
selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height))
|
|
self.addSubnode(selectionNode)
|
|
self.selectionNode = selectionNode
|
|
selectionNode.updateSelected(selected, animated: false)
|
|
let previousSubnodeTransform = self.subnodeTransform
|
|
self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0);
|
|
if animated {
|
|
selectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
|
|
|
|
if !incoming {
|
|
let position = selectionNode.layer.position
|
|
selectionNode.layer.animatePosition(from: CGPoint(x: position.x - 42.0, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if let selectionNode = self.selectionNode {
|
|
self.selectionNode = nil
|
|
let previousSubnodeTransform = self.subnodeTransform
|
|
self.subnodeTransform = CATransform3DIdentity
|
|
if animated {
|
|
self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, completion: { [weak selectionNode]_ in
|
|
selectionNode?.removeFromSupernode()
|
|
})
|
|
selectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
|
if CGFloat(0.0).isLessThanOrEqualTo(selectionNode.frame.origin.x) {
|
|
let position = selectionNode.layer.position
|
|
selectionNode.layer.animatePosition(from: position, to: CGPoint(x: position.x - 42.0, y: position.y), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
}
|
|
} else {
|
|
selectionNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func updateHighlightedState(animated: Bool) {
|
|
if let controllerInteraction = self.controllerInteraction {
|
|
var highlighted = false
|
|
if let messageStableId = self.messageStableId, let highlightedState = controllerInteraction.highlightedState {
|
|
if highlightedState.messageStableId == messageStableId {
|
|
highlighted = true
|
|
}
|
|
}
|
|
|
|
if self.highlightedState != highlighted {
|
|
self.highlightedState = highlighted
|
|
if let backgroundType = self.backgroundType {
|
|
if highlighted {
|
|
self.backgroundNode.setType(type: backgroundType, highlighted: true)
|
|
} else {
|
|
if let previousContents = self.backgroundNode.layer.contents, animated {
|
|
self.backgroundNode.setType(type: backgroundType, highlighted: false)
|
|
|
|
if let updatedContents = self.backgroundNode.layer.contents {
|
|
self.backgroundNode.layer.animate(from: previousContents as AnyObject, to: updatedContents as AnyObject, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.3)
|
|
}
|
|
} else {
|
|
self.backgroundNode.setType(type: backgroundType, highlighted: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performMessageButtonAction(button: ReplyMarkupButton) {
|
|
if let item = self.item, let controllerInteraction = self.controllerInteraction {
|
|
switch button.action {
|
|
case .text:
|
|
controllerInteraction.sendMessage(button.title)
|
|
case let .url(url):
|
|
controllerInteraction.openUrl(url)
|
|
case .requestMap:
|
|
controllerInteraction.shareCurrentLocation()
|
|
case .requestPhone:
|
|
controllerInteraction.shareAccountContact()
|
|
case .openWebApp:
|
|
controllerInteraction.requestMessageActionCallback(item.message.id, nil, true)
|
|
case let .callback(data):
|
|
controllerInteraction.requestMessageActionCallback(item.message.id, data, false)
|
|
case let .switchInline(samePeer, query):
|
|
var botPeer: Peer?
|
|
|
|
var found = false
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? InlineBotMessageAttribute {
|
|
botPeer = item.message.peers[attribute.peerId]
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
botPeer = item.message.author
|
|
}
|
|
|
|
var peerId: PeerId?
|
|
if samePeer {
|
|
peerId = item.message.id.peerId
|
|
}
|
|
if let botPeer = botPeer, let addressName = botPeer.addressName {
|
|
controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: "@\(addressName) \(query)")), nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|