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 public enum ChatMessageReplyInfoType { case bubble(incoming: Bool) case standalone } private let quoteIcon: UIImage = { return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.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: UIImageView private var lineDashView: UIImageView? 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? override public init() { self.backgroundView = UIImageView() 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) } public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (CGSize, Bool) -> 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.medium(fontSize) let textFont = Font.regular(fontSize) var titleString: String let textString: NSAttributedString let isMedia: Bool let isText: Bool var isExpiredStory: Bool = false var isStory: Bool = false if let message = arguments.message { let author = message.effectiveAuthor titleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) || arguments.parentMessage.forwardInfo != nil { if let author = forwardInfo.author { titleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) } else if let authorSignature = forwardInfo.authorSignature { titleString = authorSignature } } if message.id.peerId != arguments.parentMessage.id.peerId { //TODO:localize if let peer = message.peers[message.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) { titleString += " in \(peer.debugDisplayTitle)" } } 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 { titleString = replyForward.authorName ?? " " //TODO:localize textString = NSAttributedString(string: replyForward.quote?.text ?? "Message") isMedia = false isText = true } else if let story = arguments.story { if let authorPeer = arguments.parentMessage.peers[story.peerId] { titleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) } else { titleString = arguments.strings.User_DeletedAccount } 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 = " " textString = NSAttributedString(string: " ") isMedia = true isText = false } let placeholderColor: UIColor = arguments.parentMessage.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor let textColor: UIColor let dustColor: UIColor var authorNameColor: UIColor? var dashSecondaryColor: UIColor? let author = arguments.message?.effectiveAuthor if author?.hasCustomNameColor == true || ([Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser) { authorNameColor = author?.nameColor?.color dashSecondaryColor = author?.nameColor?.dashColors.1 // if let rawAuthorNameColor = authorNameColor { // var dimColors = false // switch arguments.presentationData.theme.theme.name { // case .builtin(.nightAccent), .builtin(.night): // dimColors = true // default: // break // } // if dimColors { // var hue: CGFloat = 0.0 // var saturation: CGFloat = 0.0 // var brightness: CGFloat = 0.0 // rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) // authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) // } // } } let mainColor: UIColor var secondaryColor: UIColor? 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 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 } 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 textColor = titleColor dustColor = titleColor } let messageText: NSAttributedString if isText, let message = arguments.message { var text: String var messageEntities: [MessageTextEntity] if let quote = arguments.quote { 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 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 } } } } 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: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), 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 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.image == nil { if case .standalone = arguments.type { node.backgroundView.image = PresentationResourcesChat.chatReplyServiceBackgroundTemplateImage(arguments.presentationData.theme.theme) } else { node.backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(arguments.presentationData.theme.theme) } 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.tintColor = mainColor node.backgroundView.frame = backgroundFrame if let secondaryColor { let lineDashView: UIImageView if let current = node.lineDashView { lineDashView = current } else { lineDashView = UIImageView(image: PresentationResourcesChat.chatReplyLineDashTemplateImage(arguments.presentationData.theme.theme)) node.lineDashView = lineDashView node.contentNode.view.addSubview(lineDashView) } lineDashView.tintColor = secondaryColor lineDashView.frame = CGRect(origin: .zero, size: CGSize(width: 3.0, height: backgroundFrame.height)) } 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 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 } }