mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reactions
This commit is contained in:
parent
2d7a45ccc1
commit
7115fee0fc
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -17,6 +17,8 @@ swift_library(
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -69,6 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, openPeer: { _, _, _ in
|
||||
}, openPeerMention: { _ in
|
||||
}, openMessageContextMenu: { _, _, _, _, _ in
|
||||
}, updateMessageReaction: { _ in
|
||||
}, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { _, _, _, _ in
|
||||
}, navigateToMessage: { _, _ in
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user