Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
overtake 2019-08-20 19:05:39 +03:00
commit 74805bb045
7 changed files with 179 additions and 69 deletions

View File

@ -2,22 +2,7 @@ import Foundation
import Postbox
import TelegramCore
public struct ReactionGestureItemValue {
public var value: String
public var text: String
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
}
public enum ReactionGestureItem {
case reaction(value: String, text: String, file: TelegramMediaFile)
case reply
}

View File

@ -5,11 +5,7 @@ import Display
import Postbox
import TelegramCore
private let shadowBlur: CGFloat = 8.0
private let minimizedReactionSize: CGFloat = 30.0
private let maximizedReactionSize: CGFloat = 60.0
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat) -> UIImage? {
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
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.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))
}
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
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
@ -34,11 +30,12 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UI
private final class ReactionNode: ASDisplayNode {
let reaction: ReactionGestureItem
private let animationNode: AnimatedStickerNode
private let imageNode: ASImageNode
var isMaximized: Bool?
private let intrinsicSize: CGSize
private let intrinsicOffset: CGPoint
init(account: Account, reaction: ReactionGestureItem) {
init(account: Account, reaction: ReactionGestureItem, maximizedReactionSize: CGFloat) {
self.reaction = reaction
self.animationNode = AnimatedStickerNode()
@ -47,18 +44,29 @@ private final class ReactionNode: ASDisplayNode {
//self.animationNode.backgroundColor = .lightGray
var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0)
switch reaction.value.value {
case "😳":
intrinsicSize.width += 8.0
intrinsicSize.height += 8.0
self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0)
case "👍":
intrinsicSize.width += 20.0
intrinsicSize.height += 20.0
self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0)
default:
self.imageNode = ASImageNode()
switch reaction {
case let .reaction(value, _, file):
switch value {
case "😳":
intrinsicSize.width += 8.0
intrinsicSize.height += 8.0
self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0)
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.imageNode.image = UIImage(named: "Chat/Context Menu/ReactionReply", in: Bundle(for: ReactionNode.self), compatibleWith: nil)
}
self.intrinsicSize = intrinsicSize
super.init()
@ -66,15 +74,17 @@ private final class ReactionNode: ASDisplayNode {
//self.backgroundColor = .green
self.addSubnode(self.animationNode)
self.animationNode.visibility = true
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.addSubnode(self.imageNode)
self.animationNode.updateLayout(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) {
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.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) {
@ -87,43 +97,46 @@ private final class ReactionNode: ASDisplayNode {
}
final class ReactionSelectionNode: ASDisplayNode {
private let account: Account
private let reactions: [ReactionGestureItem]
private let backgroundNode: ASImageNode
private let backgroundShadowNode: ASImageNode
private let bubbleNodes: [(ASImageNode, ASImageNode)]
private let reactionNodes: [ReactionNode]
private var reactionNodes: [ReactionNode] = []
private var hasSelectedNode = false
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]) {
self.account = account
self.reactions = reactions
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: 42.0)
self.backgroundShadowNode = ASImageNode()
self.backgroundShadowNode.displaysAsynchronously = false
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
let imageNode = ASImageNode()
imageNode.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * 8.0)
imageNode.displaysAsynchronously = false
imageNode.displayWithoutProcessing = true
let shadowNode = ASImageNode()
shadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * 8.0)
shadowNode.displaysAsynchronously = false
shadowNode.displayWithoutProcessing = true
return (imageNode, shadowNode)
}
self.reactionNodes = reactions.map { reaction -> ReactionNode in
return ReactionNode(account: account, reaction: reaction)
}
super.init()
self.bubbleNodes.forEach { _, shadow in
@ -134,18 +147,50 @@ final class ReactionSelectionNode: ASDisplayNode {
self.addSubnode(foreground)
}
self.addSubnode(self.backgroundNode)
self.reactionNodes.forEach(self.addSubnode(_:))
}
func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) {
let backgroundHeight: CGFloat = 42.0
let reactionSpacing: CGFloat = 6.0
let initialAnchorX = startingPoint.x
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 contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing
var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0))
backgroundFrame = backgroundFrame.offsetBy(dx: startingPoint.x - contentWidth + backgroundHeight / 2.0 - 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.backgroundShadowNode.frame = backgroundFrame
@ -201,11 +246,11 @@ final class ReactionSelectionNode: ASDisplayNode {
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].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].1.frame = secondaryBubbleFrame
}

View File

@ -6,13 +6,16 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
private var validatedGesture = false
private var firstLocation: CGPoint = CGPoint()
private var currentLocation: CGPoint = CGPoint()
private var currentReactions: [ReactionGestureItem] = []
private var isActivated = false
private var isAwaitingCompletion = false
private weak var currentContainer: ReactionSelectionParentNode?
private var activationTimer: Timer?
public var availableReactions: (() -> [ReactionGestureItem])?
public var getReactionContainer: (() -> ReactionSelectionParentNode?)?
public var began: (() -> Void)?
public var updateOffset: ((CGFloat, Bool) -> Void)?
public var completed: ((ReactionGestureItem?) -> Void)?
public var displayReply: ((CGFloat) -> Void)?
@ -31,6 +34,8 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
self.currentReactions = []
self.isActivated = false
self.isAwaitingCompletion = false
self.activationTimer?.invalidate()
self.activationTimer = nil
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
@ -40,6 +45,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
self.currentReactions = availableReactions
let touch = touches.first!
self.firstLocation = touch.location(in: nil)
self.currentLocation = self.firstLocation
} else {
self.state = .failed
}
@ -55,6 +61,7 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
guard let location = touches.first?.location(in: nil) else {
return
}
self.currentLocation = location
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.firstLocation = location
translation = CGPoint()
self.began?()
self.updateOffset?(0.0, false)
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 {
self.isActivated = true
self.displayReply?(-min(0.0, translation.x))
if !self.currentReactions.isEmpty, let reactionContainer = self.getReactionContainer?() {
self.currentContainer = reactionContainer
let reactionContainerLocation = reactionContainer.view.convert(location, from: nil)
reactionContainer.displayReactions(self.currentReactions, at: reactionContainerLocation)
}
}
} else {
if let reactionContainer = self.currentContainer {
@ -111,8 +143,8 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
if self.validatedGesture {
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
if let reaction = self.currentContainer?.selectedReaction() {
self.completed?(reaction)
self.isAwaitingCompletion = true
self.completed?(reaction)
} else {
if translation.x < -40.0 {
self.currentContainer?.dismissReactions(into: nil, hideTarget: false)

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -418,9 +418,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
}
}
}
if !item.controllerInteraction.canSetupReply(item.message) {
//return []
}
let reactions: [(String, String)] = [
("😒", "Sad"),
@ -433,14 +430,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
var result: [ReactionGestureItem] = []
for (value, text) in reactions {
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
}
reactionRecognizer.getReactionContainer = { [weak self] in
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
guard let strongSelf = self else {
return
@ -479,11 +485,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
guard let strongSelf = self, let item = strongSelf.item else {
return
}
if strongSelf.swipeToReplyNode == nil {
if strongSelf.swipeToReplyFeedback == nil {
strongSelf.swipeToReplyFeedback = HapticFeedback()
}
strongSelf.swipeToReplyFeedback?.tap()
if strongSelf.swipeToReplyFeedback == nil {
strongSelf.swipeToReplyFeedback = HapticFeedback()
}
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))
strongSelf.swipeToReplyNode = swipeToReplyNode
strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea)
@ -497,8 +503,28 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
return
}
if let item = strongSelf.item, let reaction = reaction {
strongSelf.awaitingAppliedReaction = reaction.value.value
item.controllerInteraction.updateMessageReaction(item.message.id, reaction.value.value)
switch reaction {
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 {
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
var bounds = strongSelf.bounds