import Foundation import UIKit import AsyncDisplayKit import TelegramCore import Postbox import SwiftSignalKit import Display import TelegramPresentationData import TelegramUIPreferences import AccountContext import LocalizedPeerData import PhotoResources import TelegramStringFormatting import TextFormat import ChatPresentationInterfaceState import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import AccessoryPanelNode final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() let messageId: MessageId let quote: EngineMessageReplyQuote? private var previousMediaReference: AnyMediaReference? let closeButton: HighlightableButtonNode let lineNode: ASImageNode let iconNode: ASImageNode let titleNode: ImmediateTextNode let textNode: ImmediateTextNodeWithEntities let imageNode: TransformImageNode private let actionArea: AccessibilityAreaNode private let context: AccountContext var theme: PresentationTheme var strings: PresentationStrings private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? init(context: AccountContext, messageId: MessageId, quote: EngineMessageReplyQuote?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.messageId = messageId self.quote = quote self.context = context self.theme = theme self.strings = strings self.closeButton = HighlightableButtonNode() self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.iconNode = ASImageNode() self.iconNode.displayWithoutProcessing = false self.iconNode.displaysAsynchronously = false self.iconNode.image = PresentationResourcesChat.chatInputPanelReplyIconImage(theme) self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 1 self.titleNode.displaysAsynchronously = false self.titleNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) self.textNode = ImmediateTextNodeWithEntities() self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false self.textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) self.textNode.visibility = true self.textNode.spoilerColor = self.theme.chat.inputPanel.secondaryTextColor if let animationCache = animationCache, let animationRenderer = animationRenderer { self.textNode.arguments = TextNodeWithEntities.Arguments( context: context, cache: animationCache, renderer: animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, attemptSynchronous: false ) } self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isHidden = true self.actionArea = AccessibilityAreaNode() super.init() self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) self.addSubnode(self.closeButton) self.addSubnode(self.lineNode) self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.imageNode) self.addSubnode(self.actionArea) self.messageDisposable.set((context.account.postbox.messageView(messageId) |> deliverOnMainQueue).startStrict(next: { [weak self] messageView in if let strongSelf = self { if messageView.message == nil { Queue.mainQueue().justDispatch { strongSelf.interfaceInteraction?.setupReplyMessage(nil, { _ in }) } return } let message = messageView.message var authorName = "" var text = "" var isText = true if let forwardInfo = message?.forwardInfo, forwardInfo.flags.contains(.isImported) { if let author = forwardInfo.author { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } else if let authorSignature = forwardInfo.authorSignature { authorName = authorSignature } } else if let author = message?.effectiveAuthor { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } let isMedia: Bool if let message = message { switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) { case .text: isMedia = false default: isMedia = true } let (attributedText, _, isTextValue) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) text = attributedText.string isText = isTextValue } else { isMedia = false } let textFont = Font.regular(14.0) let messageText: NSAttributedString if isText, let message = message { let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in switch entity.type { case .Spoiler, .CustomEmoji: return true case .Strikethrough: return true default: return false } } let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor if entities.count > 0 { messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), 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: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) } } else { messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) } var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? var isRoundImage = false if let message = 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 { updatedMediaReference = .message(message: MessageReference(message), media: file) isRoundImage = file.isInstantVideo if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker && !file.isAnimatedSticker { imageDimensions = representation.dimensions.cgSize } break } } } let imageNodeLayout = strongSelf.imageNode.asyncLayout() var applyImage: (() -> Void)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: 35.0, height: 35.0) var radius: CGFloat = 2.0 var imageSize = imageDimensions.aspectFilled(boundingSize) if isRoundImage { radius = floor(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())) } var mediaUpdated = false if let updatedMediaReference = updatedMediaReference, let previousMediaReference = strongSelf.previousMediaReference { mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) } else if (updatedMediaReference != nil) != (strongSelf.previousMediaReference != nil) { mediaUpdated = true } strongSelf.previousMediaReference = updatedMediaReference let hasSpoiler = message?.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) ?? false var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference, blurred: hasSpoiler) } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isVideo { updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, fileReference: fileReference, blurred: hasSpoiler) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } else { updateImageSignal = .single({ _ in return nil }) } } if let quote = strongSelf.quote { //TODO:localize strongSelf.titleNode.attributedText = NSAttributedString(string: "Reply to quote by \(authorName)", font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) strongSelf.textNode.attributedText = NSAttributedString(string: quote.text, font: textFont, textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) } else { strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) strongSelf.textNode.attributedText = messageText } let headerString: String if let message = message, message.flags.contains(.Incoming), let author = message.author { headerString = "Reply to message. From: \(EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder))" } else if let message = message, !message.flags.contains(.Incoming) { headerString = "Reply to your message" } else { headerString = "Reply to message" } strongSelf.actionArea.accessibilityLabel = "\(headerString).\n\(text)" if let applyImage = applyImage { applyImage() strongSelf.imageNode.isHidden = false } else { strongSelf.imageNode.isHidden = true } if let updateImageSignal = updateImageSignal { strongSelf.imageNode.setSignal(updateImageSignal) } if let (size, inset, interfaceState) = strongSelf.validLayout { strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState) } } })) } deinit { self.messageDisposable.dispose() } override func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } override func animateIn() { self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } override func animateOut() { self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { if self.theme !== theme { self.theme = theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.iconNode.image = PresentationResourcesChat.chatInputPanelReplyIconImage(theme) if let text = self.titleNode.attributedText?.string { self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) } if let text = self.textNode.attributedText?.string { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) } self.textNode.spoilerColor = self.theme.chat.inputPanel.secondaryTextColor if let (size, inset, interfaceState) = self.validLayout { self.updateState(size: size, inset: inset, interfaceState: interfaceState) } } } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } override func updateState(size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState) { self.validLayout = (size, inset, interfaceState) let bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 45.0)) let leftInset: CGFloat = 55.0 + inset let textLineInset: CGFloat = 10.0 let rightInset: CGFloat = 55.0 let textRightInset: CGFloat = 20.0 let closeButtonSize = CGSize(width: 44.0, height: bounds.height) let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - closeButtonSize.width - inset, y: 2.0), size: closeButtonSize) self.closeButton.frame = closeButtonFrame self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) if self.lineNode.supernode == self { self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) } if let icon = self.iconNode.image { self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) } var imageTextInset: CGFloat = 0.0 if !self.imageNode.isHidden { imageTextInset = 9.0 + 35.0 } if self.imageNode.supernode == self { self.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0)) } let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) if self.titleNode.supernode == self { self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.titleNode.insets.left, y: 7.0 - self.titleNode.insets.top), size: titleSize) } let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) let textFrame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize) if self.textNode.supernode == self { self.textNode.frame = textFrame } } @objc func closePressed() { if let dismiss = self.dismiss { dismiss() } } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, .generic) } } }