diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 47769179ca..0ac7f7abd5 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1229,12 +1229,21 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if let presentationNode = self.presentationNode { + presentationNode.addRelativeContentOffset(offset, transition: transition) + } if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode { reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y) } } + func cancelReactionAnimation() { + if let presentationNode = self.presentationNode { + presentationNode.cancelReactionAnimation() + } + } + func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { if let presentationNode = self.presentationNode { presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: completion) @@ -2372,6 +2381,10 @@ public final class ContextController: ViewController, StandalonePresentableContr } } + public func cancelReactionAnimation() { + self.controllerNode.cancelReactionAnimation() + } + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { self.controllerNode.addRelativeContentOffset(offset, transition: transition) } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 748e2630e8..4e63a212a6 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -653,4 +653,15 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo intermediateCompletion() }) } + + func cancelReactionAnimation() { + self.reactionContextNode?.cancelReactionAnimation() + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode { + reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y) + transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y) + } + } } diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index a45bdcfaac..cd27640916 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -25,7 +25,10 @@ protocol ContextControllerPresentationNode: ASDisplayNode { ) func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) + func cancelReactionAnimation() func highlightGestureMoved(location: CGPoint) func highlightGestureFinished(performAction: Bool) + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1776369416..1dadfc99ca 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -7,11 +7,6 @@ import TelegramPresentationData import AccountContext import TelegramAnimatedStickerNode -public enum ReactionGestureItem { - case like - case unlike -} - public final class ReactionContextItem { public struct Reaction: Equatable { public var rawValue: String @@ -118,6 +113,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public var reactionSelected: ((ReactionContextItem) -> Void)? private var hapticFeedback: HapticFeedback? + private var standaloneReactionAnimation: StandaloneReactionAnimation? + + private weak var animationTargetView: UIView? + private var animationHideNode: Bool = false public init(context: AccountContext, theme: PresentationTheme, items: [ReactionContextItem]) { self.theme = theme @@ -489,10 +488,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) - targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in - if let _ = self { - //strongSelf.hapticFeedback.tap() - } + targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in completedTarget = true intermediateCompletion() @@ -500,11 +496,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if hideNode { targetView.isHidden = false - /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/ - targetSnapshotView?.isHidden = true - targetScaleCompleted = true - intermediateCompletion() - //}) + targetSnapshotView?.isHidden = true + targetScaleCompleted = true + intermediateCompletion() } else { targetScaleCompleted = true intermediateCompletion() @@ -528,6 +522,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if itemNode.item.reaction.rawValue != value { continue } + + self.animationTargetView = targetView + self.animationHideNode = hideNode + + /*let standaloneReactionAnimation = StandaloneReactionAnimation() + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = self.bounds + self.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.animateReactionSelection(context: itemNode.context, theme: self.theme, reaction: itemNode.item, targetView: targetView, currentItemNode: itemNode, hideNode: hideNode, completion: completion) + + return*/ + if hideNode { targetView.isHidden = true } @@ -662,6 +668,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + public func cancelReactionAnimation() { + self.standaloneReactionAnimation?.cancel() + + if let animationTargetView = self.animationTargetView, self.animationHideNode { + animationTargetView.isHidden = false + } + } + public func setHighlightedReaction(_ value: ReactionContextItem.Reaction?) { self.highlightedReaction = value if let (size, insets, anchorRect) = self.validLayout { @@ -678,29 +692,47 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } public final class StandaloneReactionAnimation: ASDisplayNode { - private let itemNode: ReactionNode + private var itemNode: ReactionNode? = nil private let hapticFeedback = HapticFeedback() + private var isCancelled: Bool = false - public init(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem) { - self.itemNode = ReactionNode(context: context, theme: theme, item: reaction) - + private weak var targetView: UIView? + private var hideNode: Bool = false + + override public init() { super.init() self.isUserInteractionEnabled = false - - self.addSubnode(self.itemNode) } - public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + self.animateReactionSelection(context: context, theme: theme, reaction: reaction, targetView: targetView, currentItemNode: nil, hideNode: hideNode, completion: completion) + } + + func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem, targetView: UIView, currentItemNode: ReactionNode?, hideNode: Bool, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return } + + self.targetView = targetView + self.hideNode = hideNode + + let itemNode: ReactionNode + if let currentItemNode = currentItemNode { + itemNode = currentItemNode + } else { + itemNode = ReactionNode(context: context, theme: theme, item: reaction) + } + self.itemNode = itemNode + + self.addSubnode(itemNode) + if hideNode { targetView.isHidden = true } - self.itemNode.isExtracted = true + itemNode.isExtracted = true let sourceItemSize: CGFloat = 40.0 let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) @@ -732,7 +764,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) .offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) - additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.itemNode.context.account, resource: self.itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.itemNode.item.applicationAnimation.resource.id))) + additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id))) additionalAnimationNode.frame = animationFrame if incomingMessage { additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) @@ -750,9 +782,28 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } + var didBeginDismissAnimation = false + let beginDismissAnimation: () -> Void = { [weak self] in + if !didBeginDismissAnimation { + didBeginDismissAnimation = true + + guard let strongSelf = self else { + mainAnimationCompleted = true + intermediateCompletion() + return + } + strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { + mainAnimationCompleted = true + intermediateCompletion() + }) + } + } + additionalAnimationNode.completed = { _ in additionalAnimationCompleted = true intermediateCompletion() + + beginDismissAnimation() } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: { @@ -760,11 +811,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { }) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { - self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, hideNode: hideNode, completion: { - mainAnimationCompleted = true - intermediateCompletion() - }) + beginDismissAnimation() }) + } private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { @@ -795,9 +844,6 @@ public final class StandaloneReactionAnimation: ASDisplayNode { targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in - /*if let strongSelf = self { - strongSelf.hapticFeedback.tap() - }*/ completedTarget = true intermediateCompletion() @@ -805,11 +851,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if hideNode { targetView.isHidden = false - /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/ - targetSnapshotView?.isHidden = true - targetScaleCompleted = true - intermediateCompletion() - //}) + targetSnapshotView?.isHidden = true + targetScaleCompleted = true + intermediateCompletion() } else { targetScaleCompleted = true intermediateCompletion() @@ -823,6 +867,14 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: self, offset: -offset.y) } + + public func cancel() { + self.isCancelled = true + + if let targetView = self.targetView, self.hideNode { + targetView.isHidden = false + } + } } public final class StandaloneDismissReactionAnimation: ASDisplayNode { diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index ed105b9b1f..906d2f538b 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -132,6 +132,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { for reaction in availableReactions.reactions { if reaction.value == updatedReaction { if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.cancel() standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in standaloneReactionAnimation?.removeFromSupernode() }) @@ -139,19 +140,24 @@ class ReactionChatPreviewItemNode: ListViewItemNode { } if let supernode = self.supernode { - let standaloneReactionAnimation = StandaloneReactionAnimation(context: item.context, theme: item.theme, reaction: ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: reaction.value), - stillAnimation: reaction.selectAnimation, - listAnimation: reaction.activateAnimation, - applicationAnimation: reaction.effectAnimation - )) + let standaloneReactionAnimation = StandaloneReactionAnimation() self.standaloneReactionAnimation = standaloneReactionAnimation supernode.addSubnode(standaloneReactionAnimation) standaloneReactionAnimation.frame = supernode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation.animateReactionSelection( + context: item.context, theme: item.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + ), + targetView: targetView, + hideNode: true, + completion: { [weak standaloneReactionAnimation] in standaloneReactionAnimation?.removeFromSupernode() - }) + } + ) } break diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d65e57448a..90cdd71d0a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -429,12 +429,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var currentPinchController: PinchController? private weak var currentPinchSourceItemNode: ListViewItemNode? - private weak var currentReactionContextController: ContextController? - private weak var currentReactionContextItemNode: ListViewItemNode? - - private weak var currentStandaloneReactionAnimation: StandaloneReactionAnimation? - private weak var currentStandaloneReactionItemNode: ListViewItemNode? - private var screenCaptureManager: ScreenCaptureDetectionManager? private let chatAdditionalDataDisposable = MetaDisposable() @@ -594,6 +588,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return true } + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) { return false } @@ -1025,6 +1022,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), recognizer: recognizer, gesture: gesture) strongSelf.currentContextController = controller @@ -1059,8 +1058,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: updatedReaction) { - strongSelf.currentReactionContextController = controller - strongSelf.currentReactionContextItemNode = itemNode + strongSelf.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, completion: { [weak itemNode, weak targetView] in guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { @@ -1116,6 +1114,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, contentNode: sourceNode)), items: .single(items), recognizer: nil, gesture: gesture) dismissController = { [weak controller] completion in @@ -1238,21 +1238,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { for reaction in availableReactions.reactions { if reaction.value == updatedReaction { - let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: reaction.value), - stillAnimation: reaction.selectAnimation, - listAnimation: reaction.activateAnimation, - applicationAnimation: reaction.effectAnimation - )) + let standaloneReactionAnimation = StandaloneReactionAnimation() - strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation - strongSelf.currentStandaloneReactionItemNode = itemNode + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - }) + standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.presentationData.theme, + reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + ), + targetView: targetView, + hideNode: true, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) break } @@ -2471,6 +2477,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in @@ -2548,6 +2556,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G f(.dismissWithoutContent) }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in @@ -2981,6 +2991,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }) @@ -3287,6 +3299,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -4888,26 +4902,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if let reactionItemNode = strongSelf.currentReactionContextItemNode, let currentReactionContextController = strongSelf.currentReactionContextController { - if let itemNode = itemNode { - if itemNode === reactionItemNode { - currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } else { - currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } - - if let standaloneReactionItemNode = strongSelf.currentStandaloneReactionItemNode, let currentStandaloneReactionAnimation = strongSelf.currentStandaloneReactionAnimation { - if let itemNode = itemNode { - if itemNode === standaloneReactionItemNode { - currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } else { - currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } - strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } @@ -6271,6 +6265,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) contextController.dismissedForCancel = { [weak chatController] in @@ -7750,6 +7746,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }, joinGroupCall: { [weak self] activeCall in @@ -7812,6 +7811,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId), false)) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceNode: node, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { @@ -11998,6 +11999,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(id: .timestamp(timestamp), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index cf4d579b2c..92493c11a6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -5,6 +5,10 @@ import Display import ContextUI import AnimatedStickerNode import SwiftSignalKit +import ContextUI +import Postbox +import TelegramCore +import ReactionSelectionNode private final class OverlayTransitionContainerNode: ViewControllerTracingNode { override init() { @@ -595,6 +599,53 @@ public final class ChatMessageTransitionNode: ASDisplayNode { } } } + + private final class MessageReactionContext { + private(set) weak var itemNode: ListViewItemNode? + private(set) weak var contextController: ContextController? + private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation? + + var isEmpty: Bool { + return self.contextController == nil && self.standaloneReactionAnimation == nil + } + + init(itemNode: ListViewItemNode, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { + self.itemNode = itemNode + self.contextController = contextController + self.standaloneReactionAnimation = standaloneReactionAnimation + } + + func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { + guard let currentItemNode = self.itemNode else { + return + } + if itemNode == nil || itemNode === currentItemNode { + if let contextController = self.contextController { + contextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + } + } + + func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { + } + + func dismiss() { + if let contextController = self.contextController { + contextController.cancelReactionAnimation() + contextController.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + }) + } + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.cancel() + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + } + } + } private let listNode: ChatHistoryListNode private let getContentAreaInScreenSpace: () -> CGRect @@ -604,6 +655,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { private var animatingItemNodes: [AnimatingItemNode] = [] private var decorationItemNodes: [DecorationItemNode] = [] + private var messageReactionContexts: [MessageReactionContext] = [] var hasScheduledTransitions: Bool { return self.currentPendingItem != nil @@ -714,6 +766,58 @@ public final class ChatMessageTransitionNode: ASDisplayNode { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return nil } + + private func removeEmptyMessageReactionContexts() { + for i in (0 ..< self.messageReactionContexts.count).reversed() { + if self.messageReactionContexts[i].isEmpty { + self.messageReactionContexts.remove(at: i) + } + } + } + + func dismissMessageReactionContexts() { + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.dismiss() + } + self.messageReactionContexts.removeAll() + } + + func addMessageContextController(messageId: MessageId, contextController: ContextController) { + self.addMessageReactionContextContext(messageId: messageId, contextController: contextController, standaloneReactionAnimation: nil) + } + + func addMessageStandaloneReactionAnimation(messageId: MessageId, standaloneReactionAnimation: StandaloneReactionAnimation) { + self.addMessageReactionContextContext(messageId: messageId, contextController: nil, standaloneReactionAnimation: standaloneReactionAnimation) + } + + private func addMessageReactionContextContext(messageId: MessageId, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { + self.removeEmptyMessageReactionContexts() + + var messageItemNode: ListViewItemNode? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let item = itemNode.item { + for (message, _) in item.content { + if message.id == messageId { + messageItemNode = itemNode + break + } + } + } + } + } + + if let messageItemNode = messageItemNode { + for i in 0 ..< self.messageReactionContexts.count { + if self.messageReactionContexts[i].itemNode === messageItemNode { + self.messageReactionContexts[i].dismiss() + self.messageReactionContexts.remove(at: i) + break + } + } + self.messageReactionContexts.append(MessageReactionContext(itemNode: messageItemNode, contextController: contextController, standaloneReactionAnimation: standaloneReactionAnimation)) + } + } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { for animatingItemNode in self.animatingItemNodes { @@ -724,6 +828,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode { decorationItemNode.addExternalOffset(offset: offset, transition: transition) } } + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) + } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { @@ -735,6 +842,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode { decorationItemNode.addContentOffset(offset: offset) } } + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode) + } } func isAnimatingMessage(stableId: UInt32) -> Bool {