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 import TelegramNotices public final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() public let chatPeerId: EnginePeer.Id public let messageId: MessageId public let quote: EngineMessageReplyQuote? private var previousMediaReference: AnyMediaReference? public let closeButton: HighlightableButtonNode public let lineNode: ASImageNode public let iconNode: ASImageNode public let titleNode: ImmediateTextNode public let textNode: ImmediateTextNodeWithEntities public let imageNode: TransformImageNode private let actionArea: AccessibilityAreaNode private let context: AccountContext public var theme: PresentationTheme public var strings: PresentationStrings private var textIsOptions: Bool = false private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? public init(context: AccountContext, chatPeerId: EnginePeer.Id, messageId: MessageId, quote: EngineMessageReplyQuote?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.chatPeerId = chatPeerId 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(15.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 }) } } var titleText: String if let quote = strongSelf.quote { //TODO:localize titleText = "Reply to quote by \(authorName)" strongSelf.textNode.attributedText = NSAttributedString(string: quote.text, font: textFont, textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) } else { titleText = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string strongSelf.textNode.attributedText = messageText } if strongSelf.messageId.peerId != strongSelf.chatPeerId { if let peer = message?.peers[strongSelf.messageId.peerId], (peer is TelegramChannel || peer is TelegramGroup) { titleText += " in \(peer.debugDisplayTitle)" } } strongSelf.titleNode.attributedText = NSAttributedString(string: titleText, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) 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) } let _ = (ApplicationSpecificNotice.getChatReplyOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in if let strongSelf = self, count < 3 { Queue.mainQueue().after(3.0) { if let snapshotView = strongSelf.textNode.view.snapshotContentTree() { let text: String //TODO:localize if let (size, _, _) = strongSelf.validLayout, size.width > 320.0 { text = "Tap here for options" } else { text = "Tap here for forwarding options" } strongSelf.textIsOptions = true strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) strongSelf.view.addSubview(snapshotView) if let (size, inset, interfaceState) = strongSelf.validLayout { strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState) } strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } let _ = ApplicationSpecificNotice.incrementChatReplyOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).start() } } }) } })) } deinit { self.messageDisposable.dispose() } override public func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } override public func animateIn() { self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } override public func animateOut() { self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } override public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.updateThemeAndStrings(theme: theme, strings: strings, force: false) } private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, force: Bool) { if self.theme !== theme || force { 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.textIsOptions ? self.theme.chat.inputPanel.secondaryTextColor : 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 public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } override public 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 private func closePressed() { if let dismiss = self.dismiss { dismiss() } } private var previousTapTimestamp: Double? @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let timestamp = CFAbsoluteTimeGetCurrent() if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { return } self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() self.interfaceInteraction?.presentReplyOptions(self) Queue.mainQueue().after(1.5) { self.updateThemeAndStrings(theme: self.theme, strings: self.strings, force: true) } let _ = ApplicationSpecificNotice.incrementChatReplyOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start() } } /*@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.interfaceInteraction?.navigateToMessage(self.messageId, false, true, .generic) } }*/ }