mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Improve reaction selection UI
This commit is contained in:
parent
46d7c6ac38
commit
9d3c7d45dd
@ -920,7 +920,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: String, into targetNode: ASImageNode, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
func animateOutToReaction(value: String, into targetNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
guard let reactionContextNode = self.reactionContextNode else {
|
||||
self.animateOut(result: .default, completion: completion)
|
||||
return
|
||||
@ -1481,7 +1481,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
self.dismiss(result: .default, completion: completion)
|
||||
}
|
||||
|
||||
public func dismissWithReaction(value: String, into targetNode: ASImageNode, hideNode: Bool, completion: (() -> Void)?) {
|
||||
public func dismissWithReaction(value: String, into targetNode: ASDisplayNode, hideNode: Bool, completion: (() -> Void)?) {
|
||||
if !self.wasDismissed {
|
||||
self.wasDismissed = true
|
||||
self.controllerNode.animateOutToReaction(value: value, into: targetNode, hideNode: hideNode, completion: { [weak self] in
|
||||
|
@ -71,7 +71,7 @@ public extension CALayer {
|
||||
animation.isAdditive = additive
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = CACurrentMediaTime() + delay
|
||||
animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor()
|
||||
animation.fillMode = .both
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ public extension CALayer {
|
||||
}
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = CACurrentMediaTime() + delay
|
||||
animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor()
|
||||
animation.fillMode = .both
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@ public extension CALayer {
|
||||
}
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = CACurrentMediaTime() + delay
|
||||
animation.beginTime = CACurrentMediaTime() + delay * UIView.animationDurationFactor()
|
||||
animation.fillMode = .both
|
||||
}
|
||||
|
||||
|
@ -377,14 +377,12 @@ public final class ReactionContextNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func animateOutToReaction(value: String, targetNode: ASImageNode, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
public func animateOutToReaction(value: String, targetNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
for itemNode in self.itemNodes {
|
||||
switch itemNode.reaction {
|
||||
case let .reaction(itemValue, _, _):
|
||||
if itemValue == value {
|
||||
if let snapshotView = itemNode.view.snapshotContentTree(keepTransform: true) {
|
||||
let targetSnapshotView = UIImageView()
|
||||
targetSnapshotView.image = targetNode.image
|
||||
if let snapshotView = itemNode.view.snapshotContentTree(keepTransform: true), let targetSnapshotView = targetNode.view.snapshotContentTree() {
|
||||
targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view)
|
||||
itemNode.isHidden = true
|
||||
self.view.addSubview(targetSnapshotView)
|
||||
|
@ -255,7 +255,11 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.8)
|
||||
|
||||
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: floor((constrainedSize.width - contentWidth) / 2.0), dy: startingPoint.y - backgroundHeight - 12.0)
|
||||
if constrainedSize.width > 500.0 {
|
||||
backgroundFrame = backgroundFrame.offsetBy(dx: constrainedSize.width - contentWidth - 44.0, dy: startingPoint.y - backgroundHeight - 12.0)
|
||||
} else {
|
||||
backgroundFrame = backgroundFrame.offsetBy(dx: floor((constrainedSize.width - contentWidth) / 2.0), dy: startingPoint.y - backgroundHeight - 12.0)
|
||||
}
|
||||
backgroundFrame.origin.x = max(0.0, backgroundFrame.minX)
|
||||
backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX)
|
||||
|
||||
@ -383,9 +387,9 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
|
||||
let backgroundOffset: CGPoint
|
||||
if self.isRightAligned {
|
||||
backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0)
|
||||
backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: 10.0)
|
||||
} else {
|
||||
backgroundOffset = CGPoint(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0)
|
||||
backgroundOffset = CGPoint(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: 10.0)
|
||||
}
|
||||
let damping: CGFloat = 100.0
|
||||
|
||||
@ -393,14 +397,15 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1)
|
||||
var nodeOffset: CGPoint
|
||||
if self.isRightAligned {
|
||||
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 = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: 10.0)
|
||||
} else {
|
||||
nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur)
|
||||
nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: 10.0)
|
||||
}
|
||||
nodeOffset.x = -nodeOffset.x
|
||||
nodeOffset.x = 0.0
|
||||
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.28, 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)
|
||||
self.reactionNodes[i].layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: animationOffset * 0.1)
|
||||
self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping)
|
||||
//self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping, additive: true)
|
||||
}
|
||||
|
||||
self.backgroundNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping)
|
||||
@ -409,7 +414,7 @@ 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(into targetNode: ASImageNode?, hideTarget: Bool, completion: @escaping () -> Void) {
|
||||
func animateOut(into targetNode: ASDisplayNode?, hideTarget: Bool, completion: @escaping () -> Void) {
|
||||
self.hapticFeedback.prepareTap()
|
||||
|
||||
var completedContainer = false
|
||||
@ -424,9 +429,8 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
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
|
||||
targetNode.recursivelyEnsureDisplaySynchronously(true)
|
||||
if let snapshotView = self.reactionNodes[i].view.snapshotContentTree(), let targetSnapshotView = targetNode.view.snapshotContentTree() {
|
||||
targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view)
|
||||
self.reactionNodes[i].isHidden = true
|
||||
self.view.addSubview(targetSnapshotView)
|
||||
@ -485,7 +489,7 @@ final class ReactionSelectionNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
//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
|
||||
|
@ -47,7 +47,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
func dismissReactions(into targetNode: ASImageNode?, hideTarget: Bool) {
|
||||
func dismissReactions(into targetNode: ASDisplayNode?, hideTarget: Bool) {
|
||||
if let currentNode = self.currentNode {
|
||||
currentNode.animateOut(into: targetNode, hideTarget: hideTarget, completion: { [weak currentNode] in
|
||||
currentNode?.removeFromSupernode()
|
||||
|
@ -102,7 +102,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
}
|
||||
let elevate = self.shouldElevateAnchorPoint?() ?? false
|
||||
|
||||
let activationTimer = Timer(timeInterval: elevate ? 0.1 : 0.01, target: TimerTarget { [weak self] in
|
||||
let activationTimer = Timer(timeInterval: elevate ? 0.15 : 0.01, target: TimerTarget { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -178,7 +178,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
||||
}
|
||||
}
|
||||
|
||||
public func complete(into targetNode: ASImageNode?, hideTarget: Bool) {
|
||||
public func complete(into targetNode: ASDisplayNode?, hideTarget: Bool) {
|
||||
if self.isAwaitingCompletion {
|
||||
self.currentContainer?.dismissReactions(into: targetNode, hideTarget: hideTarget)
|
||||
self.state = .ended
|
||||
|
@ -567,7 +567,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
var reactionItems: [ReactionContextItem] = []
|
||||
|
||||
let reactions: [(String, String, String)] = [
|
||||
/*let reactions: [(String, String, String)] = [
|
||||
("😔", "Sad", "sad"),
|
||||
("😳", "Surprised", "surprised"),
|
||||
("😂", "Fun", "lol"),
|
||||
@ -586,7 +586,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let path = getAppBundle().path(forResource: name, ofType: "tgs") {
|
||||
reactionItems.append(ReactionContextItem(value: value, text: text, path: path))
|
||||
}
|
||||
}
|
||||
}*/
|
||||
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
|
||||
reactionItems = []
|
||||
}
|
||||
|
@ -98,7 +98,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 updateMessageReaction: (MessageId, String?) -> Void
|
||||
let openMessageReactions: (MessageId) -> Void
|
||||
let displaySwipeToReplyHint: () -> Void
|
||||
|
||||
@ -115,7 +115,7 @@ public final class ChatControllerInteraction {
|
||||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> 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, openTheme: @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, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> 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, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> 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, openTheme: @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, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
|
@ -1051,7 +1051,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
return self.contentImageNode?.playMediaWithSound()
|
||||
}
|
||||
|
||||
func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
||||
func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||
}
|
||||
|
||||
func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -522,8 +522,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
||||
if let item = strongSelf.item, let reaction = reaction {
|
||||
switch reaction {
|
||||
case let .reaction(value, _, _):
|
||||
strongSelf.awaitingAppliedReaction = (value, {})
|
||||
item.controllerInteraction.updateMessageReaction(item.message.id, value)
|
||||
var resolvedValue: String?
|
||||
if let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes), reactionsAttribute.reactions.contains(where: { $0.value == value }) {
|
||||
resolvedValue = nil
|
||||
} else {
|
||||
resolvedValue = value
|
||||
}
|
||||
strongSelf.awaitingAppliedReaction = (resolvedValue, {})
|
||||
item.controllerInteraction.updateMessageReaction(item.message.id, resolvedValue)
|
||||
case .reply:
|
||||
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
|
||||
var bounds = strongSelf.bounds
|
||||
@ -1994,13 +2000,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
||||
}
|
||||
|
||||
strongSelf.awaitingAppliedReaction = nil
|
||||
var targetNode: ASImageNode?
|
||||
var targetNode: ASDisplayNode?
|
||||
var hideTarget = false
|
||||
for contentNode in strongSelf.contentNodes {
|
||||
if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) {
|
||||
targetNode = reactionNode
|
||||
hideTarget = count == 1
|
||||
break
|
||||
if let awaitingAppliedReaction = awaitingAppliedReaction {
|
||||
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)
|
||||
@ -2853,7 +2861,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
||||
self.contextSourceNode.contentNode.addSubnode(accessoryItemNode)
|
||||
}
|
||||
|
||||
override func targetReactionNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func targetReactionNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
for contentNode in self.contentNodes {
|
||||
if let (reactionNode, count) = contentNode.reactionTargetNode(value: value) {
|
||||
return (reactionNode, count)
|
||||
|
@ -346,7 +346,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
|
||||
private let reactionCountFont = Font.semibold(11.0)
|
||||
|
||||
@ -42,26 +43,90 @@ enum ChatMessageDateAndStatusType: Equatable {
|
||||
case FreeOutgoing(ChatMessageDateAndStatusOutgoingType)
|
||||
}
|
||||
|
||||
private let reactionSize: CGFloat = 19.0
|
||||
private let reactionSize: CGFloat = 20.0
|
||||
private let reactionFont = Font.regular(12.0)
|
||||
|
||||
private final class StatusReactionNode: ASImageNode {
|
||||
private final class StatusReactionNodeParameters: NSObject {
|
||||
let value: String
|
||||
let previousValue: String?
|
||||
|
||||
init(value: String, previousValue: String?) {
|
||||
self.value = value
|
||||
self.previousValue = previousValue
|
||||
}
|
||||
}
|
||||
|
||||
private func drawReaction(context: CGContext, value: String, in rect: CGRect) {
|
||||
var fileId: Int?
|
||||
switch value {
|
||||
case "😔":
|
||||
fileId = 8
|
||||
case "😳":
|
||||
fileId = 19
|
||||
case "😂":
|
||||
fileId = 17
|
||||
case "👍":
|
||||
fileId = 6
|
||||
case "❤":
|
||||
fileId = 13
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let fileId = fileId, let path = getAppBundle().path(forResource: "simplereaction_\(fileId)@2x", ofType: "png"), let image = UIImage(contentsOfFile: path) {
|
||||
context.saveGState()
|
||||
context.translateBy(x: rect.midX, y: rect.midY)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -rect.midX, y: -rect.midY)
|
||||
context.draw(image.cgImage!, in: rect)
|
||||
context.restoreGState()
|
||||
} else {
|
||||
let string = NSAttributedString(string: value, font: reactionFont, textColor: .black)
|
||||
string.draw(at: CGPoint(x: rect.minX + 1.0, y: rect.minY + 3.0))
|
||||
}
|
||||
}
|
||||
|
||||
private final class StatusReactionNode: ASDisplayNode {
|
||||
let value: String
|
||||
var count: Int
|
||||
var previousValue: String? {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
init(value: String, count: Int) {
|
||||
init(value: String, count: Int, previousValue: String?) {
|
||||
self.value = value
|
||||
self.count = count
|
||||
self.previousValue = previousValue
|
||||
|
||||
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: reactionFont, textColor: .black)
|
||||
string.draw(at: CGPoint(x: 1.0, y: 3.0))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
self.isOpaque = false
|
||||
self.backgroundColor = nil
|
||||
}
|
||||
|
||||
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return StatusReactionNodeParameters(value: self.value, previousValue: self.previousValue)
|
||||
}
|
||||
|
||||
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
|
||||
if !isRasterizing {
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fill(bounds)
|
||||
}
|
||||
|
||||
guard let parameters = parameters as? StatusReactionNodeParameters else {
|
||||
return
|
||||
}
|
||||
drawReaction(context: context, value: parameters.value, in: bounds)
|
||||
if let previousValue = parameters.previousValue {
|
||||
let previousRect = bounds.offsetBy(dx: -14.0, dy: 0)
|
||||
context.setBlendMode(.destinationOut)
|
||||
drawReaction(context: context, value: previousValue, in: previousRect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -346,9 +411,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||
|
||||
let reactionSpacing: CGFloat = -4.0
|
||||
let reactionTrailingSpacing: CGFloat = 4.0
|
||||
var reactionInset: CGFloat = 0.0
|
||||
if !reactions.isEmpty {
|
||||
reactionInset = 5.0 + CGFloat(reactions.count) * reactionSize
|
||||
reactionInset = 5.0 + CGFloat(reactions.count) * reactionSize + CGFloat(reactions.count - 1) * reactionSpacing + reactionTrailingSpacing
|
||||
|
||||
var count = 0
|
||||
for reaction in reactions {
|
||||
@ -499,8 +566,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
if strongSelf.reactionNodes.count > i, strongSelf.reactionNodes[i].value == reactions[i].value {
|
||||
node = strongSelf.reactionNodes[i]
|
||||
node.count = Int(reactions[i].count)
|
||||
node.previousValue = i == 0 ? nil : reactions[i - 1].value
|
||||
} else {
|
||||
node = StatusReactionNode(value: reactions[i].value, count: Int(reactions[i].count))
|
||||
node = StatusReactionNode(value: reactions[i].value, count: Int(reactions[i].count), previousValue: i == 0 ? nil : reactions[i - 1].value)
|
||||
if strongSelf.reactionNodes.count > i {
|
||||
let previousNode = strongSelf.reactionNodes[i]
|
||||
if animated {
|
||||
@ -522,11 +590,15 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset - 3.0), size: CGSize(width: reactionSize, height: reactionSize))
|
||||
reactionOffset += reactionSize
|
||||
reactionOffset += reactionSize + reactionSpacing
|
||||
}
|
||||
if !reactions.isEmpty {
|
||||
reactionOffset += reactionTrailingSpacing
|
||||
}
|
||||
for _ in reactions.count ..< strongSelf.reactionNodes.count {
|
||||
let node = strongSelf.reactionNodes.removeLast()
|
||||
if animated {
|
||||
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
node?.removeFromSupernode()
|
||||
})
|
||||
@ -558,7 +630,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
if !strongSelf.reactionNodes.isEmpty {
|
||||
if false, !strongSelf.reactionNodes.isEmpty {
|
||||
if strongSelf.reactionButtonNode == nil {
|
||||
let reactionButtonNode = HighlightTrackingButtonNode()
|
||||
strongSelf.reactionButtonNode = reactionButtonNode
|
||||
@ -610,7 +682,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func reactionNode(value: String) -> (ASImageNode, Int)? {
|
||||
func reactionNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
for node in self.reactionNodes {
|
||||
if node.value == value {
|
||||
return (node, node.count)
|
||||
|
@ -119,7 +119,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
return self.interactiveFileNode.reactionTargetNode(value: value)
|
||||
}
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return self.contentNode.transitionNode(media: media)
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
return self.contentNode.reactionTargetNode(value: value)
|
||||
}
|
||||
}
|
||||
|
@ -947,7 +947,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.playerUpdateTimer = nil
|
||||
}
|
||||
|
||||
func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return self.contentNode.transitionNode(media: media)
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
return self.contentNode.reactionTargetNode(value: value)
|
||||
}
|
||||
}
|
||||
|
@ -634,7 +634,7 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
var item: ChatMessageItem?
|
||||
var accessibilityData: ChatMessageAccessibilityData?
|
||||
|
||||
var awaitingAppliedReaction: (String, () -> Void)?
|
||||
var awaitingAppliedReaction: (String?, () -> Void)?
|
||||
|
||||
public required convenience init() {
|
||||
self.init(layerBacked: false)
|
||||
@ -814,7 +814,7 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
func targetReactionNode(value: String) -> (ASImageNode, Int)? {
|
||||
func targetReactionNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -469,7 +469,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -386,7 +386,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return false
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -955,7 +955,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -579,7 +579,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
if !self.statusNode.isHidden {
|
||||
return self.statusNode.reactionNode(value: value)
|
||||
}
|
||||
|
@ -511,7 +511,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) })
|
||||
}
|
||||
|
||||
override func reactionTargetNode(value: String) -> (ASImageNode, Int)? {
|
||||
override func reactionTargetNode(value: String) -> (ASDisplayNode, Int)? {
|
||||
return self.contentNode.reactionTargetNode(value: value)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user