Swiftgram/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift
2021-12-23 16:53:22 +04:00

388 lines
22 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import LocalizedPeerData
import PhotoResources
import TelegramStringFormatting
import TextFormat
import InvisibleInkDustNode
enum ChatMessageReplyInfoType {
case bubble(incoming: Bool)
case standalone
}
class ChatMessageReplyInfoNode: ASDisplayNode {
private let contentNode: ASDisplayNode
private let lineNode: ASImageNode
private var titleNode: TextNode?
private var textNode: TextNode?
private var dustNode: InvisibleInkDustNode?
private var imageNode: TransformImageNode?
private var previousMediaReference: AnyMediaReference?
override init() {
self.contentNode = ASDisplayNode()
self.contentNode.isUserInteractionEnabled = false
self.contentNode.displaysAsynchronously = false
self.contentNode.contentMode = .left
self.contentNode.contentsScale = UIScreenScale
self.lineNode = ASImageNode()
self.lineNode.displaysAsynchronously = false
self.lineNode.displayWithoutProcessing = true
self.lineNode.isLayerBacked = true
super.init()
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.lineNode)
}
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: ChatPresentationData, _ strings: PresentationStrings, _ context: AccountContext, _ type: ChatMessageReplyInfoType, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) {
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode)
let previousMediaReference = maybeNode?.previousMediaReference
return { presentationData, strings, context, type, message, constrainedSize in
let fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let titleFont = Font.medium(fontSize)
let textFont = Font.regular(fontSize)
var titleString = message.effectiveAuthor.flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) ?? strings.User_DeletedAccount
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) {
if let author = forwardInfo.author {
titleString = EnginePeer(author).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)
} else if let authorSignature = forwardInfo.authorSignature {
titleString = authorSignature
}
}
let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId)
let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
let titleColor: UIColor
let lineImage: UIImage?
let textColor: UIColor
let dustColor: UIColor
switch type {
case let .bubble(incoming):
titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor
lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(presentationData.theme.theme)
if isMedia {
textColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
} else {
textColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor
}
dustColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
case .standalone:
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
titleColor = serviceColor.primaryText
let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners)
lineImage = graphics.chatServiceVerticalLineImage
textColor = titleColor
dustColor = titleColor
}
let messageText: NSAttributedString
if isText {
let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in
if case .Spoiler = entity.type {
return true
} else {
return false
}
}
if entities.count > 0 {
messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
} else {
messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor)
}
} else {
messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor)
}
var leftInset: CGFloat = 11.0
let spacing: CGFloat = 2.0
var updatedMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
var hasRoundImage = false
if !message.containsSecretMedia {
for media in message.media {
if let image = media as? TelegramMediaImage {
updatedMediaReference = .message(message: MessageReference(message), media: image)
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
break
} else if let file = media as? TelegramMediaFile, file.isVideo {
updatedMediaReference = .message(message: MessageReference(message), media: file)
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
} else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker {
imageDimensions = representation.dimensions.cgSize
}
if file.isInstantVideo {
hasRoundImage = true
}
break
}
}
}
var imageTextInset: CGFloat = 0.0
if let _ = imageDimensions {
imageTextInset += floor(presentationData.fontSize.baseDisplaySize * 32.0 / 17.0)
}
let maximumTextWidth = max(0.0, constrainedSize.width - imageTextInset)
let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height)
let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets))
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets))
let imageSide = titleLayout.size.height + textLayout.size.height - 16.0
var applyImage: (() -> TransformImageNode)?
if let imageDimensions = imageDimensions {
let boundingSize = CGSize(width: imageSide, height: imageSide)
leftInset += imageSide + 2.0
var radius: CGFloat = 2.0
var imageSize = imageDimensions.aspectFilled(boundingSize)
if hasRoundImage {
radius = boundingSize.width / 2.0
imageSize.width += 2.0
imageSize.height += 2.0
}
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: placeholderColor))
}
var mediaUpdated = false
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference {
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
} else if (updatedMediaReference != nil) != (previousMediaReference != nil) {
mediaUpdated = true
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
if let updatedMediaReference = updatedMediaReference, mediaUpdated && imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, photoReference: imageReference)
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
if fileReference.media.isVideo {
updateImageSignal = chatMessageVideoThumbnail(account: context.account, fileReference: fileReference)
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
updateImageSignal = chatWebpageSnippetFile(account: context.account, fileReference: fileReference, representation: iconImageRepresentation)
}
}
}
let size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing)
return (size, {
let node: ChatMessageReplyInfoNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageReplyInfoNode()
}
node.previousMediaReference = updatedMediaReference
node.titleNode?.displaysAsynchronously = !presentationData.isPreview
node.textNode?.displaysAsynchronously = !presentationData.isPreview
let titleNode = titleApply()
let textNode = textApply()
if node.titleNode == nil {
titleNode.isUserInteractionEnabled = false
node.titleNode = titleNode
node.contentNode.addSubnode(titleNode)
}
if node.textNode == nil {
textNode.isUserInteractionEnabled = false
node.textNode = textNode
node.contentNode.addSubnode(textNode)
}
if let applyImage = applyImage {
let imageNode = applyImage()
if node.imageNode == nil {
imageNode.isLayerBacked = !smartInvertColorsEnabled()
node.addSubnode(imageNode)
node.imageNode = imageNode
}
imageNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 4.0 + UIScreenPixel), size: CGSize(width: imageSide, height: imageSide))
if let updateImageSignal = updateImageSignal {
imageNode.setSignal(updateImageSignal)
}
} else if let imageNode = node.imageNode {
imageNode.removeFromSupernode()
node.imageNode = nil
}
node.imageNode?.captureProtected = message.isCopyProtected()
titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: spacing - textInsets.top), size: titleLayout.size)
let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top), size: textLayout.size)
textNode.frame = textFrame
if !textLayout.spoilers.isEmpty {
let dustNode: InvisibleInkDustNode
if let current = node.dustNode {
dustNode = current
} else {
dustNode = InvisibleInkDustNode(textNode: nil)
dustNode.isUserInteractionEnabled = false
node.dustNode = dustNode
node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode)
}
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
dustNode.update(size: dustNode.frame.size, color: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let dustNode = node.dustNode {
dustNode.removeFromSupernode()
node.dustNode = nil
}
node.lineNode.image = lineImage
node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 5.0)))
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
return node
})
}
}
func animateFromInputPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint {
let sourceParentNode = ASDisplayNode()
let sourceParentOffset: CGPoint
if let unclippedTransitionNode = unclippedTransitionNode {
unclippedTransitionNode.addSubnode(sourceParentNode)
sourceParentNode.frame = sourceReplyPanel.relativeSourceRect
sourceParentOffset = self.view.convert(CGPoint(), to: sourceParentNode.view)
sourceParentNode.clipsToBounds = true
let panelOffset = sourceReplyPanel.relativeTargetRect.minY - sourceReplyPanel.relativeSourceRect.minY
sourceParentNode.frame = sourceParentNode.frame.offsetBy(dx: 0.0, dy: panelOffset)
sourceParentNode.bounds = sourceParentNode.bounds.offsetBy(dx: 0.0, dy: panelOffset)
transition.vertical.animatePositionAdditive(layer: sourceParentNode.layer, offset: CGPoint(x: 0.0, y: -panelOffset))
transition.vertical.animateOffsetAdditive(layer: sourceParentNode.layer, offset: -panelOffset)
} else {
self.addSubnode(sourceParentNode)
sourceParentOffset = CGPoint()
}
sourceParentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak sourceParentNode] _ in
sourceParentNode?.removeFromSupernode()
})
if let titleNode = self.titleNode {
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.titleNode.frame.minX - titleNode.frame.minX,
y: localRect.minY + sourceReplyPanel.titleNode.frame.midY - titleNode.frame.midY
)
transition.horizontal.animatePositionAdditive(node: titleNode, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(node: titleNode, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.titleNode)
titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
sourceReplyPanel.titleNode.frame = sourceReplyPanel.titleNode.frame
.offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y)
.offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y)
transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.titleNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false)
transition.vertical.animatePositionAdditive(node: sourceReplyPanel.titleNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false)
}
if let textNode = self.textNode {
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.textNode.frame.minX - textNode.frame.minX,
y: localRect.minY + sourceReplyPanel.textNode.frame.midY - textNode.frame.midY
)
transition.horizontal.animatePositionAdditive(node: textNode, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(node: textNode, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.textNode)
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
sourceReplyPanel.textNode.frame = sourceReplyPanel.textNode.frame
.offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y)
.offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y)
transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.textNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false)
transition.vertical.animatePositionAdditive(node: sourceReplyPanel.textNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false)
}
if let imageNode = self.imageNode {
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.imageNode.frame.midX - imageNode.frame.midX,
y: localRect.minY + sourceReplyPanel.imageNode.frame.midY - imageNode.frame.midY
)
transition.horizontal.animatePositionAdditive(node: imageNode, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(node: imageNode, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.imageNode)
imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
sourceReplyPanel.imageNode.frame = sourceReplyPanel.imageNode.frame
.offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y)
.offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y)
transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.imageNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false)
transition.vertical.animatePositionAdditive(node: sourceReplyPanel.imageNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false)
}
do {
let lineNode = self.lineNode
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - lineNode.frame.minX,
y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - lineNode.frame.minY
)
transition.horizontal.animatePositionAdditive(node: lineNode, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(node: lineNode, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.lineNode)
lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
sourceReplyPanel.lineNode.frame = sourceReplyPanel.lineNode.frame
.offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y)
.offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y)
transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.lineNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false)
transition.vertical.animatePositionAdditive(node: sourceReplyPanel.lineNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false)
return offset
}
}
}