diff --git a/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift b/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift index a9ec62c03f..5a941f316c 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift @@ -2,22 +2,7 @@ import Foundation import Postbox import TelegramCore -public struct ReactionGestureItemValue { - public var value: String - public var text: String - public var file: TelegramMediaFile - - public init(value: String, text: String, file: TelegramMediaFile) { - self.value = value - self.text = text - self.file = file - } -} - -public final class ReactionGestureItem { - public let value: ReactionGestureItemValue - - public init(value: ReactionGestureItemValue) { - self.value = value - } +public enum ReactionGestureItem { + case reaction(value: String, text: String, file: TelegramMediaFile) + case reply } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 340530113c..1694dca4de 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -5,11 +5,7 @@ import Display import Postbox import TelegramCore -private let shadowBlur: CGFloat = 8.0 -private let minimizedReactionSize: CGFloat = 30.0 -private let maximizedReactionSize: CGFloat = 60.0 - -private func generateBubbleImage(foreground: UIColor, diameter: CGFloat) -> UIImage? { +private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(foreground.cgColor) @@ -17,7 +13,7 @@ private func generateBubbleImage(foreground: UIColor, diameter: CGFloat) -> UIIm })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) } -private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UIImage? { +private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) @@ -34,11 +30,12 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UI private final class ReactionNode: ASDisplayNode { let reaction: ReactionGestureItem private let animationNode: AnimatedStickerNode + private let imageNode: ASImageNode var isMaximized: Bool? private let intrinsicSize: CGSize private let intrinsicOffset: CGPoint - init(account: Account, reaction: ReactionGestureItem) { + init(account: Account, reaction: ReactionGestureItem, maximizedReactionSize: CGFloat) { self.reaction = reaction self.animationNode = AnimatedStickerNode() @@ -47,18 +44,29 @@ private final class ReactionNode: ASDisplayNode { //self.animationNode.backgroundColor = .lightGray var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0) - switch reaction.value.value { - case "😳": - intrinsicSize.width += 8.0 - intrinsicSize.height += 8.0 - self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0) - case "👍": - intrinsicSize.width += 20.0 - intrinsicSize.height += 20.0 - self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0) - default: + + self.imageNode = ASImageNode() + switch reaction { + case let .reaction(value, _, file): + switch value { + case "😳": + intrinsicSize.width += 8.0 + intrinsicSize.height += 8.0 + self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0) + case "👍": + intrinsicSize.width += 20.0 + intrinsicSize.height += 20.0 + self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0) + default: + self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0) + } + self.animationNode.visibility = true + self.animationNode.setup(account: account, resource: file.resource, width: Int(intrinsicSize.width) * 2, height: Int(intrinsicSize.height) * 2, mode: .direct) + case .reply: self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0) + self.imageNode.image = UIImage(named: "Chat/Context Menu/ReactionReply", in: Bundle(for: ReactionNode.self), compatibleWith: nil) } + self.intrinsicSize = intrinsicSize super.init() @@ -66,15 +74,17 @@ private final class ReactionNode: ASDisplayNode { //self.backgroundColor = .green self.addSubnode(self.animationNode) - self.animationNode.visibility = true - self.animationNode.setup(account: account, resource: reaction.value.file.resource, width: Int(self.intrinsicSize.width) * 2, height: Int(self.intrinsicSize.height) * 2, mode: .direct) + self.addSubnode(self.imageNode) self.animationNode.updateLayout(size: self.intrinsicSize) self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) + self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) } func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.animationNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true) transition.updateTransformScale(node: self.animationNode, scale: scale, beginWithCurrentState: true) + transition.updatePosition(node: self.imageNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true) + transition.updateTransformScale(node: self.imageNode, scale: scale, beginWithCurrentState: true) } func updateIsAnimating(_ isAnimating: Bool, animated: Bool) { @@ -87,43 +97,46 @@ private final class ReactionNode: ASDisplayNode { } final class ReactionSelectionNode: ASDisplayNode { + private let account: Account + private let reactions: [ReactionGestureItem] + private let backgroundNode: ASImageNode private let backgroundShadowNode: ASImageNode private let bubbleNodes: [(ASImageNode, ASImageNode)] - private let reactionNodes: [ReactionNode] + private var reactionNodes: [ReactionNode] = [] private var hasSelectedNode = false private let hapticFeedback = HapticFeedback() + private var shadowBlur: CGFloat = 8.0 + private var minimizedReactionSize: CGFloat = 30.0 + private var maximizedReactionSize: CGFloat = 60.0 + private var smallCircleSize: CGFloat = 8.0 + public init(account: Account, reactions: [ReactionGestureItem]) { + self.account = account + self.reactions = reactions + self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: 42.0) self.backgroundShadowNode = ASImageNode() self.backgroundShadowNode.displaysAsynchronously = false self.backgroundShadowNode.displayWithoutProcessing = true - self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: 42.0) self.bubbleNodes = (0 ..< 2).map { i -> (ASImageNode, ASImageNode) in let imageNode = ASImageNode() - imageNode.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * 8.0) imageNode.displaysAsynchronously = false imageNode.displayWithoutProcessing = true let shadowNode = ASImageNode() - shadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * 8.0) shadowNode.displaysAsynchronously = false shadowNode.displayWithoutProcessing = true return (imageNode, shadowNode) } - self.reactionNodes = reactions.map { reaction -> ReactionNode in - return ReactionNode(account: account, reaction: reaction) - } - super.init() self.bubbleNodes.forEach { _, shadow in @@ -134,18 +147,50 @@ final class ReactionSelectionNode: ASDisplayNode { self.addSubnode(foreground) } self.addSubnode(self.backgroundNode) - self.reactionNodes.forEach(self.addSubnode(_:)) } func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) { - let backgroundHeight: CGFloat = 42.0 - let reactionSpacing: CGFloat = 6.0 + let initialAnchorX = startingPoint.x + + if isInitial && self.reactionNodes.isEmpty { + //let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing + + //contentWidth = CGFloat(self.reactionNodes.count - 1) * X + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * 0.2 * X + // contentWidth - maximizedReactionSize = CGFloat(self.reactionNodes.count - 1) * X + CGFloat(self.reactionNodes.count + 1) * 0.2 * X + // (contentWidth - maximizedReactionSize) / (CGFloat(self.reactionNodes.count - 1) + CGFloat(self.reactionNodes.count + 1) * 0.2) = X + let availableContentWidth = max(100.0, initialAnchorX) + var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2) + minimizedReactionSize = max(16.0, floor(minimizedReactionSize)) + minimizedReactionSize = min(30.0, minimizedReactionSize) + + self.minimizedReactionSize = minimizedReactionSize + self.shadowBlur = floor(minimizedReactionSize * 0.26) + self.smallCircleSize = 8.0 + + let backgroundHeight = floor(minimizedReactionSize * 1.4) + + self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: backgroundHeight, shadowBlur: self.shadowBlur) + self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: backgroundHeight, shadowBlur: self.shadowBlur) + for i in 0 ..< self.bubbleNodes.count { + self.bubbleNodes[i].0.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) + self.bubbleNodes[i].1.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) + } + + self.reactionNodes = self.reactions.map { reaction -> ReactionNode in + return ReactionNode(account: self.account, reaction: reaction, maximizedReactionSize: self.maximizedReactionSize) + } + self.reactionNodes.forEach(self.addSubnode(_:)) + } + + let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.4) + + let reactionSpacing: CGFloat = floor(self.minimizedReactionSize * 0.2) let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0) let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0)) - backgroundFrame = backgroundFrame.offsetBy(dx: startingPoint.x - contentWidth + backgroundHeight / 2.0 - 52.0, dy: startingPoint.y - backgroundHeight - 16.0) + backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) self.backgroundNode.frame = backgroundFrame self.backgroundShadowNode.frame = backgroundFrame @@ -201,11 +246,11 @@ final class ReactionSelectionNode: ASDisplayNode { reactionX += reactionSize + reactionSpacing } - let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - 8.0 - shadowBlur, y: backgroundFrame.maxY - shadowBlur - 8.0 - shadowBlur), size: CGSize(width: 16.0 + shadowBlur * 2.0, height: 16.0 + shadowBlur * 2.0)) + let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - self.smallCircleSize - shadowBlur, y: backgroundFrame.maxY - shadowBlur - self.smallCircleSize - shadowBlur), size: CGSize(width: self.smallCircleSize * 2.0 + shadowBlur * 2.0, height: self.smallCircleSize * 2.0 + shadowBlur * 2.0)) self.bubbleNodes[1].0.frame = mainBubbleFrame self.bubbleNodes[1].1.frame = mainBubbleFrame - let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - 9.0 - (8.0 + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + 12.0 - (8.0 + shadowBlur * 2.0) / 2.0), size: CGSize(width: 8.0 + shadowBlur * 2.0, height: 8.0 + shadowBlur * 2.0)) + let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - floor(self.smallCircleSize * 0.88) - (self.smallCircleSize + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + floor(self.smallCircleSize * 4.0 / 3.0) - (self.smallCircleSize + shadowBlur * 2.0) / 2.0), size: CGSize(width: self.smallCircleSize + shadowBlur * 2.0, height: self.smallCircleSize + shadowBlur * 2.0)) self.bubbleNodes[0].0.frame = secondaryBubbleFrame self.bubbleNodes[0].1.frame = secondaryBubbleFrame } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift index 61b67da170..d9a0cc59bf 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift @@ -6,13 +6,16 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { private var validatedGesture = false private var firstLocation: CGPoint = CGPoint() + private var currentLocation: CGPoint = CGPoint() private var currentReactions: [ReactionGestureItem] = [] private var isActivated = false private var isAwaitingCompletion = false private weak var currentContainer: ReactionSelectionParentNode? + private var activationTimer: Timer? public var availableReactions: (() -> [ReactionGestureItem])? public var getReactionContainer: (() -> ReactionSelectionParentNode?)? + public var began: (() -> Void)? public var updateOffset: ((CGFloat, Bool) -> Void)? public var completed: ((ReactionGestureItem?) -> Void)? public var displayReply: ((CGFloat) -> Void)? @@ -31,6 +34,8 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.currentReactions = [] self.isActivated = false self.isAwaitingCompletion = false + self.activationTimer?.invalidate() + self.activationTimer = nil } override public func touchesBegan(_ touches: Set, with event: UIEvent) { @@ -40,6 +45,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.currentReactions = availableReactions let touch = touches.first! self.firstLocation = touch.location(in: nil) + self.currentLocation = self.firstLocation } else { self.state = .failed } @@ -55,6 +61,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { guard let location = touches.first?.location(in: nil) else { return } + self.currentLocation = location var translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) @@ -72,8 +79,38 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.validatedGesture = true self.firstLocation = location translation = CGPoint() + self.began?() self.updateOffset?(0.0, false) updatedOffset = true + + self.activationTimer?.invalidate() + final class TimerTarget: NSObject { + let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + } + + @objc func event() { + self.f() + } + } + let activationTimer = Timer(timeInterval: 0.3, target: TimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.activationTimer = nil + if strongSelf.validatedGesture { + let location = strongSelf.currentLocation + if !strongSelf.currentReactions.isEmpty, let reactionContainer = strongSelf.getReactionContainer?() { + strongSelf.currentContainer = reactionContainer + let reactionContainerLocation = reactionContainer.view.convert(location, from: nil) + reactionContainer.displayReactions(strongSelf.currentReactions, at: reactionContainerLocation) + } + } + }, selector: #selector(TimerTarget.event), userInfo: nil, repeats: false) + self.activationTimer = activationTimer + RunLoop.main.add(activationTimer, forMode: .common) } } @@ -85,11 +122,6 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { if absTranslationX > 40.0 { self.isActivated = true self.displayReply?(-min(0.0, translation.x)) - if !self.currentReactions.isEmpty, let reactionContainer = self.getReactionContainer?() { - self.currentContainer = reactionContainer - let reactionContainerLocation = reactionContainer.view.convert(location, from: nil) - reactionContainer.displayReactions(self.currentReactions, at: reactionContainerLocation) - } } } else { if let reactionContainer = self.currentContainer { @@ -111,8 +143,8 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { if self.validatedGesture { let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) if let reaction = self.currentContainer?.selectedReaction() { - self.completed?(reaction) self.isAwaitingCompletion = true + self.completed?(reaction) } else { if translation.x < -40.0 { self.currentContainer?.dismissReactions(into: nil, hideTarget: false) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/Contents.json new file mode 100644 index 0000000000..41e8c1ebb6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ReplyReaction@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ReplyReaction@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@2x.png new file mode 100644 index 0000000000..6578625abe Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@2x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@3x.png new file mode 100644 index 0000000000..d149f552b9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@3x.png differ diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift index 314bc7f4bc..6326abbc23 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift @@ -418,9 +418,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } } } - if !item.controllerInteraction.canSetupReply(item.message) { - //return [] - } let reactions: [(String, String)] = [ ("😒", "Sad"), @@ -433,14 +430,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode var result: [ReactionGestureItem] = [] for (value, text) in reactions { if let file = item.associatedData.animatedEmojiStickers[value]?.file { - result.append(ReactionGestureItem(value: ReactionGestureItemValue(value: value, text: text, file: file))) + result.append(.reaction(value: value, text: text, file: file)) } } + if item.controllerInteraction.canSetupReply(item.message) { + result.append(.reply) + } return result } reactionRecognizer.getReactionContainer = { [weak self] in return self?.item?.controllerInteraction.reactionContainerNode() } + reactionRecognizer.began = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.cancelInteractiveKeyboardGestures() + } reactionRecognizer.updateOffset = { [weak self] offset, animated in guard let strongSelf = self else { return @@ -479,11 +485,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode guard let strongSelf = self, let item = strongSelf.item else { return } - if strongSelf.swipeToReplyNode == nil { - if strongSelf.swipeToReplyFeedback == nil { - strongSelf.swipeToReplyFeedback = HapticFeedback() - } - strongSelf.swipeToReplyFeedback?.tap() + if strongSelf.swipeToReplyFeedback == nil { + strongSelf.swipeToReplyFeedback = HapticFeedback() + } + strongSelf.swipeToReplyFeedback?.tap() + if strongSelf.swipeToReplyNode == nil, false { let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper)) strongSelf.swipeToReplyNode = swipeToReplyNode strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea) @@ -497,8 +503,28 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode return } if let item = strongSelf.item, let reaction = reaction { - strongSelf.awaitingAppliedReaction = reaction.value.value - item.controllerInteraction.updateMessageReaction(item.message.id, reaction.value.value) + switch reaction { + case let .reaction(value, _, _): + strongSelf.awaitingAppliedReaction = value + item.controllerInteraction.updateMessageReaction(item.message.id, value) + case .reply: + strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false) + var bounds = strongSelf.bounds + let offset = bounds.origin.x + bounds.origin.x = 0.0 + strongSelf.bounds = bounds + if !offset.isZero { + strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } + if let swipeToReplyNode = strongSelf.swipeToReplyNode { + strongSelf.swipeToReplyNode = nil + swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in + swipeToReplyNode?.removeFromSupernode() + }) + swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + item.controllerInteraction.setupReply(item.message.id) + } } else { strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false) var bounds = strongSelf.bounds