mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-07 08:01:10 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
74805bb045
@ -2,22 +2,7 @@ import Foundation
|
|||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
public struct ReactionGestureItemValue {
|
public enum ReactionGestureItem {
|
||||||
public var value: String
|
case reaction(value: String, text: String, file: TelegramMediaFile)
|
||||||
public var text: String
|
case reply
|
||||||
public var file: TelegramMediaFile
|
|
||||||
|
|
||||||
public init(value: String, text: String, file: TelegramMediaFile) {
|
|
||||||
self.value = value
|
|
||||||
self.text = text
|
|
||||||
self.file = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class ReactionGestureItem {
|
|
||||||
public let value: ReactionGestureItemValue
|
|
||||||
|
|
||||||
public init(value: ReactionGestureItemValue) {
|
|
||||||
self.value = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,7 @@ import Display
|
|||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
private let shadowBlur: CGFloat = 8.0
|
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
|
||||||
private let minimizedReactionSize: CGFloat = 30.0
|
|
||||||
private let maximizedReactionSize: CGFloat = 60.0
|
|
||||||
|
|
||||||
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat) -> UIImage? {
|
|
||||||
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
|
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.setFillColor(foreground.cgColor)
|
context.setFillColor(foreground.cgColor)
|
||||||
@ -17,7 +13,7 @@ private func generateBubbleImage(foreground: UIColor, diameter: CGFloat) -> UIIm
|
|||||||
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0))
|
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UIImage? {
|
private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
|
||||||
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
|
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.setFillColor(UIColor.white.cgColor)
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
@ -34,11 +30,12 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UI
|
|||||||
private final class ReactionNode: ASDisplayNode {
|
private final class ReactionNode: ASDisplayNode {
|
||||||
let reaction: ReactionGestureItem
|
let reaction: ReactionGestureItem
|
||||||
private let animationNode: AnimatedStickerNode
|
private let animationNode: AnimatedStickerNode
|
||||||
|
private let imageNode: ASImageNode
|
||||||
var isMaximized: Bool?
|
var isMaximized: Bool?
|
||||||
private let intrinsicSize: CGSize
|
private let intrinsicSize: CGSize
|
||||||
private let intrinsicOffset: CGPoint
|
private let intrinsicOffset: CGPoint
|
||||||
|
|
||||||
init(account: Account, reaction: ReactionGestureItem) {
|
init(account: Account, reaction: ReactionGestureItem, maximizedReactionSize: CGFloat) {
|
||||||
self.reaction = reaction
|
self.reaction = reaction
|
||||||
|
|
||||||
self.animationNode = AnimatedStickerNode()
|
self.animationNode = AnimatedStickerNode()
|
||||||
@ -47,18 +44,29 @@ private final class ReactionNode: ASDisplayNode {
|
|||||||
//self.animationNode.backgroundColor = .lightGray
|
//self.animationNode.backgroundColor = .lightGray
|
||||||
|
|
||||||
var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0)
|
var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0)
|
||||||
switch reaction.value.value {
|
|
||||||
case "😳":
|
self.imageNode = ASImageNode()
|
||||||
intrinsicSize.width += 8.0
|
switch reaction {
|
||||||
intrinsicSize.height += 8.0
|
case let .reaction(value, _, file):
|
||||||
self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0)
|
switch value {
|
||||||
case "👍":
|
case "😳":
|
||||||
intrinsicSize.width += 20.0
|
intrinsicSize.width += 8.0
|
||||||
intrinsicSize.height += 20.0
|
intrinsicSize.height += 8.0
|
||||||
self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0)
|
self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0)
|
||||||
default:
|
case "👍":
|
||||||
|
intrinsicSize.width += 20.0
|
||||||
|
intrinsicSize.height += 20.0
|
||||||
|
self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0)
|
||||||
|
default:
|
||||||
|
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
||||||
|
}
|
||||||
|
self.animationNode.visibility = true
|
||||||
|
self.animationNode.setup(account: account, resource: file.resource, width: Int(intrinsicSize.width) * 2, height: Int(intrinsicSize.height) * 2, mode: .direct)
|
||||||
|
case .reply:
|
||||||
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
||||||
|
self.imageNode.image = UIImage(named: "Chat/Context Menu/ReactionReply", in: Bundle(for: ReactionNode.self), compatibleWith: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.intrinsicSize = intrinsicSize
|
self.intrinsicSize = intrinsicSize
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@ -66,15 +74,17 @@ private final class ReactionNode: ASDisplayNode {
|
|||||||
//self.backgroundColor = .green
|
//self.backgroundColor = .green
|
||||||
|
|
||||||
self.addSubnode(self.animationNode)
|
self.addSubnode(self.animationNode)
|
||||||
self.animationNode.visibility = true
|
self.addSubnode(self.imageNode)
|
||||||
self.animationNode.setup(account: account, resource: reaction.value.file.resource, width: Int(self.intrinsicSize.width) * 2, height: Int(self.intrinsicSize.height) * 2, mode: .direct)
|
|
||||||
self.animationNode.updateLayout(size: self.intrinsicSize)
|
self.animationNode.updateLayout(size: self.intrinsicSize)
|
||||||
self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||||
|
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition) {
|
func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
transition.updatePosition(node: self.animationNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true)
|
transition.updatePosition(node: self.animationNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true)
|
||||||
transition.updateTransformScale(node: self.animationNode, scale: scale, beginWithCurrentState: true)
|
transition.updateTransformScale(node: self.animationNode, scale: scale, beginWithCurrentState: true)
|
||||||
|
transition.updatePosition(node: self.imageNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true)
|
||||||
|
transition.updateTransformScale(node: self.imageNode, scale: scale, beginWithCurrentState: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateIsAnimating(_ isAnimating: Bool, animated: Bool) {
|
func updateIsAnimating(_ isAnimating: Bool, animated: Bool) {
|
||||||
@ -87,43 +97,46 @@ private final class ReactionNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class ReactionSelectionNode: ASDisplayNode {
|
final class ReactionSelectionNode: ASDisplayNode {
|
||||||
|
private let account: Account
|
||||||
|
private let reactions: [ReactionGestureItem]
|
||||||
|
|
||||||
private let backgroundNode: ASImageNode
|
private let backgroundNode: ASImageNode
|
||||||
private let backgroundShadowNode: ASImageNode
|
private let backgroundShadowNode: ASImageNode
|
||||||
private let bubbleNodes: [(ASImageNode, ASImageNode)]
|
private let bubbleNodes: [(ASImageNode, ASImageNode)]
|
||||||
private let reactionNodes: [ReactionNode]
|
private var reactionNodes: [ReactionNode] = []
|
||||||
private var hasSelectedNode = false
|
private var hasSelectedNode = false
|
||||||
|
|
||||||
private let hapticFeedback = HapticFeedback()
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
|
private var shadowBlur: CGFloat = 8.0
|
||||||
|
private var minimizedReactionSize: CGFloat = 30.0
|
||||||
|
private var maximizedReactionSize: CGFloat = 60.0
|
||||||
|
private var smallCircleSize: CGFloat = 8.0
|
||||||
|
|
||||||
public init(account: Account, reactions: [ReactionGestureItem]) {
|
public init(account: Account, reactions: [ReactionGestureItem]) {
|
||||||
|
self.account = account
|
||||||
|
self.reactions = reactions
|
||||||
|
|
||||||
self.backgroundNode = ASImageNode()
|
self.backgroundNode = ASImageNode()
|
||||||
self.backgroundNode.displaysAsynchronously = false
|
self.backgroundNode.displaysAsynchronously = false
|
||||||
self.backgroundNode.displayWithoutProcessing = true
|
self.backgroundNode.displayWithoutProcessing = true
|
||||||
self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: 42.0)
|
|
||||||
|
|
||||||
self.backgroundShadowNode = ASImageNode()
|
self.backgroundShadowNode = ASImageNode()
|
||||||
self.backgroundShadowNode.displaysAsynchronously = false
|
self.backgroundShadowNode.displaysAsynchronously = false
|
||||||
self.backgroundShadowNode.displayWithoutProcessing = true
|
self.backgroundShadowNode.displayWithoutProcessing = true
|
||||||
self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: 42.0)
|
|
||||||
|
|
||||||
self.bubbleNodes = (0 ..< 2).map { i -> (ASImageNode, ASImageNode) in
|
self.bubbleNodes = (0 ..< 2).map { i -> (ASImageNode, ASImageNode) in
|
||||||
let imageNode = ASImageNode()
|
let imageNode = ASImageNode()
|
||||||
imageNode.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * 8.0)
|
|
||||||
imageNode.displaysAsynchronously = false
|
imageNode.displaysAsynchronously = false
|
||||||
imageNode.displayWithoutProcessing = true
|
imageNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
let shadowNode = ASImageNode()
|
let shadowNode = ASImageNode()
|
||||||
shadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * 8.0)
|
|
||||||
shadowNode.displaysAsynchronously = false
|
shadowNode.displaysAsynchronously = false
|
||||||
shadowNode.displayWithoutProcessing = true
|
shadowNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
return (imageNode, shadowNode)
|
return (imageNode, shadowNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.reactionNodes = reactions.map { reaction -> ReactionNode in
|
|
||||||
return ReactionNode(account: account, reaction: reaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.bubbleNodes.forEach { _, shadow in
|
self.bubbleNodes.forEach { _, shadow in
|
||||||
@ -134,18 +147,50 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
self.addSubnode(foreground)
|
self.addSubnode(foreground)
|
||||||
}
|
}
|
||||||
self.addSubnode(self.backgroundNode)
|
self.addSubnode(self.backgroundNode)
|
||||||
self.reactionNodes.forEach(self.addSubnode(_:))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) {
|
func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) {
|
||||||
let backgroundHeight: CGFloat = 42.0
|
let initialAnchorX = startingPoint.x
|
||||||
let reactionSpacing: CGFloat = 6.0
|
|
||||||
|
if isInitial && self.reactionNodes.isEmpty {
|
||||||
|
//let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing
|
||||||
|
|
||||||
|
//contentWidth = CGFloat(self.reactionNodes.count - 1) * X + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * 0.2 * X
|
||||||
|
// contentWidth - maximizedReactionSize = CGFloat(self.reactionNodes.count - 1) * X + CGFloat(self.reactionNodes.count + 1) * 0.2 * X
|
||||||
|
// (contentWidth - maximizedReactionSize) / (CGFloat(self.reactionNodes.count - 1) + CGFloat(self.reactionNodes.count + 1) * 0.2) = X
|
||||||
|
let availableContentWidth = max(100.0, initialAnchorX)
|
||||||
|
var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2)
|
||||||
|
minimizedReactionSize = max(16.0, floor(minimizedReactionSize))
|
||||||
|
minimizedReactionSize = min(30.0, minimizedReactionSize)
|
||||||
|
|
||||||
|
self.minimizedReactionSize = minimizedReactionSize
|
||||||
|
self.shadowBlur = floor(minimizedReactionSize * 0.26)
|
||||||
|
self.smallCircleSize = 8.0
|
||||||
|
|
||||||
|
let backgroundHeight = floor(minimizedReactionSize * 1.4)
|
||||||
|
|
||||||
|
self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: backgroundHeight, shadowBlur: self.shadowBlur)
|
||||||
|
self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: backgroundHeight, shadowBlur: self.shadowBlur)
|
||||||
|
for i in 0 ..< self.bubbleNodes.count {
|
||||||
|
self.bubbleNodes[i].0.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur)
|
||||||
|
self.bubbleNodes[i].1.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reactionNodes = self.reactions.map { reaction -> ReactionNode in
|
||||||
|
return ReactionNode(account: self.account, reaction: reaction, maximizedReactionSize: self.maximizedReactionSize)
|
||||||
|
}
|
||||||
|
self.reactionNodes.forEach(self.addSubnode(_:))
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.4)
|
||||||
|
|
||||||
|
let reactionSpacing: CGFloat = floor(self.minimizedReactionSize * 0.2)
|
||||||
let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0)
|
let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0)
|
||||||
|
|
||||||
let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing
|
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))
|
var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0))
|
||||||
backgroundFrame = backgroundFrame.offsetBy(dx: startingPoint.x - contentWidth + backgroundHeight / 2.0 - 52.0, dy: startingPoint.y - backgroundHeight - 16.0)
|
backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0)
|
||||||
|
|
||||||
self.backgroundNode.frame = backgroundFrame
|
self.backgroundNode.frame = backgroundFrame
|
||||||
self.backgroundShadowNode.frame = backgroundFrame
|
self.backgroundShadowNode.frame = backgroundFrame
|
||||||
@ -201,11 +246,11 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
reactionX += reactionSize + reactionSpacing
|
reactionX += reactionSize + reactionSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - 8.0 - shadowBlur, y: backgroundFrame.maxY - shadowBlur - 8.0 - shadowBlur), size: CGSize(width: 16.0 + shadowBlur * 2.0, height: 16.0 + shadowBlur * 2.0))
|
let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - self.smallCircleSize - shadowBlur, y: backgroundFrame.maxY - shadowBlur - self.smallCircleSize - shadowBlur), size: CGSize(width: self.smallCircleSize * 2.0 + shadowBlur * 2.0, height: self.smallCircleSize * 2.0 + shadowBlur * 2.0))
|
||||||
self.bubbleNodes[1].0.frame = mainBubbleFrame
|
self.bubbleNodes[1].0.frame = mainBubbleFrame
|
||||||
self.bubbleNodes[1].1.frame = mainBubbleFrame
|
self.bubbleNodes[1].1.frame = mainBubbleFrame
|
||||||
|
|
||||||
let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - 9.0 - (8.0 + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + 12.0 - (8.0 + shadowBlur * 2.0) / 2.0), size: CGSize(width: 8.0 + shadowBlur * 2.0, height: 8.0 + shadowBlur * 2.0))
|
let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - floor(self.smallCircleSize * 0.88) - (self.smallCircleSize + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + floor(self.smallCircleSize * 4.0 / 3.0) - (self.smallCircleSize + shadowBlur * 2.0) / 2.0), size: CGSize(width: self.smallCircleSize + shadowBlur * 2.0, height: self.smallCircleSize + shadowBlur * 2.0))
|
||||||
self.bubbleNodes[0].0.frame = secondaryBubbleFrame
|
self.bubbleNodes[0].0.frame = secondaryBubbleFrame
|
||||||
self.bubbleNodes[0].1.frame = secondaryBubbleFrame
|
self.bubbleNodes[0].1.frame = secondaryBubbleFrame
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,16 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
private var validatedGesture = false
|
private var validatedGesture = false
|
||||||
|
|
||||||
private var firstLocation: CGPoint = CGPoint()
|
private var firstLocation: CGPoint = CGPoint()
|
||||||
|
private var currentLocation: CGPoint = CGPoint()
|
||||||
private var currentReactions: [ReactionGestureItem] = []
|
private var currentReactions: [ReactionGestureItem] = []
|
||||||
private var isActivated = false
|
private var isActivated = false
|
||||||
private var isAwaitingCompletion = false
|
private var isAwaitingCompletion = false
|
||||||
private weak var currentContainer: ReactionSelectionParentNode?
|
private weak var currentContainer: ReactionSelectionParentNode?
|
||||||
|
private var activationTimer: Timer?
|
||||||
|
|
||||||
public var availableReactions: (() -> [ReactionGestureItem])?
|
public var availableReactions: (() -> [ReactionGestureItem])?
|
||||||
public var getReactionContainer: (() -> ReactionSelectionParentNode?)?
|
public var getReactionContainer: (() -> ReactionSelectionParentNode?)?
|
||||||
|
public var began: (() -> Void)?
|
||||||
public var updateOffset: ((CGFloat, Bool) -> Void)?
|
public var updateOffset: ((CGFloat, Bool) -> Void)?
|
||||||
public var completed: ((ReactionGestureItem?) -> Void)?
|
public var completed: ((ReactionGestureItem?) -> Void)?
|
||||||
public var displayReply: ((CGFloat) -> Void)?
|
public var displayReply: ((CGFloat) -> Void)?
|
||||||
@ -31,6 +34,8 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
self.currentReactions = []
|
self.currentReactions = []
|
||||||
self.isActivated = false
|
self.isActivated = false
|
||||||
self.isAwaitingCompletion = false
|
self.isAwaitingCompletion = false
|
||||||
|
self.activationTimer?.invalidate()
|
||||||
|
self.activationTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
@ -40,6 +45,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
self.currentReactions = availableReactions
|
self.currentReactions = availableReactions
|
||||||
let touch = touches.first!
|
let touch = touches.first!
|
||||||
self.firstLocation = touch.location(in: nil)
|
self.firstLocation = touch.location(in: nil)
|
||||||
|
self.currentLocation = self.firstLocation
|
||||||
} else {
|
} else {
|
||||||
self.state = .failed
|
self.state = .failed
|
||||||
}
|
}
|
||||||
@ -55,6 +61,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
guard let location = touches.first?.location(in: nil) else {
|
guard let location = touches.first?.location(in: nil) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.currentLocation = location
|
||||||
|
|
||||||
var 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)
|
||||||
|
|
||||||
@ -72,8 +79,38 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
self.validatedGesture = true
|
self.validatedGesture = true
|
||||||
self.firstLocation = location
|
self.firstLocation = location
|
||||||
translation = CGPoint()
|
translation = CGPoint()
|
||||||
|
self.began?()
|
||||||
self.updateOffset?(0.0, false)
|
self.updateOffset?(0.0, false)
|
||||||
updatedOffset = true
|
updatedOffset = true
|
||||||
|
|
||||||
|
self.activationTimer?.invalidate()
|
||||||
|
final class TimerTarget: NSObject {
|
||||||
|
let f: () -> Void
|
||||||
|
|
||||||
|
init(_ f: @escaping () -> Void) {
|
||||||
|
self.f = f
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func event() {
|
||||||
|
self.f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let activationTimer = Timer(timeInterval: 0.3, target: TimerTarget { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.activationTimer = nil
|
||||||
|
if strongSelf.validatedGesture {
|
||||||
|
let location = strongSelf.currentLocation
|
||||||
|
if !strongSelf.currentReactions.isEmpty, let reactionContainer = strongSelf.getReactionContainer?() {
|
||||||
|
strongSelf.currentContainer = reactionContainer
|
||||||
|
let reactionContainerLocation = reactionContainer.view.convert(location, from: nil)
|
||||||
|
reactionContainer.displayReactions(strongSelf.currentReactions, at: reactionContainerLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, selector: #selector(TimerTarget.event), userInfo: nil, repeats: false)
|
||||||
|
self.activationTimer = activationTimer
|
||||||
|
RunLoop.main.add(activationTimer, forMode: .common)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,11 +122,6 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
if absTranslationX > 40.0 {
|
if absTranslationX > 40.0 {
|
||||||
self.isActivated = true
|
self.isActivated = true
|
||||||
self.displayReply?(-min(0.0, translation.x))
|
self.displayReply?(-min(0.0, translation.x))
|
||||||
if !self.currentReactions.isEmpty, let reactionContainer = self.getReactionContainer?() {
|
|
||||||
self.currentContainer = reactionContainer
|
|
||||||
let reactionContainerLocation = reactionContainer.view.convert(location, from: nil)
|
|
||||||
reactionContainer.displayReactions(self.currentReactions, at: reactionContainerLocation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let reactionContainer = self.currentContainer {
|
if let reactionContainer = self.currentContainer {
|
||||||
@ -111,8 +143,8 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
if self.validatedGesture {
|
if self.validatedGesture {
|
||||||
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
|
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
|
||||||
if let reaction = self.currentContainer?.selectedReaction() {
|
if let reaction = self.currentContainer?.selectedReaction() {
|
||||||
self.completed?(reaction)
|
|
||||||
self.isAwaitingCompletion = true
|
self.isAwaitingCompletion = true
|
||||||
|
self.completed?(reaction)
|
||||||
} else {
|
} else {
|
||||||
if translation.x < -40.0 {
|
if translation.x < -40.0 {
|
||||||
self.currentContainer?.dismissReactions(into: nil, hideTarget: false)
|
self.currentContainer?.dismissReactions(into: nil, hideTarget: false)
|
||||||
|
22
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/Contents.json
vendored
Normal file
22
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "ReplyReaction@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "ReplyReaction@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@2x.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@3x.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReactionReply.imageset/ReplyReaction@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -418,9 +418,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !item.controllerInteraction.canSetupReply(item.message) {
|
|
||||||
//return []
|
|
||||||
}
|
|
||||||
|
|
||||||
let reactions: [(String, String)] = [
|
let reactions: [(String, String)] = [
|
||||||
("😒", "Sad"),
|
("😒", "Sad"),
|
||||||
@ -433,14 +430,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
var result: [ReactionGestureItem] = []
|
var result: [ReactionGestureItem] = []
|
||||||
for (value, text) in reactions {
|
for (value, text) in reactions {
|
||||||
if let file = item.associatedData.animatedEmojiStickers[value]?.file {
|
if let file = item.associatedData.animatedEmojiStickers[value]?.file {
|
||||||
result.append(ReactionGestureItem(value: ReactionGestureItemValue(value: value, text: text, file: file)))
|
result.append(.reaction(value: value, text: text, file: file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if item.controllerInteraction.canSetupReply(item.message) {
|
||||||
|
result.append(.reply)
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
reactionRecognizer.getReactionContainer = { [weak self] in
|
reactionRecognizer.getReactionContainer = { [weak self] in
|
||||||
return self?.item?.controllerInteraction.reactionContainerNode()
|
return self?.item?.controllerInteraction.reactionContainerNode()
|
||||||
}
|
}
|
||||||
|
reactionRecognizer.began = { [weak self] in
|
||||||
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.controllerInteraction.cancelInteractiveKeyboardGestures()
|
||||||
|
}
|
||||||
reactionRecognizer.updateOffset = { [weak self] offset, animated in
|
reactionRecognizer.updateOffset = { [weak self] offset, animated in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -479,11 +485,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
guard let strongSelf = self, let item = strongSelf.item else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strongSelf.swipeToReplyNode == nil {
|
if strongSelf.swipeToReplyFeedback == nil {
|
||||||
if strongSelf.swipeToReplyFeedback == nil {
|
strongSelf.swipeToReplyFeedback = HapticFeedback()
|
||||||
strongSelf.swipeToReplyFeedback = HapticFeedback()
|
}
|
||||||
}
|
strongSelf.swipeToReplyFeedback?.tap()
|
||||||
strongSelf.swipeToReplyFeedback?.tap()
|
if strongSelf.swipeToReplyNode == nil, false {
|
||||||
let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper))
|
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.swipeToReplyNode = swipeToReplyNode
|
||||||
strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||||
@ -497,8 +503,28 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let item = strongSelf.item, let reaction = reaction {
|
if let item = strongSelf.item, let reaction = reaction {
|
||||||
strongSelf.awaitingAppliedReaction = reaction.value.value
|
switch reaction {
|
||||||
item.controllerInteraction.updateMessageReaction(item.message.id, reaction.value.value)
|
case let .reaction(value, _, _):
|
||||||
|
strongSelf.awaitingAppliedReaction = value
|
||||||
|
item.controllerInteraction.updateMessageReaction(item.message.id, value)
|
||||||
|
case .reply:
|
||||||
|
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
|
||||||
|
var bounds = strongSelf.bounds
|
||||||
|
let offset = bounds.origin.x
|
||||||
|
bounds.origin.x = 0.0
|
||||||
|
strongSelf.bounds = bounds
|
||||||
|
if !offset.isZero {
|
||||||
|
strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
}
|
||||||
|
if let swipeToReplyNode = strongSelf.swipeToReplyNode {
|
||||||
|
strongSelf.swipeToReplyNode = nil
|
||||||
|
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
|
||||||
|
swipeToReplyNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
item.controllerInteraction.setupReply(item.message.id)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
|
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
|
||||||
var bounds = strongSelf.bounds
|
var bounds = strongSelf.bounds
|
||||||
|
Loading…
x
Reference in New Issue
Block a user