mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
296 lines
15 KiB
Swift
296 lines
15 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
private let titleFont = Font.medium(14.0)
|
|
private let textFont = Font.regular(14.0)
|
|
|
|
class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
|
override var properties: ChatMessageBubbleContentProperties {
|
|
return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0)
|
|
}
|
|
|
|
private let imageNode: TransformImageNode
|
|
private let dateAndStatusNode: ChatMessageDateAndStatusNode
|
|
private let titleNode: TextNode
|
|
private let textNode: TextNode
|
|
|
|
private var item: ChatMessageItem?
|
|
private var media: TelegramMediaMap?
|
|
|
|
required init() {
|
|
self.imageNode = TransformImageNode()
|
|
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
|
self.titleNode = TextNode()
|
|
self.textNode = TextNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.imageNode)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))
|
|
self.view.addGestureRecognizer(tapRecognizer)
|
|
}
|
|
|
|
override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) {
|
|
let makeImageLayout = self.imageNode.asyncLayout()
|
|
let statusLayout = self.dateAndStatusNode.asyncLayout()
|
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
|
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
|
|
|
let previousMedia = self.media
|
|
|
|
return { item, layoutConstants, position, constrainedSize in
|
|
var selectedMedia: TelegramMediaMap?
|
|
for media in item.message.media {
|
|
if let telegramImage = media as? TelegramMediaMap {
|
|
selectedMedia = telegramImage
|
|
}
|
|
}
|
|
|
|
let imageCorners: ImageCorners
|
|
|
|
var titleString: NSAttributedString?
|
|
var textString: NSAttributedString?
|
|
|
|
let imageSize: CGSize
|
|
if let venue = selectedMedia?.venue {
|
|
imageCorners = ImageCorners(radius: 14.0)
|
|
imageSize = CGSize(width: 75.0, height: 75.0)
|
|
titleString = NSAttributedString(string: venue.title, font: titleFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingPrimaryTextColor : item.theme.chat.bubble.outgoingPrimaryTextColor)
|
|
if let address = venue.address, !address.isEmpty {
|
|
textString = NSAttributedString(string: address, font: textFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingAccentColor : item.theme.chat.bubble.outgoingAccentColor)
|
|
}
|
|
} else {
|
|
imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius)
|
|
imageSize = CGSize(width: 160.0, height: 100.0)
|
|
}
|
|
|
|
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
|
if let selectedMedia = selectedMedia, previousMedia == nil || !previousMedia!.isEqual(selectedMedia) {
|
|
updateImageSignal = chatMapSnapshotImage(account: item.account, resource: MapSnapshotMediaResource(latitude: selectedMedia.latitude, longitude: selectedMedia.longitude, width: 160, height: 100))
|
|
}
|
|
|
|
let maximumWidth: CGFloat
|
|
if let _ = titleString {
|
|
maximumWidth = CGFloat.greatestFiniteMagnitude
|
|
} else {
|
|
maximumWidth = imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right
|
|
}
|
|
|
|
return (maximumWidth, { constrainedSize in
|
|
let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: max(1.0, constrainedSize.width - imageSize.width - layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets())
|
|
let (textLayout, textApply) = makeTextLayout(textString, nil, 2, .end, CGSize(width: max(1.0, constrainedSize.width - imageSize.width - layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets())
|
|
|
|
var t = Int(item.message.timestamp)
|
|
var timeinfo = tm()
|
|
localtime_r(&t, &timeinfo)
|
|
|
|
var edited = false
|
|
var sentViaBot = false
|
|
var viewCount: Int?
|
|
for attribute in item.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
|
|
}
|
|
}
|
|
|
|
var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
|
|
|
if let author = item.message.author as? TelegramUser {
|
|
if author.botInfo != nil {
|
|
sentViaBot = true
|
|
}
|
|
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
|
dateText = "\(author.displayTitle), \(dateText)"
|
|
}
|
|
}
|
|
|
|
let statusType: ChatMessageDateAndStatusType?
|
|
if case .None = position.bottom {
|
|
if let _ = titleString {
|
|
if item.message.effectivelyIncoming {
|
|
statusType = .BubbleIncoming
|
|
} else {
|
|
if item.message.flags.contains(.Failed) {
|
|
statusType = .BubbleOutgoing(.Failed)
|
|
} else if item.message.flags.isSending {
|
|
statusType = .BubbleOutgoing(.Sending)
|
|
} else {
|
|
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
|
}
|
|
}
|
|
} else {
|
|
if item.message.effectivelyIncoming {
|
|
statusType = .ImageIncoming
|
|
} else {
|
|
if item.message.flags.contains(.Failed) {
|
|
statusType = .ImageOutgoing(.Failed)
|
|
} else if item.message.flags.isSending {
|
|
statusType = .ImageOutgoing(.Sending)
|
|
} else {
|
|
statusType = .ImageOutgoing(.Sent(read: item.read))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
statusType = nil
|
|
}
|
|
|
|
var statusSize = CGSize()
|
|
var statusApply: ((Bool) -> Void)?
|
|
|
|
if let statusType = statusType {
|
|
let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude))
|
|
statusSize = size
|
|
statusApply = apply
|
|
}
|
|
|
|
let contentWidth: CGFloat
|
|
if let _ = titleString {
|
|
contentWidth = imageSize.width + max(statusSize.width, max(titleLayout.size.width, textLayout.size.width)) + layoutConstants.text.bubbleInsets.right + 8.0
|
|
|
|
} else {
|
|
contentWidth = imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right
|
|
}
|
|
|
|
return (contentWidth, { boundingWidth in
|
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())
|
|
|
|
let imageLayoutSize = CGSize(width: imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: imageSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom)
|
|
|
|
let layoutSize: CGSize
|
|
let statusFrame: CGRect
|
|
|
|
let baseImageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize)
|
|
|
|
let imageFrame: CGRect
|
|
|
|
if let _ = titleString {
|
|
layoutSize = CGSize(width: contentWidth, height: imageLayoutSize.height + 10.0)
|
|
statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0 - 4.0), size: statusSize)
|
|
imageFrame = baseImageFrame.offsetBy(dx: 5.0, dy: 5.0)
|
|
} else {
|
|
layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height)
|
|
statusFrame = CGRect(origin: CGPoint(x: layoutSize.width - layoutConstants.image.bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - layoutConstants.image.bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
|
|
imageFrame = baseImageFrame.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top)
|
|
}
|
|
|
|
let imageApply = makeImageLayout(arguments)
|
|
|
|
return (layoutSize, { [weak self] animation in
|
|
if let strongSelf = self {
|
|
strongSelf.item = item
|
|
strongSelf.media = selectedMedia
|
|
|
|
strongSelf.imageNode.frame = imageFrame
|
|
|
|
let _ = titleApply()
|
|
let _ = textApply()
|
|
|
|
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 1.0), size: titleLayout.size)
|
|
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 19.0), size: textLayout.size)
|
|
|
|
if let statusApply = statusApply {
|
|
if strongSelf.dateAndStatusNode.supernode == nil {
|
|
strongSelf.imageNode.addSubnode(strongSelf.dateAndStatusNode)
|
|
}
|
|
var hasAnimation = true
|
|
if case .None = animation {
|
|
hasAnimation = false
|
|
}
|
|
statusApply(hasAnimation)
|
|
strongSelf.dateAndStatusNode.frame = statusFrame
|
|
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
|
strongSelf.dateAndStatusNode.removeFromSupernode()
|
|
}
|
|
|
|
if let _ = titleString {
|
|
if strongSelf.titleNode.supernode == nil {
|
|
strongSelf.addSubnode(strongSelf.titleNode)
|
|
}
|
|
if strongSelf.textNode.supernode == nil {
|
|
strongSelf.addSubnode(strongSelf.textNode)
|
|
}
|
|
} else {
|
|
if strongSelf.titleNode.supernode != nil {
|
|
strongSelf.titleNode.removeFromSupernode()
|
|
}
|
|
if strongSelf.textNode.supernode != nil {
|
|
strongSelf.textNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let updateImageSignal = updateImageSignal {
|
|
strongSelf.imageNode.setSignal(account: item.account, signal: updateImageSignal)
|
|
}
|
|
|
|
imageApply()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
override func transitionNode(media: Media) -> ASDisplayNode? {
|
|
if let currentMedia = self.media, currentMedia.isEqual(media) {
|
|
return self.imageNode
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override func updateHiddenMedia(_ media: [Media]?) {
|
|
var mediaHidden = false
|
|
if let currentMedia = self.media, let media = media {
|
|
for item in media {
|
|
if item.isEqual(currentMedia) {
|
|
mediaHidden = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
self.imageNode.isHidden = mediaHidden
|
|
}
|
|
|
|
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
|
|
return .none
|
|
}
|
|
|
|
@objc func imageTap(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if let item = self.item {
|
|
self.controllerInteraction?.openMessage(item.message.id)
|
|
}
|
|
}
|
|
}
|
|
}
|