Improve reaction selection UI

This commit is contained in:
Ali 2019-11-29 16:15:12 +04:00
parent 46d7c6ac38
commit 9d3c7d45dd
24 changed files with 150 additions and 68 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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 = []
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}