Swiftgram/TelegramUI/ChatMessageAttachedContentNode.swift
2017-06-16 12:18:03 +03:00

489 lines
26 KiB
Swift

import Foundation
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
private let titleFont: UIFont = Font.semibold(15.0)
private let textFont: UIFont = Font.regular(15.0)
private let textBoldFont: UIFont = Font.semibold(15.0)
private let textFixedFont: UIFont = Font.regular(15.0)
struct ChatMessageAttachedContentNodeMediaFlags: OptionSet {
var rawValue: Int32
init(rawValue: Int32) {
self.rawValue = rawValue
}
init() {
self.rawValue = 0
}
static let preferMediaInline = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 0)
static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1)
}
final class ChatMessageAttachedContentNode: ASDisplayNode {
private let lineNode: ASImageNode
private let textNode: TextNode
private let inlineImageNode: TransformImageNode
private var contentImageNode: ChatMessageInteractiveMediaNode?
private var contentFileNode: ChatMessageInteractiveFileNode?
private let statusNode: ChatMessageDateAndStatusNode
private var message: Message?
private var media: Media?
var openMedia: (() -> Void)?
var activateAction: (() -> Void)?
var visibility: ListViewItemNodeVisibility = .none {
didSet {
self.contentImageNode?.visibility = self.visibility
}
}
override init() {
self.lineNode = ASImageNode()
self.lineNode.isLayerBacked = true
self.lineNode.displaysAsynchronously = false
self.lineNode.displayWithoutProcessing = true
self.textNode = TextNode()
self.textNode.isLayerBacked = true
self.textNode.displaysAsynchronously = true
self.textNode.contentsScale = UIScreenScale
self.textNode.contentMode = .topLeft
self.inlineImageNode = TransformImageNode()
self.inlineImageNode.isLayerBacked = true
self.inlineImageNode.displaysAsynchronously = false
self.statusNode = ChatMessageDateAndStatusNode()
super.init()
self.addSubnode(self.lineNode)
self.addSubnode(self.textNode)
self.addSubnode(self.statusNode)
}
func asyncLayout() -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ account: Account, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) {
let textAsyncLayout = TextNode.asyncLayout(self.textNode)
let currentImage = self.media as? TelegramMediaImage
let imageLayout = self.inlineImageNode.asyncLayout()
let statusLayout = self.statusNode.asyncLayout()
let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode)
let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode)
return { theme, strings, account, message, messageRead, title, subtitle, text, entities, mediaAndFlags, displayLine, layoutConstants, position, constrainedSize in
let incoming = message.effectivelyIncoming
var insets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 5.0, right: 8.0)
switch position.top {
case .None:
insets.top += 8.0
default:
break
}
if displayLine {
insets.left += 11.0
}
var preferMediaBeforeText = false
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
preferMediaBeforeText = true
}
var t = Int(message.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
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
}
}
var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
if let author = message.author as? TelegramUser {
if author.botInfo != nil {
sentViaBot = true
}
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
dateText = "\(author.displayTitle), \(dateText)"
}
}
var textString: NSAttributedString?
var inlineImageDimensions: CGSize?
var inlineImageSize: CGSize?
var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var textCutout: TextNodeCutout?
var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude
var refineContentImageLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode)))?
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode)))?
let string = NSMutableAttributedString()
var notEmpty = false
let bubbleTheme = theme.chat.bubble
if let title = title, !title.isEmpty {
string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor))
notEmpty = true
}
if let subtitle = subtitle, !subtitle.isEmpty {
if notEmpty {
string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor))
}
string.append(NSAttributedString(string: subtitle, font: titleFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor))
notEmpty = true
}
if let text = text, !text.isEmpty {
if notEmpty {
string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor))
}
if let entities = entities {
string.append(stringWithAppliedEntities(text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: textFont, boldFont: textBoldFont, fixedFont: textFixedFont))
} else {
string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor))
}
notEmpty = true
}
textString = string
if let (media, flags) = mediaAndFlags {
if let file = media as? TelegramMediaFile {
if file.isVideo {
let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants)
initialWidth = initialImageWidth + insets.left + insets.right
refineContentImageLayout = refineLayout
} else {
var automaticDownload = false
if file.isVoice {
automaticDownload = true
}
let (_, refineLayout) = contentFileLayout(account, theme, strings, message, file, automaticDownload,message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height))
refineContentFileLayout = refineLayout
}
} else if let image = media as? TelegramMediaImage {
if !flags.contains(.preferMediaInline) {
let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants)
initialWidth = initialImageWidth + insets.left + insets.right
refineContentImageLayout = refineLayout
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
inlineImageDimensions = dimensions
if image != currentImage {
updateInlineImageSignal = chatWebpageSnippetPhoto(account: account, photo: image)
}
}
} else if let image = media as? TelegramMediaWebFile {
let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants)
initialWidth = initialImageWidth + insets.left + insets.right
refineContentImageLayout = refineLayout
}
}
if let _ = inlineImageDimensions {
inlineImageSize = CGSize(width: 54.0, height: 54.0)
if let inlineImageSize = inlineImageSize {
textCutout = TextNodeCutout(position: .TopRight, size: CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0))
}
}
return (initialWidth, { constrainedSize in
let statusType: ChatMessageDateAndStatusType
if message.effectivelyIncoming {
statusType = .BubbleIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if message.flags.isSending {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: messageRead))
}
}
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
var statusSizeAndApply: (CGSize, (Bool) -> Void)?
if (refineContentImageLayout == nil && refineContentFileLayout == nil) || preferMediaBeforeText {
statusSizeAndApply = statusLayout(theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize)
}
let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout, UIEdgeInsets())
var textFrame = CGRect(origin: CGPoint(), size: textLayout.size)
var statusFrame: CGRect?
if let (statusSize, _) = statusSizeAndApply {
var frame = CGRect(origin: CGPoint(), size: statusSize)
let trailingLineWidth = textLayout.trailingLineWidth
if textLayout.size.width - trailingLineWidth >= statusSize.width {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height)
} else if trailingLineWidth + statusSize.width < textConstrainedSize.width {
frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height)
} else {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY)
}
if let inlineImageSize = inlineImageSize {
if frame.origin.y < inlineImageSize.height + 4.0 {
frame.origin.y = inlineImageSize.height + 4.0
}
}
frame = frame.offsetBy(dx: insets.left, dy: insets.top)
statusFrame = frame
}
textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top)
let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(theme)
var boundingSize = textFrame.size
if let statusFrame = statusFrame {
boundingSize = textFrame.union(statusFrame).size
}
var lineHeight = textFrame.size.height
if let inlineImageSize = inlineImageSize {
if boundingSize.height < inlineImageSize.height {
boundingSize.height = inlineImageSize.height
}
if lineHeight < inlineImageSize.height {
lineHeight = inlineImageSize.height
}
}
var finalizeContentImageLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))?
if let refineContentImageLayout = refineContentImageLayout {
let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize)
finalizeContentImageLayout = finalizeImageLayout
boundingSize.width = max(boundingSize.width, refinedWidth)
}
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))?
if let refineContentFileLayout = refineContentFileLayout {
let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize)
finalizeContentFileLayout = finalizeFileLayout
boundingSize.width = max(boundingSize.width, refinedWidth)
}
boundingSize.width += insets.left + insets.right
boundingSize.height += insets.top + insets.bottom
lineHeight += insets.top + insets.bottom
var imageApply: (() -> Void)?
if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions {
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets())
imageApply = imageLayout(arguments)
}
return (boundingSize.width, { boundingWidth in
var adjustedBoundingSize = boundingSize
var adjustedLineHeight = lineHeight
var imageFrame: CGRect?
if let inlineImageSize = inlineImageSize {
imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize)
}
var contentImageSizeAndApply: (CGSize, () -> ChatMessageInteractiveMediaNode)?
if let finalizeContentImageLayout = finalizeContentImageLayout {
let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right)
contentImageSizeAndApply = (size, apply)
var imageHeigthAddition = size.height
if textFrame.size.height > CGFloat.ulpOfOne {
imageHeigthAddition += 2.0
}
adjustedBoundingSize.height += imageHeigthAddition + 5.0
adjustedLineHeight += imageHeigthAddition + 4.0
}
var contentFileSizeAndApply: (CGSize, () -> ChatMessageInteractiveFileNode)?
if let finalizeContentFileLayout = finalizeContentFileLayout {
let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right)
contentFileSizeAndApply = (size, apply)
var imageHeigthAddition = size.height
if textFrame.size.height > CGFloat.ulpOfOne {
imageHeigthAddition += 2.0
}
adjustedBoundingSize.height += imageHeigthAddition + 5.0
adjustedLineHeight += imageHeigthAddition + 4.0
}
/*if let _ = webPageContent?.instantPage {
adjustedBoundingSize.height += 4.0
}*/
var adjustedStatusFrame: CGRect?
if let statusFrame = statusFrame {
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size)
}
return (adjustedBoundingSize, { [weak self] animation in
if let strongSelf = self {
strongSelf.message = message
strongSelf.media = mediaAndFlags?.0
var hasAnimation = true
if case .None = animation {
hasAnimation = false
}
strongSelf.lineNode.image = lineImage
strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 0.0), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0))
strongSelf.lineNode.isHidden = !displayLine
let _ = textApply()
if let imageFrame = imageFrame {
if let updateImageSignal = updateInlineImageSignal {
strongSelf.inlineImageNode.setSignal(account: account, signal: updateImageSignal)
}
strongSelf.inlineImageNode.frame = imageFrame
if strongSelf.inlineImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.inlineImageNode)
}
if let imageApply = imageApply {
imageApply()
}
} else if strongSelf.inlineImageNode.supernode != nil {
strongSelf.inlineImageNode.removeFromSupernode()
}
var contentMediaHeight: CGFloat?
if let (contentImageSize, contentImageApply) = contentImageSizeAndApply {
contentMediaHeight = contentImageSize.height
let contentImageNode = contentImageApply()
if strongSelf.contentImageNode !== contentImageNode {
strongSelf.contentImageNode = contentImageNode
strongSelf.addSubnode(contentImageNode)
contentImageNode.activateLocalContent = { [weak strongSelf] in
if let strongSelf = strongSelf {
strongSelf.openMedia?()
}
}
contentImageNode.visibility = strongSelf.visibility
}
let _ = contentImageApply()
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize)
} else {
contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize)
}
} else if let contentImageNode = strongSelf.contentImageNode {
contentImageNode.visibility = .none
contentImageNode.removeFromSupernode()
strongSelf.contentImageNode = nil
}
if let (contentFileSize, contentFileApply) = contentFileSizeAndApply {
contentMediaHeight = contentFileSize.height
let contentFileNode = contentFileApply()
if strongSelf.contentFileNode !== contentFileNode {
strongSelf.contentFileNode = contentFileNode
strongSelf.addSubnode(contentFileNode)
contentFileNode.activateLocalContent = { [weak strongSelf] in
if let strongSelf = strongSelf {
strongSelf.openMedia?()
}
}
}
let _ = contentFileApply()
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize)
} else {
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentFileSize)
}
} else if let contentFileNode = strongSelf.contentFileNode {
contentFileNode.removeFromSupernode()
strongSelf.contentFileNode = nil
}
var textVerticalOffset: CGFloat = 0.0
if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
textVerticalOffset = contentMediaHeight + 7.0
}
strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset)
if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame {
strongSelf.statusNode.frame = adjustedStatusFrame.offsetBy(dx: 0.0, dy: textVerticalOffset)
if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode)
}
statusApply(hasAnimation)
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
}
})
})
})
}
}
func updateHiddenMedia(_ media: [Media]?) {
if let currentMedia = self.media {
if let media = media {
var found = false
for m in media {
if currentMedia.isEqual(m) {
found = true
break
}
}
if let contentImageNode = self.contentImageNode {
contentImageNode.isHidden = found
}
} else if let contentImageNode = self.contentImageNode {
contentImageNode.isHidden = false
}
}
}
func transitionNode(media: Media) -> ASDisplayNode? {
if let image = self.media as? TelegramMediaImage, image.isEqual(media) {
return self.contentImageNode
} else if let file = self.media as? TelegramMediaFile, file.isEqual(media) {
return self.contentImageNode
}
return nil
}
}