Swiftgram/TelegramUI/ChatMessageBubbleItemNode.swift
2018-12-04 02:26:57 +04:00

2010 lines
109 KiB
Swift

import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass)] {
var result: [(Message, AnyClass)] = []
var skipText = false
var addFinalText = false
outer: for message in item.content {
inner: for media in message.media {
if let _ = media as? TelegramMediaImage {
result.append((message, ChatMessageMediaBubbleContentNode.self))
} else if let file = media as? TelegramMediaFile {
if file.isVideo || (file.isAnimated && file.dimensions != nil) {
result.append((message, ChatMessageMediaBubbleContentNode.self))
} else {
result.append((message, ChatMessageFileBubbleContentNode.self))
}
} else if let action = media as? TelegramMediaAction {
if case .phoneCall = action.action {
result.append((message, ChatMessageCallBubbleContentNode.self))
} else {
result.append((message, ChatMessageActionBubbleContentNode.self))
}
} else if let _ = media as? TelegramMediaMap {
result.append((message, ChatMessageMapBubbleContentNode.self))
} else if let _ = media as? TelegramMediaGame {
skipText = true
result.append((message, ChatMessageGameBubbleContentNode.self))
break inner
} else if let _ = media as? TelegramMediaInvoice {
skipText = true
result.append((message, ChatMessageInvoiceBubbleContentNode.self))
break inner
} else if let _ = media as? TelegramMediaContact {
result.append((message, ChatMessageContactBubbleContentNode.self))
} else if let _ = media as? TelegramMediaExpiredContent {
result.removeAll()
result.append((message, ChatMessageActionBubbleContentNode.self))
return result
}
}
if !message.text.isEmpty {
if !skipText {
if case .group = item.content {
addFinalText = true
skipText = true
} else {
result.append((message, ChatMessageTextBubbleContentNode.self))
}
} else {
if case .group = item.content {
addFinalText = false
}
}
}
inner: for media in message.media {
if let webpage = media as? TelegramMediaWebpage {
if case .Loaded = webpage.content {
result.append((message, ChatMessageWebpageBubbleContentNode.self))
}
break inner
}
}
}
if addFinalText && !item.content.firstMessage.text.isEmpty {
result.append((item.content.firstMessage, ChatMessageTextBubbleContentNode.self))
}
if let additionalContent = item.additionalContent {
switch additionalContent {
case let .eventLogPreviousMessage(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self))
case let .eventLogPreviousDescription(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self))
case let .eventLogPreviousLink(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self))
}
}
return result
}
private let nameFont = Font.medium(14.0)
private let inlineBotPrefixFont = Font.regular(14.0)
private let inlineBotNameFont = nameFont
private let chatMessagePeerIdColors: [UIColor] = [
UIColor(rgb: 0xfc5c51),
UIColor(rgb: 0xfa790f),
UIColor(rgb: 0x895dd5),
UIColor(rgb: 0x0fb297),
UIColor(rgb: 0x00c0c2),
UIColor(rgb: 0x3ca5ec),
UIColor(rgb: 0x3d72ed)
]
private enum ContentNodeOperation {
case remove(index: Int)
case insert(index: Int, node: ChatMessageBubbleContentNode)
}
class ChatMessageBubbleItemNode: ChatMessageItemView {
private let backgroundNode: ChatMessageBackground
private var transitionClippingNode: ASDisplayNode?
private var selectionNode: ChatMessageSelectionNode?
private var deliveryFailedNode: ChatMessageDeliveryFailedNode?
private var swipeToReplyNode: ChatMessageSwipeToReplyNode?
private var swipeToReplyFeedback: HapticFeedback?
private var nameNode: TextNode?
private var adminBadgeNode: TextNode?
private var forwardInfoNode: ChatMessageForwardInfoNode?
private var replyInfoNode: ChatMessageReplyInfoNode?
private var contentNodes: [ChatMessageBubbleContentNode] = []
private var mosaicStatusNode: ChatMessageDateAndStatusNode?
private var actionButtonsNode: ChatMessageActionButtonsNode?
private var shareButtonNode: HighlightableButtonNode?
private var backgroundType: ChatMessageBackgroundType?
private var highlightedState: Bool = false
private var backgroundFrameTransition: (CGRect, CGRect)?
private var currentSwipeToReplyTranslation: CGFloat = 0.0
private var appliedItem: ChatMessageItem?
override var visibility: ListViewItemNodeVisibility {
didSet {
if self.visibility != oldValue {
for contentNode in self.contentNodes {
contentNode.visibility = self.visibility
}
}
}
}
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)
if let subnodes = self.subnodes {
for node in subnodes {
if node !== self.accessoryItemNode {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
}
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)
}
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.adminBadgeNode?.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 shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
return .fail
}
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 .ignore:
return .fail
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .call, .openMessage:
return .waitForSingleTap
}
}
if !strongSelf.backgroundNode.frame.contains(point) {
return .waitForSingleTap
}
}
return .waitForDoubleTap
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
for contentNode in strongSelf.contentNodes {
var translatedPoint: CGPoint?
if let point = point, contentNode.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
translatedPoint = CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY)
}
contentNode.updateTouchesAtPoint(translatedPoint)
}
}
}
self.view.addGestureRecognizer(recognizer)
let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:)))
replyRecognizer.shouldBegin = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
if strongSelf.selectionNode != nil {
return false
}
for media in item.content.firstMessage.media {
if let _ = media as? TelegramMediaExpiredContent {
return false
}
else if let media = media as? TelegramMediaAction {
if case .phoneCall(_, _, _) = media.action {
} else {
return false
}
}
}
return item.controllerInteraction.canSetupReply(item.message)
}
return false
}
self.view.addGestureRecognizer(replyRecognizer)
}
override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))))] = []
for contentNode in self.contentNodes {
if let message = contentNode.item?.message {
currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.asyncLayoutContent()))
} else {
assertionFailure()
}
}
let authorNameLayout = TextNode.asyncLayout(self.nameNode)
let adminBadgeLayout = TextNode.asyncLayout(self.adminBadgeNode)
let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode)
let currentShareButtonNode = self.shareButtonNode
let layoutConstants = self.layoutConstants
let currentItem = self.appliedItem
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
let baseWidth = params.width - params.leftInset - params.rightInset
let content = item.content
let firstMessage = content.firstMessage
let incoming = item.content.effectivelyIncoming(item.account.peerId)
var effectiveAuthor: Peer?
var ignoreForward = false
let displayAuthorInfo: Bool
let avatarInset: CGFloat
var hasAvatar = false
var allowFullWidth = false
switch item.chatLocation {
case let .peer(peerId):
if item.message.id.peerId == item.account.peerId {
if let forwardInfo = item.content.firstMessage.forwardInfo {
ignoreForward = true
effectiveAuthor = forwardInfo.author
}
displayAuthorInfo = !mergedTop.merged && incoming && effectiveAuthor != nil
} else {
effectiveAuthor = firstMessage.author
displayAuthorInfo = !mergedTop.merged && incoming && peerId.isGroupOrChannel && effectiveAuthor != nil
}
if peerId != item.account.peerId {
if peerId.isGroupOrChannel && effectiveAuthor != nil {
var isBroadcastChannel = false
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
isBroadcastChannel = true
allowFullWidth = true
}
if !isBroadcastChannel {
hasAvatar = item.content.firstMessage.effectivelyIncoming(item.account.peerId)
}
}
} else if incoming {
hasAvatar = true
}
case .group:
allowFullWidth = true
hasAvatar = true
displayAuthorInfo = true
}
if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.source == nil, forwardInfo.author.id.namespace == Namespaces.Peer.CloudUser {
for media in item.content.firstMessage.media {
if let file = media as? TelegramMediaFile, file.isMusic {
ignoreForward = true
break
}
}
}
if hasAvatar {
avatarInset = layoutConstants.avatarDiameter
} else {
avatarInset = 0.0
}
var needShareButton = false
if item.message.flags.contains(.Failed) {
needShareButton = false
} else if item.message.id.peerId == item.account.peerId {
for attribute in item.content.firstMessage.attributes {
if let _ = attribute as? SourceReferenceMessageAttribute {
needShareButton = true
break
}
}
} else if item.message.effectivelyIncoming(item.account.peerId) {
if let peer = item.message.peers[item.message.id.peerId] {
if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
needShareButton = true
}
}
}
if let info = item.message.forwardInfo {
if let author = info.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty && !(item.message.media.first is TelegramMediaAction) {
needShareButton = true
} else if let author = info.author as? TelegramChannel, case .broadcast = author.info {
needShareButton = true
}
}
if !needShareButton, let author = item.message.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty && !(item.message.media.first is TelegramMediaAction) {
needShareButton = true
}
if !needShareButton {
loop: for media in item.message.media {
if media is TelegramMediaGame || media is TelegramMediaInvoice {
needShareButton = true
break loop
} else if let media = media as? TelegramMediaWebpage, case .Loaded = media.content {
needShareButton = true
break loop
}
}
} else {
loop: for media in item.message.media {
if media is TelegramMediaAction {
needShareButton = false
break loop
}
}
}
}
var tmpWidth: CGFloat
if allowFullWidth {
tmpWidth = baseWidth
if needShareButton {
tmpWidth -= 38.0
}
} else {
tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth)
if needShareButton && tmpWidth + 32.0 > baseWidth {
tmpWidth = baseWidth - 32.0
}
}
let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
var contentPropertiesAndPrepareLayouts: [(Message, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))))] = []
var addedContentNodes: [(Message, ChatMessageBubbleContentNode)]?
let contentNodeMessagesAndClasses = contentNodeMessagesAndClassesForItem(item)
for (contentNodeMessage, contentNodeClass) in contentNodeMessagesAndClasses {
var found = false
for (currentMessage, currentClass, supportsMosaic, currentLayout) in currentContentClassesPropertiesAndLayouts {
if currentClass == contentNodeClass && currentMessage.stableId == contentNodeMessage.stableId {
contentPropertiesAndPrepareLayouts.append((contentNodeMessage, supportsMosaic, currentLayout))
found = true
break
}
}
if !found {
let contentNode = (contentNodeClass as! ChatMessageBubbleContentNode.Type).init()
contentPropertiesAndPrepareLayouts.append((contentNodeMessage, contentNode.supportsMosaic, contentNode.asyncLayoutContent()))
if addedContentNodes == nil {
addedContentNodes = []
}
addedContentNodes!.append((contentNodeMessage, contentNode))
}
}
var authorNameString: String?
let authorIsAdmin: Bool
switch content {
case let .message(_, _, _, isAdmin):
authorIsAdmin = isAdmin
case .group:
authorIsAdmin = false
}
var inlineBotNameString: String?
var replyMessage: Message?
var replyMarkup: ReplyMarkupMessageAttribute?
var authorNameColor: UIColor?
for attribute in firstMessage.attributes {
if let attribute = attribute as? InlineBotMessageAttribute {
if let peerId = attribute.peerId, let bot = firstMessage.peers[peerId] as? TelegramUser {
inlineBotNameString = bot.username
} else {
inlineBotNameString = attribute.title
}
} else if let attribute = attribute as? ReplyMessageAttribute {
replyMessage = firstMessage.associatedMessages[attribute.messageId]
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
replyMarkup = attribute
}
}
var contentPropertiesAndLayouts: [(CGSize?, ChatMessageBubbleContentProperties, ChatMessageBubblePreparePosition, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void)))] = []
let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
var backgroundHiding: ChatMessageBubbleContentBackgroundHiding?
var hasSolidWallpaper = false
if case .color = item.presentationData.theme.wallpaper {
hasSolidWallpaper = true
}
var alignment: ChatMessageBubbleContentAlignment = .none
var maximumNodeWidth = maximumContentWidth
let contentNodeCount = contentPropertiesAndPrepareLayouts.count
let read: Bool
switch item.content {
case let .message(_, value, _, _):
read = value
case let .group(messages):
read = messages[0].1
}
var mosaicStartIndex: Int?
var mosaicRange: Range<Int>?
for i in 0 ..< contentPropertiesAndPrepareLayouts.count {
if contentPropertiesAndPrepareLayouts[i].1 {
if mosaicStartIndex == nil {
mosaicStartIndex = i
}
} else if let mosaicStartIndexValue = mosaicStartIndex {
if mosaicStartIndexValue < i - 1 {
mosaicRange = mosaicStartIndexValue ..< i
}
mosaicStartIndex = nil
}
}
if let mosaicStartIndex = mosaicStartIndex {
if mosaicStartIndex < contentPropertiesAndPrepareLayouts.count - 1 {
mosaicRange = mosaicStartIndex ..< contentPropertiesAndPrepareLayouts.count
}
}
var index = 0
for (message, _, prepareLayout) in contentPropertiesAndPrepareLayouts {
let topPosition: ChatMessageBubbleRelativePosition
let bottomPosition: ChatMessageBubbleRelativePosition
topPosition = .Neighbour
bottomPosition = .Neighbour
let prepareContentPosition: ChatMessageBubblePreparePosition
if let mosaicRange = mosaicRange, mosaicRange.contains(index) {
prepareContentPosition = .mosaic(top: .None(.None(.Incoming)), bottom: index == (mosaicRange.upperBound - 1) ? bottomPosition : .None(.None(.Incoming)))
} else {
let refinedBottomPosition: ChatMessageBubbleRelativePosition
if index == contentPropertiesAndPrepareLayouts.count - 1 {
refinedBottomPosition = .None(.Left)
} else {
refinedBottomPosition = bottomPosition
}
prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition)
}
let contentItem = ChatMessageBubbleContentItem(account: item.account, controllerInteraction: item.controllerInteraction, message: message, read: read, presentationData: item.presentationData, associatedData: item.associatedData)
var itemSelection: Bool?
if case .mosaic = prepareContentPosition {
switch content {
case .message:
break
case let .group(messages):
for (m, _, selection, _) in messages {
if m.id == message.id {
switch selection {
case .none:
break
case let .selectable(selected):
itemSelection = selected
}
break
}
}
}
}
let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(contentItem, layoutConstants, prepareContentPosition, itemSelection, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth)
contentPropertiesAndLayouts.append((unboundSize, properties, prepareContentPosition, nodeLayout))
switch properties.hidesBackground {
case .never:
backgroundHiding = .never
case .emptyWallpaper:
if backgroundHiding == nil {
backgroundHiding = properties.hidesBackground
}
case .always:
backgroundHiding = .always
}
switch properties.forceAlignment {
case .none:
break
case .center:
alignment = .center
}
index += 1
}
var initialDisplayHeader = true
if let backgroundHiding = backgroundHiding, case .always = backgroundHiding {
initialDisplayHeader = false
} else {
if inlineBotNameString == nil && (ignoreForward || firstMessage.forwardInfo == nil) && replyMessage == nil {
if let first = contentPropertiesAndLayouts.first, first.1.hidesSimpleAuthorHeader {
initialDisplayHeader = false
}
}
}
if initialDisplayHeader && displayAuthorInfo {
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
authorNameString = peer.displayTitle
authorNameColor = chatMessagePeerIdColors[Int(peer.id.id % 7)]
} else if let effectiveAuthor = effectiveAuthor {
authorNameString = effectiveAuthor.displayTitle
authorNameColor = chatMessagePeerIdColors[Int(effectiveAuthor.id.id % 7)]
}
if let rawAuthorNameColor = authorNameColor {
var dimColors = false
switch item.presentationData.theme.theme.name {
case .builtin(.nightAccent), .builtin(.nightGrayscale):
dimColors = true
default:
break
}
if dimColors {
var hue: CGFloat = 0.0
var saturation: CGFloat = 0.0
var brightness: CGFloat = 0.0
rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0)
}
}
}
var displayHeader = false
if initialDisplayHeader {
if authorNameString != nil {
displayHeader = true
}
if inlineBotNameString != nil {
displayHeader = true
}
if firstMessage.forwardInfo != nil {
displayHeader = true
}
if replyMessage != nil {
displayHeader = true
}
}
let firstNodeTopPosition: ChatMessageBubbleRelativePosition
if displayHeader {
firstNodeTopPosition = .Neighbour
} else {
firstNodeTopPosition = .None(topNodeMergeStatus)
}
let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus)
var calculatedGroupFramesAndSize: ([(CGRect, MosaicItemPosition)], CGSize)?
var mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)?
if let mosaicRange = mosaicRange {
let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
let (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { $0.0 ?? CGSize(width: 256.0, height: 256.0) })
let framesAndPositions = innerFramesAndPositions.map { ($0.0.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top), $0.1) }
let size = CGSize(width: innerSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: innerSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom)
calculatedGroupFramesAndSize = (framesAndPositions, size)
maximumNodeWidth = size.width
if mosaicRange.upperBound == contentPropertiesAndLayouts.count {
let message = item.content.firstMessage
var edited = false
var sentViaBot = false
var viewCount: Int?
for attribute in message.attributes {
if let _ = attribute as? EditedMessageAttribute {
edited = true
} else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count
} else if let _ = attribute as? InlineBotMessageAttribute {
sentViaBot = true
}
}
if let author = message.author as? TelegramUser, author.botInfo != nil {
sentViaBot = true
}
let dateText = stringForMessageTimestampStatus(message: message, dateTimeFormat: item.presentationData.dateTimeFormat, strings: item.presentationData.strings)
let statusType: ChatMessageDateAndStatusType
if message.effectivelyIncoming(item.account.peerId) {
statusType = .ImageIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .ImageOutgoing(.Failed)
} else if message.flags.isSending && !message.isSentOrAcknowledged {
statusType = .ImageOutgoing(.Sending)
} else {
statusType = .ImageOutgoing(.Sent(read: item.read))
}
}
mosaicStatusSizeAndApply = mosaicStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude))
}
}
var headerSize = CGSize()
var nameNodeOriginY: CGFloat = 0.0
var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil })
var adminNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil })
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 authorNameString != nil || inlineBotNameString != nil {
if headerSize.height.isZero {
headerSize.height += 5.0
}
let inlineBotNameColor = incoming ? item.presentationData.theme.theme.chat.bubble.incomingAccentTextColor : item.presentationData.theme.theme.chat.bubble.outgoingAccentTextColor
let attributedString: NSAttributedString
var adminBadgeString: NSAttributedString?
if authorIsAdmin {
adminBadgeString = NSAttributedString(string: " \(item.presentationData.strings.Conversation_Admin)", font: inlineBotPrefixFont, textColor: incoming ? item.presentationData.theme.theme.chat.bubble.incomingSecondaryTextColor : item.presentationData.theme.theme.chat.bubble.outgoingSecondaryTextColor)
}
if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString {
let mutableString = NSMutableAttributedString(string: "\(authorNameString) ", attributes: [NSAttributedStringKey.font: nameFont, NSAttributedStringKey.foregroundColor: authorNameColor])
let bodyAttributes = MarkdownAttributeSet(font: nameFont, textColor: inlineBotNameColor)
let boldAttributes = MarkdownAttributeSet(font: inlineBotPrefixFont, textColor: inlineBotNameColor)
let botString = addAttributesToStringWithRanges(item.presentationData.strings.Conversation_MessageViaUser("@\(inlineBotNameString)"), body: bodyAttributes, argumentAttributes: [0: boldAttributes])
mutableString.append(botString)
attributedString = mutableString
} else if let authorNameString = authorNameString, let authorNameColor = authorNameColor {
attributedString = NSAttributedString(string: authorNameString, font: nameFont, textColor: authorNameColor)
} else if let inlineBotNameString = inlineBotNameString {
let bodyAttributes = MarkdownAttributeSet(font: inlineBotPrefixFont, textColor: inlineBotNameColor)
let boldAttributes = MarkdownAttributeSet(font: nameFont, textColor: inlineBotNameColor)
attributedString = addAttributesToStringWithRanges(item.presentationData.strings.Conversation_MessageViaUser("@\(inlineBotNameString)"), body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else {
attributedString = NSAttributedString(string: "", font: nameFont, textColor: inlineBotNameColor)
}
let adminBadgeSizeAndApply = adminBadgeLayout(TextNodeLayoutArguments(attributedString: adminBadgeString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
adminNodeSizeApply = (adminBadgeSizeAndApply.0.size, {
return adminBadgeSizeAndApply.1()
})
let sizeAndApply = authorNameLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - adminBadgeSizeAndApply.0.size.width), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
nameNodeSizeApply = (sizeAndApply.0.size, {
return sizeAndApply.1()
})
nameNodeOriginY = headerSize.height
headerSize.width = max(headerSize.width, nameNodeSizeApply.0.width + adminBadgeSizeAndApply.0.size.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right)
headerSize.height += nameNodeSizeApply.0.height
}
if !ignoreForward, let forwardInfo = firstMessage.forwardInfo {
if headerSize.height.isZero {
headerSize.height += 5.0
}
let forwardSource: Peer
let forwardAuthorSignature: String?
if let source = forwardInfo.source {
forwardSource = source
if let authorSignature = forwardInfo.authorSignature {
forwardAuthorSignature = authorSignature
} else if forwardInfo.author.id != source.id {
forwardAuthorSignature = forwardInfo.author.displayTitle
} else {
forwardAuthorSignature = nil
}
} else {
forwardSource = forwardInfo.author
forwardAuthorSignature = nil
}
let sizeAndApply = forwardInfoLayout(item.presentationData.theme, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, 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.isZero {
headerSize.height += 6.0
} else {
headerSize.height += 2.0
}
let sizeAndApply = replyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .bubble(incoming: incoming), replyMessage, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, 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.isZero {
headerSize.height -= 5.0
}
}
let hideBackground: Bool
if let backgroundHiding = backgroundHiding {
switch backgroundHiding {
case .never:
hideBackground = false
case .emptyWallpaper:
hideBackground = hasSolidWallpaper && !displayHeader
case .always:
hideBackground = true
}
} else {
hideBackground = false
}
var removedContentNodeIndices: [Int]?
findRemoved: for i in 0 ..< currentContentClassesPropertiesAndLayouts.count {
let currentMessage = currentContentClassesPropertiesAndLayouts[i].0
let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].1
for (contentNodeMessage, contentNodeClass) in contentNodeMessagesAndClasses {
if currentClass == contentNodeClass && currentMessage.stableId == contentNodeMessage.stableId {
continue findRemoved
}
}
if removedContentNodeIndices == nil {
removedContentNodeIndices = [i]
} else {
removedContentNodeIndices!.append(i)
}
}
var contentNodePropertiesAndFinalize: [(ChatMessageBubbleContentProperties, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))] = []
var maxContentWidth: CGFloat = headerSize.width
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
if let replyMarkup = replyMarkup {
let (minWidth, buttonsLayout) = actionButtonsLayout(item.account, item.presentationData.theme, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth)
maxContentWidth = max(maxContentWidth, minWidth)
actionButtonsFinalize = buttonsLayout
}
for i in 0 ..< contentPropertiesAndLayouts.count {
let (_, contentNodeProperties, preparePosition, contentNodeLayout) = contentPropertiesAndLayouts[i]
if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize {
let mosaicIndex = i - mosaicRange.lowerBound
let position = framesAndPositions[mosaicIndex].1
let topLeft: ChatMessageBubbleContentMosaicNeighbor
let topRight: ChatMessageBubbleContentMosaicNeighbor
let bottomLeft: ChatMessageBubbleContentMosaicNeighbor
let bottomRight: ChatMessageBubbleContentMosaicNeighbor
switch firstNodeTopPosition {
case .Neighbour:
topLeft = .merged
topRight = .merged
case let .None(status):
if position.contains(.top) && position.contains(.left) {
switch status {
case .Left:
topLeft = .merged
case .Right:
topLeft = .none(tail: false)
case .None:
topLeft = .none(tail: false)
}
} else {
topLeft = .merged
}
if position.contains(.top) && position.contains(.right) {
switch status {
case .Left:
topRight = .none(tail: false)
case .Right:
topRight = .merged
case .None:
topRight = .none(tail: false)
}
} else {
topRight = .merged
}
}
let lastMosaicBottomPosition: ChatMessageBubbleRelativePosition
if mosaicRange.upperBound - 1 == contentNodeCount - 1 {
lastMosaicBottomPosition = lastNodeTopPosition
} else {
lastMosaicBottomPosition = .Neighbour
}
if position.contains(.bottom), case .Neighbour = lastMosaicBottomPosition {
bottomLeft = .merged
bottomRight = .merged
} else {
switch lastNodeTopPosition {
case .Neighbour:
bottomLeft = .merged
bottomRight = .merged
case let .None(status):
if position.contains(.bottom) && position.contains(.left) {
switch status {
case .Left:
bottomLeft = .merged
case .Right:
bottomLeft = .none(tail: false)
case let .None(tailStatus):
if case .Incoming = tailStatus {
bottomLeft = .none(tail: true)
} else {
bottomLeft = .none(tail: false)
}
}
} else {
bottomLeft = .merged
}
if position.contains(.bottom) && position.contains(.right) {
switch status {
case .Left:
bottomRight = .none(tail: false)
case .Right:
bottomRight = .merged
case let .None(tailStatus):
if case .Outgoing = tailStatus {
bottomRight = .none(tail: true)
} else {
bottomRight = .none(tail: false)
}
}
} else {
bottomRight = .merged
}
}
}
let (_, contentNodeFinalize) = contentNodeLayout(framesAndPositions[mosaicIndex].0.size, .mosaic(position: ChatMessageBubbleContentMosaicPosition(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)))
contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize))
maxContentWidth = max(maxContentWidth, size.width)
} else {
let contentPosition: ChatMessageBubbleContentPosition
switch preparePosition {
case .linear:
let topPosition: ChatMessageBubbleRelativePosition
let bottomPosition: ChatMessageBubbleRelativePosition
if i == 0 {
topPosition = firstNodeTopPosition
} else {
topPosition = .Neighbour
}
if i == contentNodeCount - 1 {
bottomPosition = lastNodeTopPosition
} else {
bottomPosition = .Neighbour
}
contentPosition = .linear(top: topPosition, bottom: bottomPosition)
case .mosaic:
assertionFailure()
contentPosition = .linear(top: .Neighbour, bottom: .Neighbour)
}
let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), contentPosition)
#if DEBUG
if contentNodeWidth > maximumNodeWidth {
print("contentNodeWidth \(contentNodeWidth) > \(maximumNodeWidth)")
}
#endif
maxContentWidth = max(maxContentWidth, contentNodeWidth)
contentNodePropertiesAndFinalize.append((contentNodeProperties, contentNodeFinalize))
}
}
var contentSize = CGSize(width: maxContentWidth, height: 0.0)
var contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, (ListViewItemUpdateAnimation, Bool) -> Void)] = []
var contentNodesHeight: CGFloat = 0.0
var mosaicStatusOrigin: CGPoint?
for i in 0 ..< contentNodePropertiesAndFinalize.count {
let (properties, finalize) = contentNodePropertiesAndFinalize[i]
if let mosaicRange = mosaicRange, mosaicRange.contains(i), let (framesAndPositions, size) = calculatedGroupFramesAndSize {
let mosaicIndex = i - mosaicRange.lowerBound
if mosaicIndex == 0 {
if !headerSize.height.isZero {
contentNodesHeight += 7.0
}
}
let (_, apply) = finalize(maxContentWidth)
let contentNodeFrame = framesAndPositions[mosaicIndex].0.offsetBy(dx: 0.0, dy: contentNodesHeight)
contentNodeFramesPropertiesAndApply.append((contentNodeFrame, properties, apply))
if mosaicIndex == mosaicRange.upperBound - 1 {
contentNodesHeight += size.height
mosaicStatusOrigin = contentNodeFrame.bottomRight
}
} else {
if i == 0 && !headerSize.height.isZero {
contentNodesHeight += properties.headerSpacing
}
let (size, apply) = finalize(maxContentWidth)
contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodesHeight), size: size), properties, apply))
contentNodesHeight += size.height
}
}
contentSize.height += contentNodesHeight
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
if let actionButtonsFinalize = actionButtonsFinalize {
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
}
let minimalContentSize: CGSize
if hideBackground {
minimalContentSize = CGSize(width: 1.0, height: 1.0)
} else {
minimalContentSize = layoutConstants.bubble.minimumSize
}
let calculatedBubbleHeight = headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom
let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(minimalContentSize.height, calculatedBubbleHeight))
var contentVerticalOffset: CGFloat = 0.0
if minimalContentSize.height > calculatedBubbleHeight + 2.0 {
contentVerticalOffset = floorToScreenPixels((minimalContentSize.height - calculatedBubbleHeight) / 2.0)
}
var deliveryFailedInset: CGFloat = 0.0
if item.content.firstMessage.flags.contains(.Failed) {
deliveryFailedInset += 24.0
}
let backgroundFrame: CGRect
let contentOrigin: CGPoint
let contentUpperRightCorner: CGPoint
switch alignment {
case .none:
backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset - deliveryFailedInset), y: 0.0), size: layoutBubbleSize)
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 + contentVerticalOffset)
contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height)
case .center:
let availableWidth = params.width - params.leftInset - params.rightInset
backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: 0.0), size: layoutBubbleSize)
contentOrigin = CGPoint(x: backgroundFrame.minX + floor(layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left) / 2.0, y: backgroundFrame.minY + layoutConstants.bubble.contentInsets.top + headerSize.height + contentVerticalOffset)
contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height)
}
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height)
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
layoutSize.height += actionButtonsSizeAndApply.0.height
}
var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0)
if dateHeaderAtBottom {
layoutInsets.top += layoutConstants.timestampHeaderHeight
}
var updatedShareButtonBackground: UIImage?
var updatedShareButtonNode: HighlightableButtonNode?
if needShareButton {
if currentShareButtonNode != nil {
updatedShareButtonNode = currentShareButtonNode
if item.presentationData.theme !== currentItem?.presentationData.theme {
let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
if item.message.id.peerId == item.account.peerId {
updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage
} else {
updatedShareButtonBackground = graphics.chatBubbleShareButtonImage
}
}
} else {
let buttonNode = HighlightableButtonNode()
let buttonIcon: UIImage?
let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
if item.message.id.peerId == item.account.peerId {
buttonIcon = graphics.chatBubbleNavigateButtonImage
} else {
buttonIcon = graphics.chatBubbleShareButtonImage
}
buttonNode.setBackgroundImage(buttonIcon, for: [.normal])
updatedShareButtonNode = buttonNode
}
}
let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets)
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
var updatedMergedTop = mergedBottom
var updatedMergedBottom = mergedTop
if mosaicRange == nil {
if contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false {
updatedMergedTop = .semanticallyMerged
}
if headerSize.height.isZero && contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false {
updatedMergedBottom = .none
}
if actionButtonsSizeAndApply != nil {
updatedMergedTop = .fullyMerged
}
}
return (layout, { [weak self] animation, synchronousLoads in
if let strongSelf = self {
strongSelf.appliedItem = item
var transition: ContainedViewLayoutTransition = .immediate
if case let .System(duration) = animation {
transition = .animated(duration: duration, curve: .spring)
}
var forceBackgroundSide = false
if actionButtonsSizeAndApply != nil {
forceBackgroundSide = true
} else if case .semanticallyMerged = updatedMergedTop {
forceBackgroundSide = true
}
let mergeType = ChatMessageBackgroundMergeType(top: updatedMergedTop == .fullyMerged, bottom: updatedMergedBottom == .fullyMerged, side: forceBackgroundSide)
let backgroundType: ChatMessageBackgroundType
if hideBackground {
backgroundType = .none
} else if !incoming {
backgroundType = .outgoing(mergeType)
} else {
backgroundType = .incoming(mergeType)
}
strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, transition: transition)
strongSelf.backgroundType = backgroundType
if item.content.firstMessage.flags.contains(.Failed) {
let deliveryFailedNode: ChatMessageDeliveryFailedNode
var isAppearing = false
if let current = strongSelf.deliveryFailedNode {
deliveryFailedNode = current
} else {
isAppearing = true
deliveryFailedNode = ChatMessageDeliveryFailedNode(tapped: {
if let item = self?.item {
item.controllerInteraction.requestRedeliveryOfFailedMessages(item.content.firstMessage.id)
}
})
strongSelf.deliveryFailedNode = deliveryFailedNode
strongSelf.addSubnode(deliveryFailedNode)
}
let deliveryFailedSize = deliveryFailedNode.updateLayout(theme: item.presentationData.theme.theme)
let deliveryFailedFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + deliveryFailedInset - deliveryFailedSize.width, y: backgroundFrame.maxY - deliveryFailedSize.height), size: deliveryFailedSize)
if isAppearing {
deliveryFailedNode.frame = deliveryFailedFrame
transition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0))
} else {
transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedFrame)
}
} else if let deliveryFailedNode = strongSelf.deliveryFailedNode {
strongSelf.deliveryFailedNode = nil
transition.updateAlpha(node: deliveryFailedNode, alpha: 0.0)
transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in
deliveryFailedNode?.removeFromSupernode()
})
}
if let nameNode = nameNodeSizeApply.1() {
strongSelf.nameNode = nameNode
if nameNode.supernode == nil {
if !nameNode.isNodeLoaded {
nameNode.isUserInteractionEnabled = false
}
strongSelf.addSubnode(nameNode)
}
nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0)
if let adminBadgeNode = adminNodeSizeApply.1() {
strongSelf.adminBadgeNode = adminBadgeNode
let adminBadgeFrame = CGRect(origin: CGPoint(x: contentUpperRightCorner.x - layoutConstants.text.bubbleInsets.left - adminNodeSizeApply.0.width, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: adminNodeSizeApply.0)
if adminBadgeNode.supernode == nil {
if !adminBadgeNode.isNodeLoaded {
adminBadgeNode.isUserInteractionEnabled = false
}
strongSelf.addSubnode(adminBadgeNode)
adminBadgeNode.frame = adminBadgeFrame
} else {
let previousAdminBadgeFrame = adminBadgeNode.frame
adminBadgeNode.frame = adminBadgeFrame
transition.animatePositionAdditive(node: adminBadgeNode, offset: CGPoint(x: previousAdminBadgeFrame.maxX - adminBadgeFrame.maxX, y: 0.0))
}
} else {
strongSelf.adminBadgeNode?.removeFromSupernode()
strongSelf.adminBadgeNode = nil
}
} else {
strongSelf.nameNode?.removeFromSupernode()
strongSelf.nameNode = nil
strongSelf.adminBadgeNode?.removeFromSupernode()
strongSelf.adminBadgeNode = 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() {
let node = updatedContentNodes[index]
if animation.isAnimated {
node.animateRemovalFromBubble(0.2, completion: { [weak node] in
node?.removeFromSupernode()
})
} else {
node.removeFromSupernode()
}
let _ = updatedContentNodes.remove(at: index)
}
}
if let addedContentNodes = addedContentNodes {
for (contentNodeMessage, contentNode) in addedContentNodes {
updatedContentNodes.append(contentNode)
strongSelf.addSubnode(contentNode)
contentNode.visibility = strongSelf.visibility
}
}
var sortedContentNodes: [ChatMessageBubbleContentNode] = []
outer: for (message, nodeClass) in contentNodeMessagesAndClasses {
if let addedContentNodes = addedContentNodes {
for (contentNodeMessage, contentNode) in addedContentNodes {
if type(of: contentNode) == nodeClass && contentNodeMessage.stableId == message.stableId {
sortedContentNodes.append(contentNode)
continue outer
}
}
}
for contentNode in updatedContentNodes {
if type(of: contentNode) == nodeClass && contentNode.item?.message.stableId == message.stableId {
sortedContentNodes.append(contentNode)
continue outer
}
}
}
assert(sortedContentNodes.count == updatedContentNodes.count)
strongSelf.contentNodes = sortedContentNodes
}
var contentNodeIndex = 0
for (relativeFrame, _, apply) in contentNodeFramesPropertiesAndApply {
apply(animation, synchronousLoads)
let contentNode = strongSelf.contentNodes[contentNodeIndex]
let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y)
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.1 === 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
}
if let mosaicStatusOrigin = mosaicStatusOrigin, let (size, apply) = mosaicStatusSizeAndApply {
let mosaicStatusNode = apply(false)
if mosaicStatusNode !== strongSelf.mosaicStatusNode {
strongSelf.mosaicStatusNode?.removeFromSupernode()
strongSelf.mosaicStatusNode = mosaicStatusNode
strongSelf.addSubnode(mosaicStatusNode)
}
let absoluteOrigin = mosaicStatusOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y)
mosaicStatusNode.frame = CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size)
} else if let mosaicStatusNode = strongSelf.mosaicStatusNode {
strongSelf.mosaicStatusNode = nil
mosaicStatusNode.removeFromSupernode()
}
if let updatedShareButtonNode = updatedShareButtonNode {
if updatedShareButtonNode !== strongSelf.shareButtonNode {
if let shareButtonNode = strongSelf.shareButtonNode {
shareButtonNode.removeFromSupernode()
}
strongSelf.shareButtonNode = updatedShareButtonNode
strongSelf.addSubnode(updatedShareButtonNode)
updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside)
}
if let updatedShareButtonBackground = updatedShareButtonBackground {
strongSelf.shareButtonNode?.setBackgroundImage(updatedShareButtonBackground, for: [.normal])
}
} else if let shareButtonNode = strongSelf.shareButtonNode {
shareButtonNode.removeFromSupernode()
strongSelf.shareButtonNode = nil
}
if case .System = animation {
if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) {
strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame)
strongSelf.enableTransitionClippingNode()
}
if let shareButtonNode = strongSelf.shareButtonNode {
let currentBackgroundFrame = strongSelf.backgroundNode.frame
shareButtonNode.frame = CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0))
}
} else {
if let _ = strongSelf.backgroundFrameTransition {
strongSelf.animateFrameTransition(1.0, backgroundFrame.size.height)
strongSelf.backgroundFrameTransition = nil
}
strongSelf.backgroundNode.frame = backgroundFrame
if let shareButtonNode = strongSelf.shareButtonNode {
shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0))
}
strongSelf.disableTransitionClippingNode()
}
let offset: CGFloat = params.leftInset + (incoming ? 42.0 : 0.0)
strongSelf.selectionNode?.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: params.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 shareButtonNode = self.shareButtonNode {
shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0))
}
if let transitionClippingNode = self.transitionClippingNode {
var fixedBackgroundFrame = backgroundFrame
fixedBackgroundFrame = fixedBackgroundFrame.insetBy(dx: 0.0, dy: self.backgroundNode.type == ChatMessageBackgroundType.none ? 0.0 : 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 .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.content.firstMessage.author {
let navigate: ChatControllerInteractionNavigateToPeer
if item.content.firstMessage.id.peerId == item.account.peerId {
navigate = .chat(textInputState: nil, messageId: nil)
} else {
navigate = .info
}
item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, navigate, item.message)
}
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 {
var botAddressName: String?
if let peerId = attribute.peerId, let botPeer = item.message.peers[peerId], let addressName = botPeer.addressName {
botAddressName = addressName
} else {
botAddressName = attribute.title
}
if let botAddressName = botAddressName {
item.controllerInteraction.updateInputState { textInputState in
return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " "))
}
item.controllerInteraction.updateInputMode { _ in
return .text
}
}
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 {
item.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 {
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
} else {
item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .info, 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, .ignore:
break
case let .url(url, concealed):
foundTapAction = true
self.item?.controllerInteraction.openUrl(url, concealed, nil)
break loop
case let .peerMention(peerId, _):
foundTapAction = true
self.item?.controllerInteraction.openPeer(peerId, .chat(textInputState: nil, messageId: nil), nil)
break loop
case let .textMention(name):
foundTapAction = true
self.item?.controllerInteraction.openPeerMention(name)
break loop
case let .botCommand(command):
foundTapAction = true
if let item = self.item {
item.controllerInteraction.sendBotCommand(item.message.id, command)
}
break loop
case let .hashtag(peerName, hashtag):
foundTapAction = true
self.item?.controllerInteraction.openHashtag(peerName, hashtag)
break loop
case .instantPage:
foundTapAction = true
if let item = self.item {
item.controllerInteraction.openInstantPage(item.message)
}
break loop
case let .call(peerId):
foundTapAction = true
self.item?.controllerInteraction.callPeer(peerId)
break loop
case .openMessage:
foundTapAction = true
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
break loop
}
}
if !foundTapAction {
self.item?.controllerInteraction.clickThroughMessage()
}
case .longTap, .doubleTap:
if let item = self.item, self.backgroundNode.frame.contains(location) {
var foundTapAction = false
var tapMessage: Message? = item.content.firstMessage
var selectAll = false
loop: for contentNode in self.contentNodes {
if !contentNode.frame.contains(location) {
continue loop
} else if contentNode is ChatMessageTextBubbleContentNode {
selectAll = true
}
tapMessage = contentNode.item?.message
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY))
switch tapAction {
case .none, .ignore:
break
case let .url(url, _):
foundTapAction = true
item.controllerInteraction.longTap(.url(url))
break loop
case let .peerMention(peerId, mention):
foundTapAction = true
item.controllerInteraction.longTap(.peerMention(peerId, mention))
break loop
case let .textMention(name):
foundTapAction = true
item.controllerInteraction.longTap(.mention(name))
break loop
case let .botCommand(command):
foundTapAction = true
item.controllerInteraction.longTap(.command(command))
break loop
case let .hashtag(_, hashtag):
foundTapAction = true
item.controllerInteraction.longTap(.hashtag(hashtag))
break loop
case .instantPage:
break
case .call:
break
case .openMessage:
foundTapAction = false
break
}
}
if !foundTapAction, let tapMessage = tapMessage {
var subFrame = self.backgroundNode.frame
if case .group = item.content {
for contentNode in self.contentNodes {
if contentNode.item?.message.stableId == tapMessage.stableId {
subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0)
break
}
}
}
item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame)
}
}
default:
break
}
}
default:
break
}
}
private func traceSelectionNodes(parent: ASDisplayNode, point: CGPoint) -> ASDisplayNode? {
if let parent = parent as? GridMessageSelectionNode, parent.bounds.contains(point) {
return parent
} else {
if let parentSubnodes = parent.subnodes {
for subnode in parentSubnodes {
let subnodeFrame = subnode.frame
if let result = traceSelectionNodes(parent: subnode, point: point.offsetBy(dx: -subnodeFrame.minX, dy: -subnodeFrame.minY)) {
return result
}
}
}
return nil
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
return shareButtonNode.view
}
if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) {
return self.view
}
if let selectionNode = self.selectionNode {
if let result = self.traceSelectionNodes(parent: self, point: point.offsetBy(dx: -42.0, dy: 0.0)) {
return result.view
}
var selectionNodeFrame = selectionNode.frame
selectionNodeFrame.origin.x -= 42.0
selectionNodeFrame.size.width += 42.0 * 2.0
if selectionNodeFrame.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, () -> UIView?)? {
for contentNode in self.contentNodes {
if let result = contentNode.transitionNode(messageId: id, media: media) {
return result
}
}
return nil
}
override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? {
for contentNode in self.contentNodes {
let frame = contentNode.frame
if let result = contentNode.peekPreviewContent(at: point.offsetBy(dx: -frame.minX, dy: -frame.minY)) {
return result
}
}
return nil
}
override func updateHiddenMedia() {
var hasHiddenMosaicStatus = false
if let item = self.item {
for contentNode in self.contentNodes {
if let contentItem = contentNode.item {
if contentNode.updateHiddenMedia(item.controllerInteraction.hiddenMedia[contentItem.message.id]) {
if let mosaicStatusNode = self.mosaicStatusNode, mosaicStatusNode.frame.intersects(contentNode.frame) {
hasHiddenMosaicStatus = true
}
}
}
}
}
if let mosaicStatusNode = self.mosaicStatusNode {
if mosaicStatusNode.alpha.isZero != hasHiddenMosaicStatus {
if hasHiddenMosaicStatus {
mosaicStatusNode.alpha = 0.0
} else {
mosaicStatusNode.alpha = 1.0
mosaicStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
}
override func updateAutomaticMediaDownloadSettings() {
if let item = self.item {
for contentNode in self.contentNodes {
contentNode.updateAutomaticMediaDownloadSettings(item.controllerInteraction.automaticMediaDownloadSettings)
}
}
}
override func updateSelectionState(animated: Bool) {
guard let item = self.item else {
return
}
var canHaveSelection = true
switch item.content {
case let .message(message, _, _, _):
for media in message.media {
if let action = media as? TelegramMediaAction {
if case .phoneCall = action.action { } else {
canHaveSelection = false
break
}
}
}
default:
break
}
if let selectionState = item.controllerInteraction.selectionState, canHaveSelection {
var selected = false
var incoming = true
switch item.content {
case let .message(message, _, _, _):
selected = selectionState.selectedIds.contains(message.id)
case let .group(messages: messages):
var allSelected = !messages.isEmpty
for (message, _, _, _) in messages {
if !selectionState.selectedIds.contains(message.id) {
allSelected = false
break
}
}
selected = allSelected
}
incoming = item.message.effectivelyIncoming(item.account.peerId)
let offset: CGFloat = incoming ? 42.0 : 0.0
if let selectionNode = self.selectionNode {
selectionNode.updateSelected(selected, animated: animated)
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(theme: item.presentationData.theme.theme, toggle: { [weak self] value in
if let strongSelf = self, let item = strongSelf.item {
switch item.content {
case let .message(message, _, _, _):
item.controllerInteraction.toggleMessagesSelection([message.id], value)
case let .group(messages):
item.controllerInteraction.toggleMessagesSelection(messages.map { $0.0.id }, value)
}
}
})
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) {
super.updateHighlightedState(animated: animated)
guard let item = self.item, let _ = self.backgroundType else {
return
}
var highlighted = false
for contentNode in self.contentNodes {
let _ = contentNode.updateHighlightedState(animated: animated)
}
if let highlightedState = item.controllerInteraction.highlightedState {
for message in item.content {
if highlightedState.messageStableId == message.stableId {
highlighted = true
break
}
}
}
if self.highlightedState != highlighted {
self.highlightedState = highlighted
if let backgroundType = self.backgroundType {
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
if highlighted {
self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, transition: .immediate)
} else {
if let previousContents = self.backgroundNode.layer.contents, animated {
self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate)
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.42)
}
} else {
self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate)
}
}
}
}
}
@objc func shareButtonPressed() {
if let item = self.item {
if item.content.firstMessage.id.peerId == item.account.peerId {
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId)
break
}
}
} else {
item.controllerInteraction.openMessageShareMenu(item.message.id)
}
}
}
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
switch recognizer.state {
case .began:
self.currentSwipeToReplyTranslation = 0.0
if self.swipeToReplyFeedback == nil {
self.swipeToReplyFeedback = HapticFeedback()
self.swipeToReplyFeedback?.prepareImpact()
}
self.item?.controllerInteraction.cancelInteractiveKeyboardGestures()
case .changed:
var translation = recognizer.translation(in: self.view)
translation.x = max(-80.0, min(0.0, translation.x))
var animateReplyNodeIn = false
if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) {
if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item {
self.swipeToReplyFeedback?.impact()
let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.bubble.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: item.presentationData.theme.theme.chat.bubble.shareButtonStrokeColor, foregroundColor: item.presentationData.theme.theme.chat.bubble.shareButtonForegroundColor)
self.swipeToReplyNode = swipeToReplyNode
self.addSubnode(swipeToReplyNode)
animateReplyNodeIn = true
}
}
self.currentSwipeToReplyTranslation = translation.x
var bounds = self.bounds
bounds.origin.x = -translation.x
self.bounds = bounds
if let swipeToReplyNode = self.swipeToReplyNode {
swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0))
if animateReplyNodeIn {
swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
} else {
swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0))
}
}
case .cancelled, .ended:
self.swipeToReplyFeedback = nil
let translation = recognizer.translation(in: self.view)
if case .ended = recognizer.state, translation.x < -45.0 {
if let item = self.item {
item.controllerInteraction.setupReply(item.message.id)
}
}
var bounds = self.bounds
let previousBounds = bounds
bounds.origin.x = 0.0
self.bounds = bounds
self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
if let swipeToReplyNode = self.swipeToReplyNode {
self.swipeToReplyNode = nil
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
swipeToReplyNode?.removeFromSupernode()
})
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
default:
break
}
}
}