Swiftgram/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift
2023-05-26 22:13:22 +04:00

550 lines
30 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
enum ChatMessageReplyInfoType {
case bubble(incoming: Bool)
case standalone
}
class ChatMessageReplyInfoNode: ASDisplayNode {
class Arguments {
let presentationData: ChatPresentationData
let strings: PresentationStrings
let context: AccountContext
let type: ChatMessageReplyInfoType
let message: Message?
let story: StoryId?
let parentMessage: Message
let constrainedSize: CGSize
let animationCache: AnimationCache?
let animationRenderer: MultiAnimationRenderer?
let associatedData: ChatMessageItemAssociatedData
init(
presentationData: ChatPresentationData,
strings: PresentationStrings,
context: AccountContext,
type: ChatMessageReplyInfoType,
message: Message?,
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.story = story
self.parentMessage = parentMessage
self.constrainedSize = constrainedSize
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.associatedData = associatedData
}
}
var visibility: Bool = false {
didSet {
if self.visibility != oldValue {
self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil
}
}
}
private let contentNode: ASDisplayNode
private let lineNode: ASImageNode
private var titleNode: TextNode?
private var textNode: TextNodeWithEntities?
private var dustNode: InvisibleInkDustNode?
private var imageNode: TransformImageNode?
private var previousMediaReference: AnyMediaReference?
override init() {
self.contentNode = ASDisplayNode()
self.contentNode.isUserInteractionEnabled = false
self.contentNode.displaysAsynchronously = false
self.contentNode.contentMode = .left
self.contentNode.contentsScale = UIScreenScale
self.lineNode = ASImageNode()
self.lineNode.displaysAsynchronously = false
self.lineNode.displayWithoutProcessing = true
self.lineNode.isLayerBacked = true
super.init()
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.lineNode)
}
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (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
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
}
}
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 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
}
//TODO:localize
textString = NSAttributedString(string: "Story")
isMedia = true
isText = false
} 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 lineImage: UIImage?
let textColor: UIColor
let dustColor: UIColor
var authorNameColor: UIColor?
let author = arguments.message?.effectiveAuthor
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser {
authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] }
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)
}
}
}
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
lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(arguments.presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(arguments.presentationData.theme.theme)
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
let graphics = PresentationResourcesChat.additionalGraphics(arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners)
lineImage = graphics.chatServiceVerticalLineImage
textColor = titleColor
dustColor = titleColor
}
let messageText: NSAttributedString
if isText, let message = arguments.message {
var text = foldLineBreaks(message.text)
var 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(trimToLineCount(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: 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 - imageTextInset)
let contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height)
let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets))
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets))
let imageSide = titleLayout.size.height + textLayout.size.height - 16.0
var applyImage: (() -> TransformImageNode)?
if let imageDimensions = imageDimensions {
let boundingSize = CGSize(width: imageSide, height: imageSide)
leftInset += imageSide + 2.0
var radius: CGFloat = 2.0
var imageSize = imageDimensions.aspectFilled(boundingSize)
if hasRoundImage {
radius = boundingSize.width / 2.0
imageSize.width += 2.0
imageSize.height += 2.0
}
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: placeholderColor))
}
var mediaUpdated = false
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference {
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
} else if (updatedMediaReference != nil) != (previousMediaReference != nil) {
mediaUpdated = true
}
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)
}
}
}
let size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing)
return (size, { 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//!smartInvertColorsEnabled()
node.addSubnode(imageNode)
node.imageNode = imageNode
}
imageNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 4.0 + UIScreenPixel), size: CGSize(width: imageSide, height: imageSide))
if let updateImageSignal = updateImageSignal {
imageNode.setSignal(updateImageSignal)
}
} else if let imageNode = node.imageNode {
imageNode.removeFromSupernode()
node.imageNode = nil
}
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, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size)
textNode.textNode.frame = textFrame
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
}
node.lineNode.image = lineImage
node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 4.0)))
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
return node
})
}
}
func animateFromInputPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint {
let sourceParentNode = ASDisplayNode()
let sourceParentOffset: CGPoint
if let unclippedTransitionNode = unclippedTransitionNode {
unclippedTransitionNode.addSubnode(sourceParentNode)
sourceParentNode.frame = sourceReplyPanel.relativeSourceRect
sourceParentOffset = self.view.convert(CGPoint(), to: sourceParentNode.view)
sourceParentNode.clipsToBounds = true
let panelOffset = sourceReplyPanel.relativeTargetRect.minY - sourceReplyPanel.relativeSourceRect.minY
sourceParentNode.frame = sourceParentNode.frame.offsetBy(dx: 0.0, dy: panelOffset)
sourceParentNode.bounds = sourceParentNode.bounds.offsetBy(dx: 0.0, dy: panelOffset)
transition.vertical.animatePositionAdditive(layer: sourceParentNode.layer, offset: CGPoint(x: 0.0, y: -panelOffset))
transition.vertical.animateOffsetAdditive(layer: sourceParentNode.layer, offset: -panelOffset)
} else {
self.addSubnode(sourceParentNode)
sourceParentOffset = CGPoint()
}
sourceParentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak sourceParentNode] _ in
sourceParentNode?.removeFromSupernode()
})
if let titleNode = self.titleNode {
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.titleNode.frame.minX - titleNode.frame.minX,
y: localRect.minY + sourceReplyPanel.titleNode.frame.midY - titleNode.frame.midY
)
transition.horizontal.animatePositionAdditive(node: titleNode, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(node: titleNode, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.titleNode)
titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
sourceReplyPanel.titleNode.frame = sourceReplyPanel.titleNode.frame
.offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y)
.offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y)
transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.titleNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false)
transition.vertical.animatePositionAdditive(node: sourceReplyPanel.titleNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false)
}
if let textNode = self.textNode {
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.textNode.frame.minX - textNode.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 lineNode = self.lineNode
let offset = CGPoint(
x: localRect.minX + sourceReplyPanel.lineNode.frame.minX - lineNode.frame.minX,
y: localRect.minY + sourceReplyPanel.lineNode.frame.minY - lineNode.frame.minY
)
transition.horizontal.animatePositionAdditive(node: lineNode, offset: CGPoint(x: offset.x, y: 0.0))
transition.vertical.animatePositionAdditive(node: lineNode, offset: CGPoint(x: 0.0, y: offset.y))
sourceParentNode.addSubnode(sourceReplyPanel.lineNode)
lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
sourceReplyPanel.lineNode.frame = sourceReplyPanel.lineNode.frame
.offsetBy(dx: sourceParentOffset.x, dy: sourceParentOffset.y)
.offsetBy(dx: localRect.minX - offset.x, dy: localRect.minY - offset.y)
transition.horizontal.animatePositionAdditive(node: sourceReplyPanel.lineNode, offset: CGPoint(x: offset.x, y: 0.0), removeOnCompletion: false)
transition.vertical.animatePositionAdditive(node: sourceReplyPanel.lineNode, offset: CGPoint(x: 0.0, y: offset.y), removeOnCompletion: false)
return offset
}
}
func mediaTransitionView() -> UIView? {
if let imageNode = self.imageNode {
return imageNode.view
}
return nil
}
}