2023-10-20 21:51:45 +04:00

930 lines
50 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
import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import ChatMessageItemCommon
import MessageInlineBlockBackgroundView
public enum ChatMessageReplyInfoType {
case bubble(incoming: Bool)
case standalone
}
private let quoteIcon: UIImage = {
return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed().withRenderingMode(.alwaysTemplate)
}()
private let channelIcon: UIImage = {
let sourceImage = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")!
return generateImage(CGSize(width: sourceImage.size.width + 4.0, height: sourceImage.size.height + 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
sourceImage.draw(at: CGPoint(x: 2.0, y: 1.0 + UIScreenPixel))
UIGraphicsPopContext()
})!.precomposed().withRenderingMode(.alwaysTemplate)
}()
private let groupIcon: UIImage = {
let sourceImage = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")!
return generateImage(CGSize(width: sourceImage.size.width + 3.0, height: sourceImage.size.height + 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
sourceImage.draw(at: CGPoint(x: 3.0, y: 1.0 - UIScreenPixel))
UIGraphicsPopContext()
})!.precomposed().withRenderingMode(.alwaysTemplate)
}()
public class ChatMessageReplyInfoNode: ASDisplayNode {
public final class TransitionReplyPanel {
public let titleNode: ASDisplayNode
public let textNode: ASDisplayNode
public let lineNode: ASDisplayNode
public let imageNode: ASDisplayNode
public let relativeSourceRect: CGRect
public let relativeTargetRect: CGRect
public init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) {
self.titleNode = titleNode
self.textNode = textNode
self.lineNode = lineNode
self.imageNode = imageNode
self.relativeSourceRect = relativeSourceRect
self.relativeTargetRect = relativeTargetRect
}
}
public class Arguments {
public let presentationData: ChatPresentationData
public let strings: PresentationStrings
public let context: AccountContext
public let type: ChatMessageReplyInfoType
public let message: Message?
public let replyForward: QuotedReplyMessageAttribute?
public let quote: EngineMessageReplyQuote?
public let story: StoryId?
public let parentMessage: Message
public let constrainedSize: CGSize
public let animationCache: AnimationCache?
public let animationRenderer: MultiAnimationRenderer?
public let associatedData: ChatMessageItemAssociatedData
public init(
presentationData: ChatPresentationData,
strings: PresentationStrings,
context: AccountContext,
type: ChatMessageReplyInfoType,
message: Message?,
replyForward: QuotedReplyMessageAttribute?,
quote: EngineMessageReplyQuote?,
story: StoryId?,
parentMessage: Message,
constrainedSize: CGSize,
animationCache: AnimationCache?,
animationRenderer: MultiAnimationRenderer?,
associatedData: ChatMessageItemAssociatedData
) {
self.presentationData = presentationData
self.strings = strings
self.context = context
self.type = type
self.message = message
self.replyForward = replyForward
self.quote = quote
self.story = story
self.parentMessage = parentMessage
self.constrainedSize = constrainedSize
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.associatedData = associatedData
}
}
public var visibility: Bool = false {
didSet {
if self.visibility != oldValue {
self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil
}
}
}
private let backgroundView: MessageInlineBlockBackgroundView
private var quoteIconView: UIImageView?
private let contentNode: ASDisplayNode
private var titleNode: TextNode?
private var textNode: TextNodeWithEntities?
private var dustNode: InvisibleInkDustNode?
private var imageNode: TransformImageNode?
private var previousMediaReference: AnyMediaReference?
private var expiredStoryIconView: UIImageView?
private var currentProgressDisposable: Disposable?
override public init() {
self.backgroundView = MessageInlineBlockBackgroundView(frame: CGRect())
self.contentNode = ASDisplayNode()
self.contentNode.isUserInteractionEnabled = false
self.contentNode.displaysAsynchronously = false
self.contentNode.contentMode = .left
self.contentNode.contentsScale = UIScreenScale
super.init()
self.addSubnode(self.contentNode)
}
deinit {
self.currentProgressDisposable?.dispose()
}
public func makeProgress() -> Promise<Bool> {
let progress = Promise<Bool>()
self.currentProgressDisposable?.dispose()
self.currentProgressDisposable = (progress.get()
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
guard let self else {
return
}
self.backgroundView.displayProgress = hasProgress
})
return progress
}
public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode) {
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)
let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode)
let previousMediaReference = maybeNode?.previousMediaReference
return { arguments in
let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let titleFont = Font.semibold(fontSize)
let textFont = Font.regular(fontSize)
var titleString: NSAttributedString
var textString: NSAttributedString
let isMedia: Bool
let isText: Bool
var isExpiredStory: Bool = false
var isStory: Bool = false
let titleColor: UIColor
let mainColor: UIColor
let dustColor: UIColor
var secondaryColor: UIColor?
var authorNameColor: UIColor?
var dashSecondaryColor: UIColor?
let author = arguments.message?.effectiveAuthor
authorNameColor = author?.nameColor?.color
dashSecondaryColor = author?.nameColor?.dashColors.1
switch arguments.type {
case let .bubble(incoming):
titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
if incoming {
if let authorNameColor {
mainColor = authorNameColor
secondaryColor = dashSecondaryColor
} else {
mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor
}
} else {
mainColor = arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
if dashSecondaryColor != nil {
secondaryColor = .clear
}
}
dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
case .standalone:
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
titleColor = serviceColor.primaryText
mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText
dustColor = titleColor
}
if let message = arguments.message {
let author = message.effectiveAuthor
let rawTitleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) || arguments.parentMessage.forwardInfo != nil {
if let author = forwardInfo.author {
let rawTitleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
} else if let authorSignature = forwardInfo.authorSignature {
let rawTitleString = authorSignature
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
}
}
if message.id.peerId != arguments.parentMessage.id.peerId, let peer = message.peers[message.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
}
let font = titleFont
let runDelegateData = RunDelegateData(
ascent: font.ascender,
descent: font.descender,
width: channelIcon.size.width
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateCurrentVersion,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
let rawTitleString = NSMutableAttributedString(attributedString: titleString)
rawTitleString.insert(NSAttributedString(string: ">", attributes: [
.attachment: channelIcon,
.foregroundColor: titleColor,
NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String): runDelegate
]), at: 0)
titleString = rawTitleString
} else {
let rawTitleString = NSMutableAttributedString(attributedString: titleString)
rawTitleString.append(NSAttributedString(string: ">", attributes: [
.attachment: groupIcon,
.foregroundColor: titleColor,
NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String): runDelegate
]))
rawTitleString.append(NSAttributedString(string: peer.debugDisplayTitle, font: titleFont, textColor: titleColor))
titleString = rawTitleString
}
}
}
let (textStringValue, isMediaValue, isTextValue) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId)
textString = textStringValue
isMedia = isMediaValue
isText = isTextValue
} else if let replyForward = arguments.replyForward {
if let replyAuthorId = replyForward.peerId, let replyAuthor = arguments.parentMessage.peers[replyAuthorId] {
let rawTitleString = EnginePeer(replyAuthor).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
} else {
let rawTitleString = replyForward.authorName ?? " "
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
}
//TODO:localize
textString = NSAttributedString(string: replyForward.quote?.text ?? "Message")
if let media = replyForward.quote?.media {
if let text = replyForward.quote?.text, !text.isEmpty {
} else {
if let contentKind = mediaContentKind(EngineMedia(media), message: nil, strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) {
let (string, _) = stringForMediaKind(contentKind, strings: arguments.strings)
textString = string
} else {
textString = NSAttributedString(string: "Message")
}
}
isMedia = true
} else {
isMedia = false
}
isText = replyForward.quote?.text != nil && replyForward.quote?.text != ""
} else if let story = arguments.story {
if let authorPeer = arguments.parentMessage.peers[story.peerId] {
let rawTitleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
} else {
let rawTitleString = arguments.strings.User_DeletedAccount
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
}
isText = false
var hideStory = false
if let peer = arguments.parentMessage.peers[story.peerId] as? TelegramChannel, peer.username == nil, peer.usernames.isEmpty {
switch peer.participationStatus {
case .member:
break
case .kicked, .left:
hideStory = true
}
}
if let storyItem = arguments.parentMessage.associatedStories[story], storyItem.data.isEmpty {
isExpiredStory = true
textString = NSAttributedString(string: arguments.strings.Chat_ReplyExpiredStory)
isMedia = false
} else if hideStory {
isExpiredStory = true
textString = NSAttributedString(string: arguments.strings.Chat_ReplyStoryPrivateChannel)
isMedia = false
} else {
isStory = true
textString = NSAttributedString(string: arguments.strings.Chat_ReplyStory)
isMedia = true
}
} else {
titleString = NSAttributedString(string: " ", font: titleFont, textColor: titleColor)
textString = NSAttributedString(string: " ")
isMedia = true
isText = false
}
let isIncoming = arguments.parentMessage.effectivelyIncoming(arguments.context.account.peerId)
let placeholderColor: UIColor = isIncoming ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
let textColor: UIColor
switch arguments.type {
case let .bubble(incoming):
if isExpiredStory || isStory {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
} else if isMedia {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
} else {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
}
case .standalone:
textColor = titleColor
}
let messageText: NSAttributedString
if isText, let message = arguments.message {
var text: String
var messageEntities: [MessageTextEntity]
if let quote = arguments.quote, !quote.text.isEmpty {
text = quote.text
messageEntities = quote.entities
} else {
text = foldLineBreaks(message.text)
messageEntities = message.textEntitiesAttribute?.entities ?? []
}
if let translateToLanguage = arguments.associatedData.translateToLanguage, !text.isEmpty {
for attribute in message.attributes {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
text = attribute.text
messageEntities = attribute.entities
break
}
}
}
let entities = messageEntities.filter { entity in
if case .Strikethrough = entity.type {
return true
} else if case .Spoiler = entity.type {
return true
} else if case .CustomEmoji = entity.type {
return true
} else {
return false
}
}
if entities.count > 0 {
messageText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message)
} else {
messageText = NSAttributedString(string: text, font: textFont, textColor: textColor)
}
} else if isText, let replyForward = arguments.replyForward, let quote = replyForward.quote {
let entities = quote.entities.filter { entity in
if case .Strikethrough = entity.type {
return true
} else if case .Spoiler = entity.type {
return true
} else if case .CustomEmoji = entity.type {
return true
} else {
return false
}
}
if entities.count > 0 {
messageText = stringWithAppliedEntities(quote.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
} else {
messageText = NSAttributedString(string: quote.text, font: textFont, textColor: textColor)
}
} else {
messageText = NSAttributedString(string: textString.string, font: textFont, textColor: textColor)
}
var leftInset: CGFloat = 11.0
let spacing: CGFloat = 2.0
var updatedMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
var hasRoundImage = false
if let message = arguments.message, !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 && !file.isVideoSticker {
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
}
}
} else if let story = arguments.story, let storyPeer = arguments.parentMessage.peers[story.peerId], let storyItem = arguments.parentMessage.associatedStories[story] {
if let itemValue = storyItem.get(Stories.StoredItem.self), case let .item(item) = itemValue, let peerReference = PeerReference(storyPeer) {
if let image = item.media as? TelegramMediaImage {
updatedMediaReference = .story(peer: peerReference, id: story.id, media: image)
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
} else if let file = item.media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker {
updatedMediaReference = .story(peer: peerReference, id: story.id, media: file)
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
} else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker {
imageDimensions = representation.dimensions.cgSize
}
}
}
} else if let replyForward = arguments.replyForward, let media = replyForward.quote?.media {
if let image = media as? TelegramMediaImage {
updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: image)
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
} else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker {
updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), 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
}
}
}
var imageTextInset: CGFloat = 0.0
if let _ = imageDimensions {
imageTextInset += floor(arguments.presentationData.fontSize.baseDisplaySize * 32.0 / 17.0)
}
let maximumTextWidth = max(0.0, arguments.constrainedSize.width - 8.0 - imageTextInset)
var contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height)
let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
var additionalTitleWidth: CGFloat = 0.0
var maxTitleNumberOfLines = 1
var maxTextNumberOfLines = 1
var adjustedConstrainedTextSize = contrainedTextSize
var textCutout: TextNodeCutout?
var textCutoutWidth: CGFloat = 0.0
if arguments.quote != nil || arguments.replyForward?.quote != nil {
additionalTitleWidth += 10.0
if case .bubble = arguments.type {
maxTitleNumberOfLines = 2
maxTextNumberOfLines = 5
if imageTextInset != 0.0 {
adjustedConstrainedTextSize.width += imageTextInset
textCutout = TextNodeCutout(topLeft: CGSize(width: imageTextInset + 6.0, height: 10.0))
textCutoutWidth = imageTextInset + 6.0
}
}
}
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
if isExpiredStory || isStory {
contrainedTextSize.width -= 26.0
}
if titleLayout.numberOfLines > 1 {
textCutout = nil
}
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: maxTextNumberOfLines, truncationType: .end, constrainedSize: adjustedConstrainedTextSize, alignment: .natural, lineSpacing: 0.07, cutout: textCutout, insets: textInsets))
let imageSide: CGFloat
let titleLineHeight: CGFloat = titleLayout.linesRects().first?.height ?? 12.0
imageSide = titleLineHeight * 2.0
var applyImage: (() -> TransformImageNode)?
if let imageDimensions = imageDimensions {
let boundingSize = CGSize(width: imageSide, height: imageSide)
leftInset += imageSide + 6.0
var radius: CGFloat = 4.0
var imageSize = imageDimensions.aspectFilled(boundingSize)
if hasRoundImage {
radius = boundingSize.width / 2.0
imageSize.width += 2.0
imageSize.height += 2.0
}
if !isExpiredStory {
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
}
let hasSpoiler: Bool
if let message = arguments.message {
hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute })
} else {
hasSpoiler = false
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var mediaUserLocation: MediaResourceUserLocation = .other
if let message = arguments.message {
mediaUserLocation = .peer(message.id.peerId)
}
if let updatedMediaReference = updatedMediaReference, mediaUpdated && imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
updateImageSignal = chatMessagePhotoThumbnail(account: arguments.context.account, userLocation: mediaUserLocation, photoReference: imageReference, blurred: hasSpoiler)
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
if fileReference.media.isVideo {
updateImageSignal = chatMessageVideoThumbnail(account: arguments.context.account, userLocation: mediaUserLocation, fileReference: fileReference, blurred: hasSpoiler)
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
updateImageSignal = chatWebpageSnippetFile(account: arguments.context.account, userLocation: mediaUserLocation, mediaReference: fileReference.abstract, representation: iconImageRepresentation)
}
}
}
var size = CGSize(width: max(titleLayout.size.width + additionalTitleWidth - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right - textCutoutWidth) + leftInset + 6.0, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing)
if isExpiredStory || isStory {
size.width += 16.0
}
return (size, { realSize, attemptSynchronous, animation in
let node: ChatMessageReplyInfoNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageReplyInfoNode()
}
node.previousMediaReference = updatedMediaReference
node.titleNode?.displaysAsynchronously = !arguments.presentationData.isPreview
node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
let titleNode = titleApply()
var textArguments: TextNodeWithEntities.Arguments?
if let cache = arguments.animationCache, let renderer = arguments.animationRenderer {
textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor, attemptSynchronous: attemptSynchronous)
}
let textNode = textApply(textArguments)
textNode.visibilityRect = node.visibility ? CGRect.infinite : nil
if node.titleNode == nil {
titleNode.isUserInteractionEnabled = false
node.titleNode = titleNode
node.contentNode.addSubnode(titleNode)
}
if node.textNode == nil {
textNode.textNode.isUserInteractionEnabled = false
node.textNode = textNode
node.contentNode.addSubnode(textNode.textNode)
}
if let applyImage = applyImage {
let imageNode = applyImage()
if node.imageNode == nil {
imageNode.isLayerBacked = false
node.addSubnode(imageNode)
node.imageNode = imageNode
}
imageNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 3.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
}
if let message = arguments.message {
node.imageNode?.captureProtected = message.isCopyProtected()
}
titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: spacing - textInsets.top + 1.0), size: titleLayout.size)
let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0 - textCutoutWidth, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size)
textNode.textNode.frame = textFrame.offsetBy(dx: (isExpiredStory || isStory) ? 18.0 : 0.0, dy: 0.0)
if isExpiredStory || isStory {
let expiredStoryIconView: UIImageView
if let current = node.expiredStoryIconView {
expiredStoryIconView = current
} else {
expiredStoryIconView = UIImageView()
node.expiredStoryIconView = expiredStoryIconView
node.view.addSubview(expiredStoryIconView)
}
let imageType: ChatExpiredStoryIndicatorType
switch arguments.type {
case .standalone:
imageType = .free
case let .bubble(incoming):
imageType = incoming ? .incoming : .outgoing
}
if isExpiredStory {
expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(arguments.presentationData.theme.theme, type: imageType)
} else {
expiredStoryIconView.image = PresentationResourcesChat.chatReplyStoryIndicatorIcon(arguments.presentationData.theme.theme, type: imageType)
}
if let image = expiredStoryIconView.image {
let imageSize: CGSize
if isExpiredStory {
imageSize = CGSize(width: floor(image.size.width * 1.22), height: floor(image.size.height * 1.22))
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: textFrame.minX - 2.0, y: textFrame.minY + 2.0), size: imageSize)
} else {
imageSize = image.size
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: textFrame.minX - 1.0, y: textFrame.minY + 3.0 + UIScreenPixel), size: imageSize)
}
}
} else if let expiredStoryIconView = node.expiredStoryIconView {
expiredStoryIconView.removeFromSuperview()
}
if !textLayout.spoilers.isEmpty {
let dustNode: InvisibleInkDustNode
if let current = node.dustNode {
dustNode = current
} else {
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: arguments.context.sharedContext.energyUsageSettings.fullTranslucency)
dustNode.isUserInteractionEnabled = false
node.dustNode = dustNode
node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode.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, textColor: 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
}
if node.backgroundView.superview == nil {
node.contentNode.view.insertSubview(node.backgroundView, at: 0)
}
var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: realSize.width, height: realSize.height + 2.0))
if case .standalone = arguments.type {
backgroundFrame.size.height -= 1.0
}
node.backgroundView.frame = backgroundFrame
var pattern: MessageInlineBlockBackgroundView.Pattern?
if let backgroundEmojiId = author?.backgroundEmojiId {
pattern = MessageInlineBlockBackgroundView.Pattern(
context: arguments.context,
fileId: backgroundEmojiId,
file: arguments.parentMessage.associatedMedia[MediaId(
namespace: Namespaces.Media.CloudFile,
id: backgroundEmojiId
)] as? TelegramMediaFile
)
}
node.backgroundView.update(
size: backgroundFrame.size,
primaryColor: mainColor,
secondaryColor: secondaryColor,
pattern: pattern,
animation: animation
)
let _ = secondaryColor
/*if let secondaryColor {
let lineDashView: UIImageView
if let current = node.lineDashView {
lineDashView = current
} else {
lineDashView = UIImageView(image: PresentationResourcesChat.chatReplyLineDashTemplateImage(arguments.presentationData.theme.theme, incoming: isIncoming))
lineDashView.clipsToBounds = true
node.lineDashView = lineDashView
node.contentNode.view.addSubview(lineDashView)
}
lineDashView.tintColor = secondaryColor
lineDashView.frame = CGRect(origin: .zero, size: CGSize(width: 12.0, height: backgroundFrame.height))
lineDashView.layer.cornerRadius = 6.0
if #available(iOS 13.0, *) {
lineDashView.layer.cornerCurve = .continuous
}
} else {
if let lineDashView = node.lineDashView {
node.lineDashView = nil
lineDashView.removeFromSuperview()
}
}*/
if arguments.quote != nil || arguments.replyForward?.quote != nil {
let quoteIconView: UIImageView
if let current = node.quoteIconView {
quoteIconView = current
} else {
quoteIconView = UIImageView(image: quoteIcon)
node.quoteIconView = quoteIconView
node.contentNode.view.addSubview(quoteIconView)
}
quoteIconView.tintColor = mainColor
quoteIconView.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 4.0 - quoteIcon.size.width, y: backgroundFrame.minY + 4.0), size: quoteIcon.size)
} else {
if let quoteIconView = node.quoteIconView {
node.quoteIconView = nil
quoteIconView.removeFromSuperview()
}
}
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
return node
})
}
}
public func updateTouchesAtPoint(_ point: CGPoint?) {
var isHighlighted = false
if point != nil {
isHighlighted = true
}
let transition: ContainedViewLayoutTransition = .animated(duration: isHighlighted ? 0.3 : 0.2, curve: .easeInOut)
let scale: CGFloat = isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0
transition.updateSublayerTransformScale(node: self, scale: scale, beginWithCurrentState: true)
}
public func animateFromInputPanel(sourceReplyPanel: TransitionReplyPanel, 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.textNode.frame.minX,
y: localRect.minY + sourceReplyPanel.textNode.frame.midY - textNode.textNode.frame.midY
)
transition.horizontal.animatePositionAdditive(node: textNode.textNode, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(node: textNode.textNode, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.textNode)
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 backgroundView = self.backgroundView
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - backgroundView.frame.minX,
y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - backgroundView.frame.minY
)
transition.horizontal.animatePositionAdditive(layer: backgroundView.layer, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(layer: backgroundView.layer, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.lineNode)
backgroundView.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
}
}
public func mediaTransitionView() -> UIView? {
if let imageNode = self.imageNode {
return imageNode.view
}
return nil
}
}