From 0230fa9b279d6f2cf079e7fd48666850b4e2a49d Mon Sep 17 00:00:00 2001 From: Peter <> Date: Mon, 19 Aug 2019 23:27:00 +0300 Subject: [PATCH] Update reactions animation --- .../Sources/ReactionGestureItem.swift | 21 +--- .../Sources/ReactionSelectionNode.swift | 113 ++++++++++++------ .../ReactionSwipeGestureRecognizer.swift | 44 ++++++- .../ReactionReply.imageset/Contents.json | 22 ++++ .../ReplyReaction@2x.png | Bin 0 -> 9714 bytes .../ReplyReaction@3x.png | Bin 0 -> 15430 bytes .../ChatMessageBubbleItemNode.swift | 48 ++++++-- 7 files changed, 179 insertions(+), 69 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@2x.png create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@3x.png 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 0000000000000000000000000000000000000000..6578625abe9d2ed7fae04517643d19f26d4e072a GIT binary patch literal 9714 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d149f552b9a09e10fd2132151b70e4adb6ed6dfc GIT binary patch literal 15430 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 E0B3SAvj6}9 literal 0 HcmV?d00001 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