diff --git a/submodules/Display/Display/CAAnimationUtils.swift b/submodules/Display/Display/CAAnimationUtils.swift index 7a0df6e933..81cf94009a 100644 --- a/submodules/Display/Display/CAAnimationUtils.swift +++ b/submodules/Display/Display/CAAnimationUtils.swift @@ -130,7 +130,7 @@ public extension CALayer { self.add(animationGroup, forKey: key) } - public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { let k = Float(UIView.animationDurationFactor()) var speed: Float = 1.0 if k != 0 && k != 1 { @@ -152,6 +152,7 @@ public extension CALayer { animation.keyTimes = keyTimes animation.speed = speed animation.duration = duration + animation.isAdditive = additive if let completion = completion { animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index e46aa7ca51..a7590bff40 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -32,7 +32,7 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UI } private final class ReactionNode: ASDisplayNode { - private let reaction: ReactionGestureItem + let reaction: ReactionGestureItem private let animationNode: AnimatedStickerNode var isMaximized: Bool? private let intrinsicSize: CGSize @@ -91,6 +91,9 @@ final class ReactionSelectionNode: ASDisplayNode { private let backgroundShadowNode: ASImageNode private let bubbleNodes: [(ASImageNode, ASImageNode)] private let reactionNodes: [ReactionNode] + private var hasSelectedNode = false + + private let hapticFeedback = HapticFeedback() public init(account: Account, reactions: [ReactionGestureItem]) { self.backgroundNode = ASImageNode() @@ -142,7 +145,7 @@ final class ReactionSelectionNode: ASDisplayNode { 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, dy: startingPoint.y - backgroundHeight - 16.0) + backgroundFrame = backgroundFrame.offsetBy(dx: startingPoint.x - contentWidth + backgroundHeight / 2.0 - 52.0, dy: startingPoint.y - backgroundHeight - 16.0) self.backgroundNode.frame = backgroundFrame self.backgroundShadowNode.frame = backgroundFrame @@ -152,8 +155,15 @@ final class ReactionSelectionNode: ASDisplayNode { let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart)) var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing + if offsetFromStart > backgroundFrame.maxX - shadowBlur { + self.hasSelectedNode = false + } else { + self.hasSelectedNode = true + } + var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count)) maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex)) + for i in 0 ..< self.reactionNodes.count { let isMaximized = i == maximizedIndex @@ -174,6 +184,9 @@ final class ReactionSelectionNode: ASDisplayNode { if self.reactionNodes[i].isMaximized != isMaximized { self.reactionNodes[i].isMaximized = isMaximized self.reactionNodes[i].updateIsAnimating(isMaximized, animated: !isInitial) + if isMaximized { + self.hapticFeedback.tap() + } } var reactionFrame = CGRect(origin: CGPoint(x: reactionX, y: backgroundFrame.maxY - shadowBlur - minimizedReactionVerticalInset - reactionSize), size: CGSize(width: reactionSize, height: reactionSize)) @@ -209,8 +222,10 @@ final class ReactionSelectionNode: ASDisplayNode { for i in 0 ..< self.reactionNodes.count { let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1) - let nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur) - self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.05, initialVelocity: 0.0, damping: damping) + var nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur) + nodeOffset.x = -nodeOffset.x + nodeOffset.y = 30.0 + self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.08, initialVelocity: 0.0, damping: damping) self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) } @@ -220,7 +235,110 @@ final class ReactionSelectionNode: ASDisplayNode { self.backgroundShadowNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) } - func animateOut(completion: @escaping () -> Void) { - completion() + func animateOut(into targetNode: ASImageNode?, hideTarget: Bool, completion: @escaping () -> Void) { + self.hapticFeedback.prepareTap() + + var completedContainer = false + var completedTarget = true + + let intermediateCompletion: () -> Void = { + if completedContainer && completedTarget { + completion() + } + } + + if let targetNode = targetNode { + for i in 0 ..< self.reactionNodes.count { + if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { + if let snapshotView = self.reactionNodes[i].view.snapshotContentTree() { + let targetSnapshotView = UIImageView() + targetSnapshotView.image = targetNode.image + targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view) + self.reactionNodes[i].isHidden = true + self.view.addSubview(targetSnapshotView) + self.view.addSubview(snapshotView) + completedTarget = false + let targetPosition = self.view.convert(targetNode.bounds.center, from: targetNode.view) + let duration: Double = 0.3 + if hideTarget { + targetNode.isHidden = true + } + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + targetSnapshotView.layer.animateScale(from: snapshotView.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: 0.3, removeOnCompletion: false) + + + let sourcePoint = snapshotView.center + let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - 30.0) + + let x1 = sourcePoint.x + let y1 = sourcePoint.y + let x2 = midPoint.x + let y2 = midPoint.y + let x3 = targetPosition.x + let y3 = targetPosition.y + + let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + + var keyframes: [AnyObject] = [] + for i in 0 ..< 10 { + let k = CGFloat(i) / CGFloat(10 - 1) + let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k + let y = a * x * x + b * x + c + keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) + } + + snapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.hapticFeedback.tap() + } + completedTarget = true + if hideTarget { + targetNode.isHidden = false + targetNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0) + } + intermediateCompletion() + }) + targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false) + + snapshotView.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / snapshotView.bounds.width, duration: 0.3, removeOnCompletion: false) + } + break + } + } + } + + self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + self.backgroundShadowNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.backgroundShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completedContainer = true + intermediateCompletion() + }) + for (node, shadow) in self.bubbleNodes { + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + shadow.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + shadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + for i in 0 ..< self.reactionNodes.count { + self.reactionNodes[i].layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + self.reactionNodes[i].layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + } + + func selectedReaction() -> ReactionGestureItem? { + if !self.hasSelectedNode { + return nil + } + for i in 0 ..< self.reactionNodes.count { + if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { + return self.reactionNodes[i].reaction + } + } + return nil } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift index 547cdee6c2..8a0f78779a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift @@ -36,9 +36,16 @@ public final class ReactionSelectionParentNode: ASDisplayNode { } } - func dismissReactions() { + func selectedReaction() -> ReactionGestureItem? { if let currentNode = self.currentNode { - currentNode.animateOut(completion: { [weak currentNode] in + return currentNode.selectedReaction() + } + return nil + } + + func dismissReactions(into targetNode: ASImageNode?, hideTarget: Bool) { + if let currentNode = self.currentNode { + currentNode.animateOut(into: targetNode, hideTarget: hideTarget, completion: { [weak currentNode] in currentNode?.removeFromSupernode() }) self.currentNode = nil diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift index 23e40513ff..61b67da170 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift @@ -8,12 +8,15 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { private var firstLocation: CGPoint = CGPoint() private var currentReactions: [ReactionGestureItem] = [] private var isActivated = false + private var isAwaitingCompletion = false private weak var currentContainer: ReactionSelectionParentNode? public var availableReactions: (() -> [ReactionGestureItem])? public var getReactionContainer: (() -> ReactionSelectionParentNode?)? public var updateOffset: ((CGFloat, Bool) -> Void)? - public var completed: (() -> Void)? + public var completed: ((ReactionGestureItem?) -> Void)? + public var displayReply: ((CGFloat) -> Void)? + public var activateReply: (() -> Void)? override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) @@ -27,6 +30,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.validatedGesture = false self.currentReactions = [] self.isActivated = false + self.isAwaitingCompletion = false } override public func touchesBegan(_ touches: Set, with event: UIEvent) { @@ -42,6 +46,9 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { } override public func touchesMoved(_ touches: Set, with event: UIEvent) { + if self.isAwaitingCompletion { + return + } guard let _ = self.view else { return } @@ -49,7 +56,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { return } - let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) + var translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) let absTranslationX: CGFloat = abs(translation.x) let absTranslationY: CGFloat = abs(translation.y) @@ -63,7 +70,9 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { self.state = .failed } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { self.validatedGesture = true - self.updateOffset?(translation.x, true) + self.firstLocation = location + translation = CGPoint() + self.updateOffset?(0.0, false) updatedOffset = true } } @@ -75,6 +84,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { if !self.isActivated { 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) @@ -92,12 +102,40 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { } override public func touchesEnded(_ touches: Set, with event: UIEvent) { - if self.validatedGesture { - self.completed?() + if self.isAwaitingCompletion { + return + } + guard let location = touches.first?.location(in: nil) else { + return + } + 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 + } else { + if translation.x < -40.0 { + self.currentContainer?.dismissReactions(into: nil, hideTarget: false) + self.activateReply?() + self.state = .ended + } else { + self.currentContainer?.dismissReactions(into: nil, hideTarget: false) + self.completed?(nil) + self.state = .cancelled + super.touchesEnded(touches, with: event) + } + } + } else { + self.currentContainer?.dismissReactions(into: nil, hideTarget: false) + self.state = .cancelled + super.touchesEnded(touches, with: event) + } + } + + public func complete(into targetNode: ASImageNode?, hideTarget: Bool) { + if self.isAwaitingCompletion { + self.currentContainer?.dismissReactions(into: targetNode, hideTarget: hideTarget) + self.state = .ended } - self.currentContainer?.dismissReactions() - self.state = .ended - - super.touchesEnded(touches, with: event) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index b6386235a7..9f422aa62e 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -1531,6 +1531,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G window.rootViewController?.present(controller, animated: true) } } + }, updateMessageReaction: { [weak self] messageId, reaction in + guard let strongSelf = self else { + return + } + let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, reactions: [reaction]).start() }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift index 3f20eaa0fc..c36a16e51d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift @@ -94,6 +94,7 @@ public final class ChatControllerInteraction { let sendScheduledMessagesNow: ([MessageId]) -> Void let editScheduledMessagesTime: ([MessageId]) -> Void let performTextSelectionAction: (UInt32, String, TextSelectionAction) -> Void + let updateMessageReaction: (MessageId, String) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -108,7 +109,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: String? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -156,6 +157,7 @@ public final class ChatControllerInteraction { self.sendScheduledMessagesNow = sendScheduledMessagesNow self.editScheduledMessagesTime = editScheduledMessagesTime self.performTextSelectionAction = performTextSelectionAction + self.updateMessageReaction = updateMessageReaction self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -190,6 +192,7 @@ public final class ChatControllerInteraction { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in + }, updateMessageReaction: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index 94612b26b5..9c1ba28f25 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -462,9 +462,22 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, false, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal, reactionCount: dateReactionCount) + + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, false, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions) var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift index 90caf793b9..9aa176ad01 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAttachedContentNode.swift @@ -314,7 +314,20 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount) var webpageGalleryMediaCount: Int? for media in message.media { @@ -534,7 +547,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize) + statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions) } default: break @@ -1023,4 +1036,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.contentImageNode?.playMediaWithSound() } + + func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + if !self.statusNode.isHidden { + return self.statusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift index bf0d688081..9c64baca75 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift @@ -172,4 +172,8 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func updateIsExtractedToContextPreview(_ value: Bool) { } + + func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift index 9d9ae21399..56be379212 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift @@ -179,6 +179,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var appliedForwardInfo: (Peer?, String?)? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? + private var reactionRecognizer: ReactionSwipeGestureRecognizer? + + private var awaitingAppliedReaction: String? override var visibility: ListViewItemNodeVisibility { didSet { @@ -391,6 +394,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.view.addGestureRecognizer(replyRecognizer)*/ let reactionRecognizer = ReactionSwipeGestureRecognizer(target: nil, action: nil) + self.reactionRecognizer = reactionRecognizer reactionRecognizer.availableReactions = { [weak self] in guard let strongSelf = self, let item = strongSelf.item else { return [] @@ -410,7 +414,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } if !item.controllerInteraction.canSetupReply(item.message) { - return [] + //return [] } let reactions: [(String, String)] = [ @@ -442,9 +446,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if animated { strongSelf.layer.animateBoundsOriginXAdditive(from: -offset, to: 0.0, duration: 0.1, mediaTimingFunction: CAMediaTimingFunction(name: .easeOut)) } + if let swipeToReplyNode = strongSelf.swipeToReplyNode { + swipeToReplyNode.alpha = max(0.0, min(1.0, abs(offset / 40.0))) + } } - reactionRecognizer.completed = { [weak self] in - guard let strongSelf = self else { + reactionRecognizer.activateReply = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item else { return } var bounds = strongSelf.bounds @@ -454,6 +461,56 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { 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) + } + reactionRecognizer.displayReply = { [weak self] offset in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + if strongSelf.swipeToReplyNode == nil { + if strongSelf.swipeToReplyFeedback == nil { + strongSelf.swipeToReplyFeedback = HapticFeedback() + } + strongSelf.swipeToReplyFeedback?.tap() + 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) + swipeToReplyNode.frame = CGRect(origin: CGPoint(x: strongSelf.bounds.size.width, y: floor((strongSelf.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) + swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } + } + reactionRecognizer.completed = { [weak self] reaction in + guard let strongSelf = self else { + return + } + if let item = strongSelf.item, let reaction = reaction { + strongSelf.awaitingAppliedReaction = reaction.value.value + item.controllerInteraction.updateMessageReaction(item.message.id, reaction.value.value) + } else { + 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) + } + } } self.view.addGestureRecognizer(reactionRecognizer) } @@ -512,7 +569,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), - mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), + mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction]) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), currentShareButtonNode: HighlightableButtonNode?, layoutConstants: ChatMessageItemLayoutConstants, currentItem: ChatMessageItem?, @@ -956,7 +1013,20 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) let statusType: ChatMessageDateAndStatusType if message.effectivelyIncoming(item.context.account.peerId) { @@ -971,7 +1041,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude)) + mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions) } } @@ -1843,6 +1913,35 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } strongSelf.updateSearchTextHighlightState() + + if let awaitingAppliedReaction = strongSelf.awaitingAppliedReaction { + 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) + } + + strongSelf.awaitingAppliedReaction = nil + var targetNode: ASImageNode? + var hideTarget = false + for contentNode in strongSelf.contentNodes { + if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) { + targetNode = reactionNode + hideTarget = count == 1 + break + } + } + strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget) + } } override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift index bf4d8183b6..387be91901 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageCallBubbleContentNode.swift @@ -128,7 +128,7 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { buttonImage = PresentationResourcesChat.chatBubbleOutgoingCallButtonImage(item.presentationData.theme.theme) } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: 0) let statusText: String if let callDuration = callDuration, callDuration > 1 { @@ -214,4 +214,8 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { return .none } } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift index 5ba0d0481c..db94480bd5 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -149,7 +149,20 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) let statusType: ChatMessageDateAndStatusType? switch position { @@ -173,7 +186,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude)) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions) statusSize = size statusApply = apply } @@ -330,4 +343,11 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let _ = item.controllerInteraction.openMessage(item.message, .default) } } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + if !self.dateAndStatusNode.isHidden { + return self.dateAndStatusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift index d78286a8ad..e8b3639af7 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit import Postbox +import TelegramCore import Display import SwiftSignalKit import TelegramPresentationData @@ -104,6 +105,28 @@ enum ChatMessageDateAndStatusType: Equatable { } } +private let reactionSize: CGFloat = 18.0 + +private final class StatusReactionNode: ASImageNode { + let value: String + var count: Int + + init(value: String, count: Int) { + self.value = value + self.count = count + + super.init() + + self.image = generateImage(CGSize(width: reactionSize, height: reactionSize), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + let string = NSAttributedString(string: value, font: Font.regular(11.0), textColor: .black) + string.draw(at: CGPoint(x: 1.0, y: 3.0)) + UIGraphicsPopContext() + }) + } +} + class ChatMessageDateAndStatusNode: ASDisplayNode { private var backgroundNode: ASImageNode? private var checkSentNode: ASImageNode? @@ -112,6 +135,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private var clockMinNode: ASImageNode? private let dateNode: TextNode private var impressionIcon: ASImageNode? + private var reactionNodes: [StatusReactionNode] = [] private var type: ChatMessageDateAndStatusType? private var theme: ChatPresentationThemeData? @@ -128,7 +152,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { self.addSubnode(self.dateNode) } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction]) -> (CGSize, (Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) var checkReadNode = self.checkReadNode @@ -142,11 +166,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let currentType = self.type let currentTheme = self.theme - return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize in + return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions in let dateColor: UIColor var backgroundImage: UIImage? var outgoingStatus: ChatMessageDateAndStatusOutgoingType? - let leftInset: CGFloat + var leftInset: CGFloat let loadedCheckFullImage: UIImage? let loadedCheckPartialImage: UIImage? @@ -372,6 +396,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0) } + var reactionInset: CGFloat = 0.0 + if !reactions.isEmpty { + reactionInset = 1.0 + CGFloat(reactions.count) * reactionSize + } + leftInset += reactionInset + let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom) return (layoutSize, { [weak self] animated in @@ -423,7 +453,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } else if themeUpdated { clockFrameNode.image = clockFrameImage } - clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y) + clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y) if let clockFrameNode = strongSelf.clockFrameNode { maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0) } @@ -440,7 +470,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } else if themeUpdated { clockMinNode.image = clockMinImage } - clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x, y: backgroundInsets.top + clockPosition.y) + clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y) if let clockMinNode = strongSelf.clockMinNode { maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0) } @@ -465,7 +495,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { animateSentNode = animated } checkSentNode.isHidden = false - checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top) + checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top) } else { checkSentNode.isHidden = true } @@ -485,7 +515,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { animateReadNode = animated } checkReadNode.isHidden = false - checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left, dy: backgroundInsets.top) + checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top) } else { checkReadNode.isHidden = true } @@ -502,22 +532,47 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.checkSentNode = nil strongSelf.checkReadNode = nil } + + var reactionOffset: CGFloat = leftInset - reactionInset + backgroundInsets.left + for i in 0 ..< reactions.count { + let node: StatusReactionNode + if strongSelf.reactionNodes.count > i, strongSelf.reactionNodes[i].value == reactions[i].value { + node = strongSelf.reactionNodes[i] + node.count = Int(reactions[i].count) + } else { + node = StatusReactionNode(value: reactions[i].value, count: Int(reactions[i].count)) + if strongSelf.reactionNodes.count > i { + strongSelf.reactionNodes[i].removeFromSupernode() + strongSelf.reactionNodes[i] = node + } else { + strongSelf.reactionNodes.append(node) + } + } + if node.supernode == nil { + strongSelf.addSubnode(node) + } + node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + 1.0 + offset - 3.0), size: CGSize(width: reactionSize, height: reactionSize)) + reactionOffset += reactionSize + } + for _ in reactions.count ..< strongSelf.reactionNodes.count { + strongSelf.reactionNodes.removeLast().removeFromSupernode() + } } }) } } - static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { + static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction]) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { let currentLayout = node?.asyncLayout() - return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize in + return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions in let resultNode: ChatMessageDateAndStatusNode let resultSizeAndApply: (CGSize, (Bool) -> Void) if let node = node, let currentLayout = currentLayout { resultNode = node - resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize) + resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions) } else { resultNode = ChatMessageDateAndStatusNode() - resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize) + resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions) } return (resultSizeAndApply.0, { animated in @@ -526,4 +581,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { }) } } + + func reactionNode(value: String) -> (ASImageNode, Int)? { + for node in self.reactionNodes { + if node.value == value { + return (node, node.count) + } + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift index b63e8b2f87..c82890b1ed 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -117,4 +117,8 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + return self.interactiveFileNode.reactionTargetNode(value: value) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift index 4df8507a01..74edfa23c4 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -132,4 +132,8 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { } return self.contentNode.transitionNode(media: media) } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + return self.contentNode.reactionTargetNode(value: value) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift index fa5055e1f7..9ff1088523 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -276,9 +276,22 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } - let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize) + let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, reactionCount: dateReactionCount) + + let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions) statusSize = size statusApply = apply } @@ -927,4 +940,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.playerUpdateTimer?.invalidate() self.playerUpdateTimer = nil } + + func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + if !self.dateAndStatusNode.isHidden { + return self.dateAndStatusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift index 8f5fe2d7bb..8358db5816 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift @@ -246,23 +246,31 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - let edited = false + var edited = false let sentViaBot = false var viewCount: Int? = nil for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute { - // edited = true + if let attribute = attribute as? EditedMessageAttribute { + edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count - }// else if let _ = attribute as? InlineBotMessageAttribute { - // sentViaBot = true - // } + } } - // if let author = item.message.author as? TelegramUser, author.botInfo != nil { - // sentViaBot = true - // } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount) let maxDateAndStatusWidth: CGFloat if case .bubble = statusDisplayType { @@ -270,7 +278,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else { maxDateAndStatusWidth = width - videoFrame.midX - 85.0 } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude)) + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions) var contentSize = imageSize var dateAndStatusOverflow = false diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index 3cda6cfff0..9451371c86 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -135,4 +135,8 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } return self.contentNode.transitionNode(media: media) } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + return self.contentNode.reactionTargetNode(value: value) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift index 275180a4a8..e784f9d292 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -181,13 +181,26 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + if let selectedMedia = selectedMedia { if selectedMedia.liveBroadcastingTimeout != nil { edited = false } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) let statusType: ChatMessageDateAndStatusType? switch position { @@ -225,7 +238,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude)) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions) statusSize = size statusApply = apply } @@ -455,4 +468,11 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + if !self.dateAndStatusNode.isHidden { + return self.dateAndStatusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift index dee7f10b5d..2443bbb718 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -164,7 +164,20 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) let statusType: ChatMessageDateAndStatusType? switch position { @@ -192,7 +205,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude)) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: imageSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateReactions) statusSize = size statusApply = apply } @@ -359,4 +372,11 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return false } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + if !self.dateAndStatusNode.isHidden { + return self.dateAndStatusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift index 27cf7b4aa9..76b54dc82b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift @@ -596,7 +596,20 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) let statusType: ChatMessageDateAndStatusType? switch position { @@ -620,7 +633,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions) statusSize = size statusApply = apply } @@ -942,4 +955,11 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { return .none } } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + if !self.statusNode.isHidden { + return self.statusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift index 9a78830bf5..2e9eb7d99c 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageRestrictedBubbleContentNode.swift @@ -59,7 +59,20 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) let statusType: ChatMessageDateAndStatusType? switch position { @@ -83,7 +96,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions) statusSize = size statusApply = apply } @@ -95,7 +108,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { let textFont = item.presentationData.messageFont let forceStatusNewline = false - let attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont) + let attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor.withAlphaComponent(0.7), linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont) var cutout: TextNodeCutout? if let statusSize = statusSize, !forceStatusNewline { @@ -230,4 +243,11 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } + + override func reactionTargetNode(value: String) -> (ASImageNode, Int)? { + if !self.statusNode.isHidden { + return self.statusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift index eef28e8174..262e137a64 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageStickerItemNode.swift @@ -265,9 +265,22 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude)) + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, reactionCount: dateReactionCount) + + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions) var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift index 4de4f8bcbb..2168a835ff 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -106,7 +106,20 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings) + var dateReactions: [MessageReaction] = [] + var dateReactionCount = 0 + if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), !reactionsAttribute.reactions.isEmpty { + for reaction in reactionsAttribute.reactions { + if reaction.isSelected { + dateReactions.insert(reaction, at: 0) + } else { + dateReactions.append(reaction) + } + dateReactionCount += Int(reaction.count) + } + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, reactionCount: dateReactionCount) let statusType: ChatMessageDateAndStatusType? switch position { @@ -130,7 +143,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize) + let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions) statusSize = size statusApply = apply } @@ -158,20 +171,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { rawText = item.presentationData.strings.Conversation_UnsupportedMediaPlaceholder messageEntities = [MessageTextEntity(range: 0.. (ASImageNode, Int)? { + if !self.statusNode.isHidden { + return self.statusNode.reactionNode(value: value) + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift index 059c5cc92b..65c1109da2 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift @@ -410,6 +410,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in + }, updateMessageReaction: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift index 15bc05eb88..76554ee22c 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift @@ -112,6 +112,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in + }, updateMessageReaction: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index 04d64427e7..f44edfbf6a 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -286,6 +286,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, sendScheduledMessagesNow: { _ in }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in + }, updateMessageReaction: { _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift index 8b198fe8a9..30d0fe4a60 100644 --- a/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift @@ -11,7 +11,7 @@ enum MessageTimestampStatusFormat { case minimal } -func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular) -> String { +func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, reactionCount: Int) -> String { let timestamp: Int32 if let scheduleTime = message.scheduleTime { timestamp = scheduleTime