Reactions

This commit is contained in:
Ali 2021-11-19 18:06:28 +04:00
parent 2d7a45ccc1
commit 7115fee0fc
15 changed files with 632 additions and 377 deletions

View File

@ -576,10 +576,11 @@ private final class DayComponent: Component {
self.titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
if let mediaPreviewView = self.mediaPreviewView {
mediaPreviewView.frame = contentFrame
mediaPreviewView.bounds = CGRect(origin: CGPoint(), size: contentFrame.size)
mediaPreviewView.position = CGPoint(x: contentFrame.midX, y: contentFrame.midY)
mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false)
mediaPreviewView.sublayerTransform = CATransform3DMakeScale(contentScale, contentScale, 1.0)
mediaPreviewView.transform = CATransform3DMakeScale(contentScale, contentScale, 1.0)
if animateMediaIn {
mediaPreviewView.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)

View File

@ -1114,6 +1114,13 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode {
reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y)
transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y)
}
}
func animateOutToReaction(value: String, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) {
guard let reactionContextNode = self.reactionContextNode else {
self.animateOut(result: .default, completion: completion)
@ -1128,10 +1135,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
self.reactionContextNodeIsAnimatingOut = true
self.animateOut(result: .default, completion: {
contentCompleted = true
intermediateCompletion()
})
reactionContextNode.animateOutToReaction(value: value, targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: hideNode, completion: { [weak self] in
guard let strongSelf = self else {
return
@ -1141,6 +1144,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
reactionCompleted = true
intermediateCompletion()
})
self.animateOut(result: .default, completion: {
contentCompleted = true
intermediateCompletion()
})
self.isUserInteractionEnabled = false
}
@ -1180,7 +1189,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
if !items.reactionItems.isEmpty, let context = items.context {
let reactionContextNode = ReactionContextNode(account: context.account, theme: self.presentationData.theme, items: items.reactionItems)
let reactionContextNode = ReactionContextNode(context: context, theme: self.presentationData.theme, items: items.reactionItems)
self.reactionContextNode = reactionContextNode
self.addSubnode(reactionContextNode)
@ -1188,12 +1197,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else {
return
}
switch reaction {
case .like:
controller.reactionSelected?(.like)
case .unlike:
controller.reactionSelected?(.unlike)
}
controller.reactionSelected?(reaction)
}
}
@ -1749,6 +1753,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if !self.bounds.contains(point) {
return nil
}
if !self.isUserInteractionEnabled {
return nil
}
if let reactionContextNode = self.reactionContextNode {
if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) {
@ -1996,7 +2003,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
private var shouldBeDismissedDisposable: Disposable?
public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)?
public var reactionSelected: ((ReactionContextItem) -> Void)?
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil) {
self.account = account
@ -2151,4 +2158,8 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.dismissed?()
}
}
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
}
}

View File

@ -389,6 +389,17 @@ public extension ContainedViewLayoutTransition {
}
}
func animatePositionWithKeyframes(node: ASDisplayNode, keyframes: [AnyObject], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:
completion?(true)
case let .animated(duration, curve):
node.layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, completion: { value in
completion?(value)
})
}
}
func animateFrame(node: ASDisplayNode, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:

View File

@ -17,6 +17,8 @@ swift_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/StickerResources:StickerResources",
"//submodules/AccountContext:AccountContext",
],
visibility = [
"//visibility:public",

View File

@ -4,6 +4,8 @@ import Display
import AnimatedStickerNode
import TelegramCore
import TelegramPresentationData
import AccountContext
import TelegramAnimatedStickerNode
public enum ReactionGestureItem {
case like
@ -11,15 +13,26 @@ public enum ReactionGestureItem {
}
public final class ReactionContextItem {
public enum Reaction {
case like
case unlike
public struct Reaction {
public var rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
public let reaction: ReactionContextItem.Reaction
public let listAnimation: TelegramMediaFile
public let applicationAnimation: TelegramMediaFile
public init(reaction: ReactionContextItem.Reaction) {
public init(
reaction: ReactionContextItem.Reaction,
listAnimation: TelegramMediaFile,
applicationAnimation: TelegramMediaFile
) {
self.reaction = reaction
self.listAnimation = listAnimation
self.applicationAnimation = applicationAnimation
}
}
@ -75,7 +88,7 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shado
})?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0))
}
public final class ReactionContextNode: ASDisplayNode {
public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private let theme: PresentationTheme
private let items: [ReactionContextItem]
@ -90,18 +103,20 @@ public final class ReactionContextNode: ASDisplayNode {
private let smallCircleShadowNode: ASImageNode
private let contentContainer: ASDisplayNode
private let contentContainerMask: UIImageView
private let scrollNode: ASScrollNode
private var itemNodes: [ReactionNode] = []
private let disclosureButton: HighlightTrackingButtonNode
private var isExpanded: Bool = true
private var highlightedReaction: ReactionContextItem.Reaction?
private var validLayout: (CGSize, UIEdgeInsets, CGRect)?
private var isLeftAligned: Bool = true
public var reactionSelected: ((ReactionGestureItem) -> Void)?
public var reactionSelected: ((ReactionContextItem) -> Void)?
private let hapticFeedback = HapticFeedback()
public init(account: Account, theme: PresentationTheme, items: [ReactionContextItem]) {
public init(context: AccountContext, theme: PresentationTheme, items: [ReactionContextItem]) {
self.theme = theme
self.items = items
@ -144,27 +159,46 @@ public final class ReactionContextNode: ASDisplayNode {
self.largeCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: largeCircleSize, shadowBlur: shadowBlur)
self.smallCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: smallCircleSize, shadowBlur: shadowBlur)
self.scrollNode = ASScrollNode()
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.contentContainer = ASDisplayNode()
self.contentContainer.clipsToBounds = true
self.contentContainer.addSubnode(self.scrollNode)
self.disclosureButton = HighlightTrackingButtonNode()
self.disclosureButton.hitTestSlop = UIEdgeInsets(top: -6.0, left: -6.0, bottom: -6.0, right: -6.0)
let buttonImage = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
self.contentContainerMask = UIImageView()
let maskGradientWidth: CGFloat = 10.0
self.contentContainerMask.image = generateImage(CGSize(width: maskGradientWidth * 2.0 + 1.0, height: 8.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.contextMenu.dimColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setLineJoin(.round)
context.beginPath()
context.move(to: CGPoint(x: 8.0, y: size.height / 2.0 + 3.0))
context.addLine(to: CGPoint(x: size.width / 2.0, y: 11.0))
context.addLine(to: CGPoint(x: size.width - 8.0, y: size.height / 2.0 + 3.0))
context.strokePath()
})
self.disclosureButton.setImage(buttonImage, for: [])
let shadowColor = UIColor.black
let stepCount = 10
var colors: [CGColor] = []
var locations: [CGFloat] = []
for i in 0 ... stepCount {
let t = CGFloat(i) / CGFloat(stepCount)
colors.append(shadowColor.withAlphaComponent(t * t).cgColor)
locations.append(t)
}
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: maskGradientWidth, y: 0.0), options: CGGradientDrawingOptions())
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: 0.0), end: CGPoint(x: maskGradientWidth + 1.0, y: 0.0), options: CGGradientDrawingOptions())
context.setFillColor(shadowColor.cgColor)
context.fill(CGRect(origin: CGPoint(x: maskGradientWidth, y: 0.0), size: CGSize(width: 1.0, height: size.height)))
})?.stretchableImage(withLeftCapWidth: Int(maskGradientWidth), topCapHeight: 0)
self.contentContainer.view.mask = self.contentContainerMask
//self.contentContainer.view.addSubview(self.contentContainerMask)
super.init()
@ -177,30 +211,14 @@ public final class ReactionContextNode: ASDisplayNode {
self.backgroundContainerNode.addSubnode(self.backgroundNode)
self.addSubnode(self.backgroundContainerNode)
self.contentContainer.addSubnode(self.disclosureButton)
self.scrollNode.view.delegate = self
self.itemNodes = self.items.map { item in
let reactionItem: ReactionGestureItem
switch item.reaction {
case .like:
reactionItem = .like
case .unlike:
reactionItem = .unlike
}
return ReactionNode(account: account, theme: theme, reaction: reactionItem, maximizedReactionSize: 30.0, loadFirstFrame: true)
return ReactionNode(context: context, theme: theme, item: item)
}
self.itemNodes.forEach(self.contentContainer.addSubnode)
self.itemNodes.forEach(self.scrollNode.addSubnode)
self.addSubnode(self.contentContainer)
self.disclosureButton.addTarget(self, action: #selector(self.disclosurePressed), forControlEvents: .touchUpInside)
self.disclosureButton.highligthedChanged = { [weak self] highlighted in
if highlighted {
self?.disclosureButton.layer.animateScale(from: 1.0, to: 0.8, duration: 0.15, removeOnCompletion: false)
} else {
self?.disclosureButton.layer.animateScale(from: 0.8, to: 1.0, duration: 0.25)
}
}
}
override public func didLoad() {
@ -213,7 +231,7 @@ public final class ReactionContextNode: ASDisplayNode {
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil)
}
private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (CGRect, Bool) {
private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (backgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) {
var contentSize = contentSize
contentSize.width = max(52.0, contentSize.width)
contentSize.height = 52.0
@ -233,80 +251,87 @@ public final class ReactionContextNode: ASDisplayNode {
rect.origin.x = max(sideInset, rect.origin.x)
rect.origin.y = max(insets.top + sideInset, rect.origin.y)
rect.origin.x = min(containerSize.width - contentSize.width - sideInset, rect.origin.x)
return (rect, isLeftAligned)
let cloudSourcePoint: CGFloat
if isLeftAligned {
cloudSourcePoint = min(rect.maxX - rect.height / 2.0, anchorRect.maxX - 4.0)
} else {
cloudSourcePoint = max(rect.minX + rect.height / 2.0, anchorRect.minX)
}
return (rect, isLeftAligned, cloudSourcePoint)
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
private func updateScrolling(transition: ContainedViewLayoutTransition) {
let sideInset: CGFloat = 14.0
let minScale: CGFloat = 0.6
let scaleDistance: CGFloat = 30.0
let visibleBounds = self.scrollNode.view.bounds
for itemNode in self.itemNodes {
if itemNode.isExtracted {
continue
}
let itemScale: CGFloat
let itemFrame = itemNode.frame.offsetBy(dx: -visibleBounds.minX, dy: 0.0)
if itemFrame.minX < sideInset || itemFrame.maxX > visibleBounds.width - sideInset {
let edgeDistance: CGFloat
if itemFrame.minX < sideInset {
edgeDistance = sideInset - itemFrame.minX
} else {
edgeDistance = itemFrame.maxX - (visibleBounds.width - sideInset)
}
let edgeFactor: CGFloat = min(1.0, edgeDistance / scaleDistance)
itemScale = edgeFactor * minScale + (1.0 - edgeFactor) * 1.0
} else {
itemScale = 1.0
}
transition.updateSublayerTransformScale(node: itemNode, scale: itemScale)
}
}
private func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) {
self.validLayout = (size, insets, anchorRect)
let sideInset: CGFloat = 12.0
let itemSpacing: CGFloat = 6.0
let minimizedItemSize: CGFloat = 30.0
let maximizedItemSize: CGFloat = 30.0 - 18.0
let sideInset: CGFloat = 14.0
let itemSpacing: CGFloat = 9.0
let itemSize: CGFloat = 40.0
let shadowBlur: CGFloat = 5.0
let verticalInset: CGFloat = 13.0
let rowHeight: CGFloat = 30.0
let rowSpacing: CGFloat = itemSpacing
let columnCount = min(6, self.items.count)
let contentWidth = CGFloat(columnCount) * minimizedItemSize + (CGFloat(columnCount) - 1.0) * itemSpacing + sideInset * 2.0
let rowCount = self.items.count / columnCount + (self.items.count % columnCount == 0 ? 0 : 1)
let completeContentWidth = CGFloat(self.items.count) * itemSize + (CGFloat(self.items.count) - 1.0) * itemSpacing + sideInset * 2.0
let minVisibleItemCount: CGFloat = min(CGFloat(self.items.count), 6.5)
let visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0)
let expandedRowCount = self.isExpanded ? rowCount : 1
let contentHeight = verticalInset * 2.0 + rowHeight
let contentHeight = verticalInset * 2.0 + rowHeight * CGFloat(expandedRowCount) + CGFloat(expandedRowCount - 1) * rowSpacing
let (backgroundFrame, isLeftAligned) = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: anchorRect, contentSize: CGSize(width: contentWidth, height: contentHeight))
let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight))
self.isLeftAligned = isLeftAligned
transition.updateFrame(node: self.contentContainer, frame: backgroundFrame)
transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: backgroundFrame.size.height)
for i in 0 ..< self.items.count {
let rowIndex = i / columnCount
let columnIndex = i % columnCount
let row = CGFloat(rowIndex)
let columnIndex = i
let column = CGFloat(columnIndex)
let itemSize: CGFloat = minimizedItemSize
let itemOffset: CGFloat = 0.0
let itemOffsetY: CGFloat = -1.0
let itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (minimizedItemSize + itemSpacing) - itemOffset, y: verticalInset + row * (rowHeight + rowSpacing) + floor((rowHeight - minimizedItemSize) / 2.0) - itemOffset), size: CGSize(width: itemSize, height: itemSize))
transition.updateFrame(node: self.itemNodes[i], frame: itemFrame, beginWithCurrentState: true)
self.itemNodes[i].updateLayout(size: CGSize(width: itemSize, height: itemSize), scale: itemSize / (maximizedItemSize + 18.0), transition: transition, displayText: false)
self.itemNodes[i].updateIsAnimating(false, animated: false)
if rowIndex != 0 || columnIndex == columnCount - 1 {
if self.isExpanded {
if self.itemNodes[i].alpha.isZero {
self.itemNodes[i].alpha = 1.0
if transition.isAnimated {
let delayOffset: Double = 1.0 - Double(columnIndex) / Double(columnCount - 1)
self.itemNodes[i].layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4 + delayOffset * 0.32, initialVelocity: 0.0, damping: 95.0)
self.itemNodes[i].layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05)
}
}
} else {
self.itemNodes[i].alpha = 0.0
}
} else {
self.itemNodes[i].alpha = 1.0
}
if rowIndex == 0 && columnIndex == columnCount - 1 {
transition.updateFrame(node: self.disclosureButton, frame: itemFrame)
if self.isExpanded {
if self.disclosureButton.alpha.isEqual(to: 1.0) {
self.disclosureButton.alpha = 0.0
if transition.isAnimated {
self.disclosureButton.layer.animateScale(from: 0.8, to: 0.1, duration: 0.2, removeOnCompletion: false)
self.disclosureButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.disclosureButton.layer.removeAnimation(forKey: "scale")
})
}
}
} else {
self.disclosureButton.alpha = 1.0
}
let itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize))
if !self.itemNodes[i].isExtracted {
transition.updateFrame(node: self.itemNodes[i], frame: itemFrame, beginWithCurrentState: true)
self.itemNodes[i].updateLayout(size: CGSize(width: itemSize, height: itemSize), isExpanded: false, transition: transition)
}
}
self.updateScrolling(transition: transition)
let isInOverflow = backgroundFrame.maxY > anchorRect.minY
let backgroundAlpha: CGFloat = isInOverflow ? 1.0 : 0.8
@ -324,10 +349,10 @@ public final class ReactionContextNode: ASDisplayNode {
let largeCircleFrame: CGRect
let smallCircleFrame: CGRect
if isLeftAligned {
largeCircleFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize))
largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize))
smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize))
} else {
largeCircleFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize))
largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize))
smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.minX + 3.0 - smallCircleSize, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize))
}
@ -339,12 +364,20 @@ public final class ReactionContextNode: ASDisplayNode {
if let animateInFromAnchorRect = animateInFromAnchorRect {
let springDuration: Double = 0.42
let springDamping: CGFloat = 104.0
let springDelay: Double = 0.22
let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: contentWidth, height: contentHeight)).0
let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: backgroundFrame.height, height: contentHeight)).0
self.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.minX - backgroundFrame.minX, y: sourceBackgroundFrame.minY - backgroundFrame.minY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true)
self.backgroundNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping)
self.contentContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true)
self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping)
//self.contentContainerMask.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true)
//self.contentContainerMask.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping)
} else if let animateOutToAnchorRect = animateOutToAnchorRect {
let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: contentWidth, height: contentHeight)).0
let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0
self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: targetBackgroundFrame.minX - backgroundFrame.minX, y: targetBackgroundFrame.minY - backgroundFrame.minY), duration: 0.2, removeOnCompletion: false, additive: true)
}
@ -374,12 +407,19 @@ public final class ReactionContextNode: ASDisplayNode {
self.backgroundNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay)
self.backgroundShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay)
if let itemNode = self.itemNodes.first {
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
let itemDelay = mainCircleDelay + 0.1 + Double(i) * 0.03
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: itemDelay)
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: itemDelay, initialVelocity: 0.0)
}
/*if let itemNode = self.itemNodes.first {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay)
itemNode.didAppear()
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay, completion: { _ in
})
}
}*/
}
public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) {
@ -390,84 +430,162 @@ public final class ReactionContextNode: ASDisplayNode {
self.smallCircleNode.layer.animateAlpha(from: self.smallCircleNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.smallCircleShadowNode.layer.animateAlpha(from: self.smallCircleShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
for itemNode in self.itemNodes {
if itemNode.isExtracted {
continue
}
itemNode.layer.animateAlpha(from: itemNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
self.disclosureButton.layer.animateAlpha(from: self.disclosureButton.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
if let targetAnchorRect = targetAnchorRect, let (size, insets, anchorRect) = self.validLayout {
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: targetAnchorRect)
}
}
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [AnyObject] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
var keyframes: [AnyObject] = []
if abs(y1 - y3) < 5.0 || abs(x1 - x3) < 5.0 {
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y)))
}
} else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y)))
}
}
return keyframes
}
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetFilledNode: ASDisplayNode, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
targetSnapshotView.frame = self.view.convert(targetFilledNode.bounds, from: targetFilledNode.view)
self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view)
var completedTarget = false
var targetScaleCompleted = false
let intermediateCompletion: () -> Void = {
if completedTarget && targetScaleCompleted {
completion()
}
}
let targetPosition = self.view.convert(targetFilledNode.bounds.center, from: targetFilledNode.view)
let duration: Double = 0.16
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false)
targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8)
targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in
if hideNode {
targetFilledNode.isHidden = false
targetFilledNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in
targetSnapshotView?.isHidden = true
targetScaleCompleted = true
intermediateCompletion()
})
} else {
targetSnapshotView?.isHidden = true
targetScaleCompleted = true
intermediateCompletion()
}
})
let keyframes = self.generateParabollicMotionKeyframes(from: itemNode.frame.center, to: targetPosition, elevation: 30.0)
itemNode.layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.hapticFeedback.tap()
}
completedTarget = true
intermediateCompletion()
})
targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", removeOnCompletion: false)
itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / itemNode.bounds.width, duration: duration, removeOnCompletion: false)
}
public func animateOutToReaction(value: String, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) {
for itemNode in self.itemNodes {
switch itemNode.reaction {
case .like:
if let snapshotView = itemNode.view.snapshotContentTree(keepTransform: true), let targetSnapshotView = targetFilledNode.view.snapshotContentTree() {
targetSnapshotView.frame = self.view.convert(targetFilledNode.bounds, from: targetFilledNode.view)
itemNode.isHidden = true
self.view.addSubview(targetSnapshotView)
self.view.addSubview(snapshotView)
snapshotView.frame = itemNode.view.convert(itemNode.view.bounds, to: self.view).offsetBy(dx: 25.0, dy: 1.0)
var completedTarget = false
let intermediateCompletion: () -> Void = {
if completedTarget {
completion()
}
if itemNode.item.reaction.rawValue != value {
continue
}
if let targetSnapshotView = targetFilledNode.view.snapshotContentTree() {
if hideNode {
targetFilledNode.isHidden = true
}
itemNode.isExtracted = true
let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view)
let selfTargetRect = self.view.convert(targetFilledNode.bounds, from: targetFilledNode.view)
let expandedScale: CGFloat = 4.0
let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale))
let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
self.addSubnode(itemNode)
itemNode.frame = selfSourceRect
itemNode.position = expandedFrame.center
transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size))
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: transition)
transition.animatePositionWithKeyframes(node: itemNode, keyframes: self.generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0))
let additionalAnimationNode = AnimatedStickerNode()
let incomingMessage: Bool = self.isLeftAligned
let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5)
.offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0)
//animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0))
additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
additionalAnimationNode.frame = animationFrame
if incomingMessage {
additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
}
additionalAnimationNode.updateLayout(size: animationFrame.size)
self.addSubnode(additionalAnimationNode)
var mainAnimationCompleted = false
var additionalAnimationCompleted = false
let intermediateCompletion: () -> Void = {
if mainAnimationCompleted && additionalAnimationCompleted {
completion()
}
let targetPosition = self.view.convert(targetFilledNode.bounds.center, from: targetFilledNode.view)
let duration: Double = 0.3
if hideNode {
targetFilledNode.isHidden = true
}
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
targetSnapshotView.layer.animateScale(from: snapshotView.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: 0.3, removeOnCompletion: false)
let sourcePoint = snapshotView.center
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - 30.0)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
var keyframes: [AnyObject] = []
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y)))
}
snapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.hapticFeedback.tap()
}
completedTarget = true
if hideNode {
targetFilledNode.isHidden = false
targetFilledNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0)
targetEmptyNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0)
}
}
additionalAnimationNode.completed = { _ in
additionalAnimationCompleted = true
intermediateCompletion()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: {
additionalAnimationNode.visibility = true
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: {
self.animateFromItemNodeToReaction(itemNode: itemNode, targetFilledNode: targetFilledNode, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: {
mainAnimationCompleted = true
intermediateCompletion()
})
targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false)
snapshotView.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / snapshotView.bounds.width, duration: 0.3, removeOnCompletion: false)
return
}
default:
break
})
return
}
}
completion()
@ -475,16 +593,14 @@ public final class ReactionContextNode: ASDisplayNode {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let contentPoint = self.contentContainer.view.convert(point, from: self.view)
if !self.disclosureButton.alpha.isZero {
if let result = self.disclosureButton.hitTest(self.disclosureButton.view.convert(point, from: self.view), with: event) {
return result
}
if self.contentContainer.bounds.contains(contentPoint) {
return self.contentContainer.hitTest(contentPoint, with: event)
}
for itemNode in self.itemNodes {
/*for itemNode in self.itemNodes {
if !itemNode.alpha.isZero && itemNode.frame.contains(contentPoint) {
return self.view
}
}
}*/
return nil
}
@ -497,16 +613,14 @@ public final class ReactionContextNode: ASDisplayNode {
}
}
public func reaction(at point: CGPoint) -> ReactionGestureItem? {
let contentPoint = self.contentContainer.view.convert(point, from: self.view)
for itemNode in self.itemNodes {
if !itemNode.alpha.isZero && itemNode.frame.contains(contentPoint) {
return itemNode.reaction
}
}
for itemNode in self.itemNodes {
if !itemNode.alpha.isZero && itemNode.frame.insetBy(dx: -8.0, dy: -8.0).contains(contentPoint) {
return itemNode.reaction
public func reaction(at point: CGPoint) -> ReactionContextItem? {
for i in 0 ..< 2 {
let touchInset: CGFloat = i == 0 ? 0.0 : 8.0
for itemNode in self.itemNodes {
let itemPoint = self.view.convert(point, to: itemNode.view)
if itemNode.bounds.insetBy(dx: -touchInset, dy: -touchInset).contains(itemPoint) {
return itemNode.item
}
}
}
return nil

View File

@ -7,6 +7,9 @@ import TelegramPresentationData
import AppBundle
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SwiftSignalKit
import StickerResources
import AccountContext
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
@ -33,124 +36,124 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shado
private let font = Font.medium(13.0)
final class ReactionNode: ASDisplayNode {
let reaction: ReactionGestureItem
private let textBackgroundNode: ASImageNode
private let textNode: ImmediateTextNode
private let animationNode: AnimatedStickerNode
private let imageNode: ASImageNode
private let additionalImageNode: ASImageNode
var isMaximized: Bool?
private let intrinsicSize: CGSize
private let intrinsicOffset: CGPoint
let context: AccountContext
let item: ReactionContextItem
private let staticImageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
init(account: Account, theme: PresentationTheme, reaction: ReactionGestureItem, maximizedReactionSize: CGFloat, loadFirstFrame: Bool) {
self.reaction = reaction
private var fetchStickerDisposable: Disposable?
private var fetchFullAnimationDisposable: Disposable?
private var validSize: CGSize?
var isExtracted: Bool = false
init(context: AccountContext, theme: PresentationTheme, item: ReactionContextItem) {
self.context = context
self.item = item
self.textBackgroundNode = ASImageNode()
self.textBackgroundNode.displaysAsynchronously = false
self.textBackgroundNode.displayWithoutProcessing = true
self.textBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillFloating.withAlphaComponent(0.8))
self.textBackgroundNode.alpha = 0.0
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
let reactionText: String = ""
self.textNode.attributedText = NSAttributedString(string: reactionText, font: font, textColor: theme.chat.serviceMessage.dateTextColor.withWallpaper)
let textSize = self.textNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let textBackgroundSize = CGSize(width: textSize.width + 12.0, height: 20.0)
let textBackgroundFrame = CGRect(origin: CGPoint(), size: textBackgroundSize)
let textFrame = CGRect(origin: CGPoint(x: floor((textBackgroundFrame.width - textSize.width) / 2.0), y: floor((textBackgroundFrame.height - textSize.height) / 2.0)), size: textSize)
self.textBackgroundNode.frame = textBackgroundFrame
self.textNode.frame = textFrame
self.textNode.alpha = 0.0
self.animationNode = AnimatedStickerNode()
self.animationNode.automaticallyLoadFirstFrame = loadFirstFrame
self.animationNode.playToCompletionOnStop = true
var intrinsicSize = CGSize(width: maximizedReactionSize + 14.0, height: maximizedReactionSize + 14.0)
self.imageNode = ASImageNode()
self.additionalImageNode = ASImageNode()
switch reaction {
case .like:
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
self.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Reactions/ContextHeartFilled"), color: UIColor(rgb: 0xfe1512))
case .unlike:
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
self.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Reactions/ContextHeartBrokenL"), color: UIColor(rgb: 0xfe1512))
self.additionalImageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Reactions/ContextHeartBrokenR"), color: UIColor(rgb: 0xfe1512))
}
if let image = self.imageNode.image {
intrinsicSize = image.size
}
self.intrinsicSize = intrinsicSize
self.staticImageNode = TransformImageNode()
super.init()
//self.backgroundColor = .gray
//self.backgroundColor = UIColor(white: 0.0, alpha: 0.1)
//self.textBackgroundNode.addSubnode(self.textNode)
//self.addSubnode(self.textBackgroundNode)
self.addSubnode(self.staticImageNode)
/*self.addSubnode(self.animationNode)
//self.addSubnode(self.animationNode)
self.addSubnode(self.imageNode)
self.addSubnode(self.additionalImageNode)
self.animationNode.updateLayout(size: self.intrinsicSize)
self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)*/
switch reaction {
case .like:
self.imageNode.frame = CGRect(origin: CGPoint(x: -5.0, y: -5.0), size: self.intrinsicSize)
case .unlike:
self.imageNode.frame = CGRect(origin: CGPoint(x: -6.0, y: -5.0), size: self.intrinsicSize)
self.additionalImageNode.frame = CGRect(origin: CGPoint(x: -3.0, y: -5.0), size: self.intrinsicSize)
self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.listAnimation.resource)).start()
self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.applicationAnimation.resource)).start()
//let _ = context.meshAnimationCache.get(resource: item.applicationAnimation.resource)
}
deinit {
self.fetchStickerDisposable?.dispose()
self.fetchFullAnimationDisposable?.dispose()
}
func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
let intrinsicSize = size
let animationSize = self.item.listAnimation.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
var animationDisplaySize = animationSize.aspectFitted(intrinsicSize)
var scalingFactor: CGFloat = 1.0
var offsetFactor: CGFloat = 0.0
switch self.item.reaction.rawValue {
case "💸":
scalingFactor = 1.25
offsetFactor = -0.04
case "👍":
scalingFactor = 1.4
offsetFactor = 0.02
case "👎":
scalingFactor = 1.4
offsetFactor = -0.01
case "😂":
scalingFactor = 1.2
case "🍆":
scalingFactor = 1.1
offsetFactor = -0.01
case "👻":
scalingFactor = 1.2
case "🎃":
scalingFactor = 1.15
offsetFactor = -0.08
case "🎈":
offsetFactor = 0.03
case "🎉":
offsetFactor = -0.01
default:
break
}
}
func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition, displayText: Bool) {
/*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)
transition.updatePosition(node: self.textBackgroundNode, position: CGPoint(x: size.width / 2.0, y: displayText ? -24.0 : (size.height / 2.0)), beginWithCurrentState: true)
transition.updateTransformScale(node: self.textBackgroundNode, scale: displayText ? 1.0 : 0.1, beginWithCurrentState: true)
animationDisplaySize.width = floor(animationDisplaySize.width * scalingFactor)
animationDisplaySize.height = floor(animationDisplaySize.height * scalingFactor)
transition.updateAlpha(node: self.textBackgroundNode, alpha: displayText ? 1.0 : 0.0, beginWithCurrentState: true)
transition.updateAlpha(node: self.textNode, alpha: displayText ? 1.0 : 0.0, beginWithCurrentState: true)*/
}
func updateIsAnimating(_ isAnimating: Bool, animated: Bool) {
if isAnimating {
self.animationNode.visibility = true
} else {
self.animationNode.visibility = false
var animationFrame = CGRect(origin: CGPoint(x: floor((intrinsicSize.width - animationDisplaySize.width) / 2.0), y: floor((intrinsicSize.height - animationDisplaySize.height) / 2.0)), size: animationDisplaySize)
animationFrame.origin.y = floor(animationFrame.origin.y + animationFrame.height * offsetFactor)
if isExpanded, self.animationNode == nil {
let animationNode = AnimatedStickerNode()
self.animationNode = animationNode
self.addSubnode(animationNode)
animationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.staticImageNode.isHidden = true
}
animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource), width: Int(size.width * 2.0), height: Int(size.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
animationNode.frame = animationFrame
animationNode.updateLayout(size: animationFrame.size)
if transition.isAnimated, !self.staticImageNode.frame.isEmpty {
transition.animateTransformScale(node: animationNode, from: self.staticImageNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: animationNode, offset: CGPoint(x: self.staticImageNode.frame.midX - animationFrame.midX, y: self.staticImageNode.frame.midY - animationFrame.midY))
}
animationNode.visibility = true
}
if self.validSize != size {
self.validSize = size
self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.listAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false))
let imageApply = self.staticImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationDisplaySize, boundingSize: animationDisplaySize, intrinsicInsets: UIEdgeInsets()))
imageApply()
transition.updateFrame(node: self.staticImageNode, frame: animationFrame)
}
}
func didAppear() {
switch self.reaction {
case .like:
self.imageNode.layer.animateScale(from: 1.0, to: 1.08, duration: 0.12, delay: 0.22, removeOnCompletion: false, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.imageNode.layer.animateScale(from: 1.08, to: 1.0, duration: 0.12)
})
case .unlike:
self.imageNode.layer.animatePosition(from: CGPoint(x: -2.5, y: 0.0), to: CGPoint(), duration: 0.2, delay: 0.15, additive: true)
self.additionalImageNode.layer.animatePosition(from: CGPoint(x: 2.5, y: 0.0), to: CGPoint(), duration: 0.2, delay: 0.15, additive: true)
}
}
}
final class ReactionSelectionNode: ASDisplayNode {
/*final class ReactionSelectionNode: ASDisplayNode {
private let account: Account
private let theme: PresentationTheme
private let reactions: [ReactionGestureItem]
@ -485,4 +488,4 @@ final class ReactionSelectionNode: ASDisplayNode {
}
}
*/

View File

@ -426,6 +426,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private weak var currentPinchController: PinchController?
private weak var currentPinchSourceItemNode: ListViewItemNode?
private weak var currentReactionContextController: ContextController?
private weak var currentReactionContextItemNode: ListViewItemNode?
private var screenCaptureManager: ScreenCaptureDetectionManager?
private let chatAdditionalDataDisposable = MetaDisposable()
@ -937,8 +940,47 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break
}
}
let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
).start(next: { actions, animatedEmojiStickers, chatTextSelectionTips in
let additionalAnimatedEmojiStickers = strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false)
|> map { animatedEmoji -> [String: [Int: StickerPackItem]] in
let sequence = "0⃣1⃣2⃣3⃣4⃣5⃣6⃣7⃣8⃣9".strippedEmoji
var animatedEmojiStickers: [String: [Int: StickerPackItem]] = [:]
switch animatedEmoji {
case let .result(_, items, _):
for item in items {
let indexKeys = item.getStringRepresentationsOfIndexKeys()
if indexKeys.count > 1, let first = indexKeys.first, let last = indexKeys.last {
let emoji: String?
let indexEmoji: String?
if sequence.contains(first.strippedEmoji) {
emoji = last
indexEmoji = first
} else if sequence.contains(last.strippedEmoji) {
emoji = first
indexEmoji = last
} else {
emoji = nil
indexEmoji = nil
}
if let emoji = emoji?.strippedEmoji, let indexEmoji = indexEmoji?.strippedEmoji.first, let strIndex = sequence.firstIndex(of: indexEmoji) {
let index = sequence.distance(from: sequence.startIndex, to: strIndex)
if animatedEmojiStickers[emoji] != nil {
animatedEmojiStickers[emoji]![index] = item
} else {
animatedEmojiStickers[emoji] = [index: item]
}
}
}
}
default:
break
}
return animatedEmojiStickers
}
let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), additionalAnimatedEmojiStickers, ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
).start(next: { actions, animatedEmojiStickers, additionalAnimatedEmojiStickers, chatTextSelectionTips in
var actions = actions
guard let strongSelf = self, !actions.items.isEmpty else {
@ -970,52 +1012,81 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
actions.context = strongSelf.context
var hasLike = false
let hearts: [String] = ["", "❤️"]
for attribute in messages[0].attributes {
if let attribute = attribute as? ReactionsMessageAttribute {
for reaction in attribute.reactions {
if hearts.contains(reaction.value) {
if reaction.isSelected {
hasLike = true
if message.id.peerId.namespace != Namespaces.Peer.SecretChat {
if case let .result(_, stickers, _) = animatedEmojiStickers {
for fullSticker in additionalAnimatedEmojiStickers {
guard let fullStickerItem = fullSticker.value.first?.value else {
continue
}
inner: for sticker in stickers {
for attribute in sticker.file.attributes {
if case let .Sticker(displayText, _, _) = attribute {
if displayText == fullSticker.key {
actions.reactionItems.append(ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: displayText),
listAnimation: sticker.file,
applicationAnimation: fullStickerItem.file
))
break inner
}
}
}
}
}
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
if let value = attribute.value, hearts.contains(value) {
hasLike = true
}
}
actions.reactionItems.sort(by: { lhs, rhs in
return lhs.reaction.rawValue < rhs.reaction.rawValue
})
}
actions.reactionItems = [ReactionContextItem(reaction: hasLike ? .unlike : .like)]
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), recognizer: recognizer, gesture: gesture)
strongSelf.currentContextController = controller
let _ = strongSelf.context.meshAnimationCache.get(bundleName: "Hearts")
controller.reactionSelected = { [weak controller] value in
guard let strongSelf = self, let message = updatedMessages.first else {
return
}
let hearts: [String] = ["", "❤️"]
var updatedReaction: String? = value.reaction.rawValue
for attribute in messages[0].attributes {
if let attribute = attribute as? ReactionsMessageAttribute {
for reaction in attribute.reactions {
if reaction.value == value.reaction.rawValue {
if reaction.isSelected {
updatedReaction = nil
}
}
}
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
if let current = attribute.value, current == value.reaction.rawValue {
updatedReaction = nil
}
}
}
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
if item.message.id == message.id {
switch value {
case .like:
itemNode.awaitingAppliedReaction = (hearts[0], { [weak itemNode] in
if let updatedReaction = updatedReaction {
itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in
guard let controller = controller else {
return
}
if let itemNode = itemNode, let (targetEmptyNode, targetFilledNode) = itemNode.targetReactionNode(value: hearts[0]) {
controller.dismissWithReaction(value: hearts[0], targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: true, completion: { [weak itemNode, weak targetFilledNode] in
if let itemNode = itemNode, let (targetEmptyNode, targetFilledNode) = itemNode.targetReactionNode(value: updatedReaction) {
strongSelf.currentReactionContextController = controller
strongSelf.currentReactionContextItemNode = itemNode
controller.dismissWithReaction(value: updatedReaction, targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: true, completion: { [weak itemNode, weak targetFilledNode] in
guard let strongSelf = self, let itemNode = itemNode, let targetFilledNode = targetFilledNode else {
return
}
let targetFrame = targetFilledNode.view.convert(targetFilledNode.bounds, to: itemNode.view).offsetBy(dx: 0.0, dy: itemNode.insets.top)
let _ = strongSelf
let _ = itemNode
let _ = targetFilledNode
/*let targetFrame = targetFilledNode.view.convert(targetFilledNode.bounds, to: itemNode.view).offsetBy(dx: 0.0, dy: itemNode.insets.top)
if #available(iOS 13.0, *), let meshAnimation = strongSelf.context.meshAnimationCache.get(bundleName: "Hearts") {
if let animationView = MeshRenderer() {
@ -1039,32 +1110,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
animationView.add(mesh: meshAnimation, offset: CGPoint())
}
}
}*/
})
} else {
controller.dismiss()
}
})
case .unlike:
} else if updatedReaction == nil {
itemNode.awaitingAppliedReaction = (nil, {
guard let controller = controller else {
return
}
controller.dismiss()
})
controller?.dismiss()
}
}
}
}
switch value {
case .like:
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: hearts[0]).start()
case .unlike:
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: nil).start()
}
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start()
}
strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
@ -1074,6 +1134,52 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.window?.presentInGlobalOverlay(controller)
})
}
}, updateMessageReaction: { [weak self] message in
guard let strongSelf = self else {
return
}
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
return
}
var updatedReaction: String? = ""
for attribute in message.attributes {
if let attribute = attribute as? ReactionsMessageAttribute {
for reaction in attribute.reactions {
if reaction.value == updatedReaction {
if reaction.isSelected {
updatedReaction = nil
}
}
}
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
if let current = attribute.value, current == updatedReaction {
updatedReaction = nil
}
}
}
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
if item.message.id == message.id {
itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in
if let updatedReaction = updatedReaction, let itemNode = itemNode, let (_, targetFilledNode) = itemNode.targetReactionNode(value: updatedReaction) {
targetFilledNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: 90.0)
if let strongSelf = self {
if strongSelf.selectPollOptionFeedback == nil {
strongSelf.selectPollOptionFeedback = HapticFeedback()
}
strongSelf.selectPollOptionFeedback?.tap()
}
}
})
}
}
}
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start()
}, activateMessagePinch: { [weak self] sourceNode in
guard let strongSelf = self else {
return
@ -4651,6 +4757,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
}
}
if let reactionItemNode = strongSelf.currentReactionContextItemNode, let currentReactionContextController = strongSelf.currentReactionContextController {
if let itemNode = itemNode {
if itemNode === reactionItemNode {
currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
}
} else {
currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
}
}
strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode)
}

View File

@ -51,6 +51,7 @@ public final class ChatControllerInteraction {
let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void
let openPeerMention: (String) -> Void
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void
let updateMessageReaction: (Message) -> Void
let activateMessagePinch: (PinchSourceContainerNode) -> Void
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
let navigateToMessage: (MessageId, MessageId) -> Void
@ -145,6 +146,7 @@ public final class ChatControllerInteraction {
openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void,
openPeerMention: @escaping (String) -> Void,
openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void,
updateMessageReaction: @escaping (Message) -> Void,
activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void,
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
navigateToMessage: @escaping (MessageId, MessageId) -> Void,
@ -225,6 +227,7 @@ public final class ChatControllerInteraction {
self.openPeer = openPeer
self.openPeerMention = openPeerMention
self.openMessageContextMenu = openMessageContextMenu
self.updateMessageReaction = updateMessageReaction
self.activateMessagePinch = activateMessagePinch
self.openMessageContextActions = openMessageContextActions
self.navigateToMessage = navigateToMessage
@ -307,7 +310,7 @@ public final class ChatControllerInteraction {
static var `default`: ChatControllerInteraction {
return ChatControllerInteraction(openMessage: { _, _ in
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in }, navigationController: {
return nil
}, chatControllerNode: {

View File

@ -2833,8 +2833,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
f()
case let .optionalAction(f):
f()
case let .openContextMenu(tapMessage, selectAll, subFrame):
self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
case let .openContextMenu(tapMessage, _, _):
self.item?.controllerInteraction.updateMessageReaction(tapMessage)
//self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
}
} else if case .tap = gesture {
self.item?.controllerInteraction.clickThroughMessage()

View File

@ -44,55 +44,45 @@ private let reactionCountFont = Font.semibold(11.0)
private let reactionFont = Font.regular(12.0)
private final class StatusReactionNode: ASDisplayNode {
let emptyImageNode: ASImageNode
let selectedImageNode: ASImageNode
private var theme: PresentationTheme?
private var value: String?
private var isSelected: Bool?
override init() {
self.emptyImageNode = ASImageNode()
self.emptyImageNode.displaysAsynchronously = false
self.selectedImageNode = ASImageNode()
self.selectedImageNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.emptyImageNode)
self.addSubnode(self.selectedImageNode)
}
func update(type: ChatMessageDateAndStatusType, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) {
if self.theme !== theme {
self.theme = theme
func update(type: ChatMessageDateAndStatusType, value: String, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) {
if self.value != value {
self.value = value
let emptyImage: UIImage?
let selectedImage: UIImage?
switch type {
case .BubbleIncoming:
emptyImage = PresentationResourcesChat.chatMessageLike(theme, incoming: true, isSelected: false)
selectedImage = PresentationResourcesChat.chatMessageLike(theme, incoming: true, isSelected: true)
case .BubbleOutgoing:
emptyImage = PresentationResourcesChat.chatMessageLike(theme, incoming: false, isSelected: false)
selectedImage = PresentationResourcesChat.chatMessageLike(theme, incoming: false, isSelected: true)
case .ImageIncoming, .ImageOutgoing:
emptyImage = PresentationResourcesChat.chatMessageMediaLike(theme, isSelected: false)
selectedImage = PresentationResourcesChat.chatMessageMediaLike(theme, isSelected: true)
case .FreeIncoming, .FreeOutgoing:
emptyImage = PresentationResourcesChat.chatMessageFreeLike(theme, wallpaper: wallpaper, isSelected: false)
selectedImage = PresentationResourcesChat.chatMessageFreeLike(theme, wallpaper: wallpaper, isSelected: true)
}
if let emptyImage = emptyImage, let selectedImage = selectedImage {
self.emptyImageNode.image = emptyImage
self.emptyImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: emptyImage.size)
let selectedImage: UIImage? = generateImage(CGSize(width: 14.0, height: 14.0), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: size.width / 20.0, y: size.width / 20.0)
let string = NSAttributedString(string: value, font: reactionFont, textColor: .black)
string.draw(at: CGPoint(x: 1.0, y: 2.0))
UIGraphicsPopContext()
})
if let selectedImage = selectedImage {
self.selectedImageNode.image = selectedImage
self.selectedImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: selectedImage.size)
}
}
if self.isSelected != isSelected {
/*if self.isSelected != isSelected {
let wasSelected = self.isSelected
self.isSelected = isSelected
@ -143,7 +133,7 @@ private final class StatusReactionNode: ASDisplayNode {
}
}
}
}
}*/
}
}
@ -758,7 +748,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
}
}
node.update(type: type, isSelected: reactions[i].isSelected, count: Int(reactions[i].count), theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, animated: false)
node.update(type: type, value: reactions[i].value, isSelected: reactions[i].isSelected, count: Int(reactions[i].count), theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, animated: false)
if node.supernode == nil {
strongSelf.addSubnode(node)
if animated {
@ -892,7 +882,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
func reactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? {
for node in self.reactionNodes {
return (node.emptyImageNode, node.selectedImageNode)
return (node.selectedImageNode, node.selectedImageNode)
}
return nil
}

View File

@ -255,6 +255,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
self?.openPeerMention(name)
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in
self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame)
}, updateMessageReaction: { _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in

View File

@ -108,7 +108,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?
self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _ in }, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in }, navigationController: {

View File

@ -69,6 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openPeer: { _, _, _ in
}, openPeerMention: { _ in
}, openMessageContextMenu: { _, _, _, _, _ in
}, updateMessageReaction: { _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _ in

View File

@ -1819,6 +1819,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture)
strongSelf.controller?.window?.presentInGlobalOverlay(controller)
})
}, updateMessageReaction: { _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { [weak self] message, node, rect, gesture in
guard let strongSelf = self else {

View File

@ -1239,7 +1239,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let controllerInteraction: ChatControllerInteraction
controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, activateMessagePinch: { _ in
return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _ in }, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
}, tapMessage: { message in
tapMessage?(message)