mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reaction improvements
This commit is contained in:
parent
2397f3c5b1
commit
c58b8c33c4
@ -294,3 +294,172 @@ public final class AnimatedAvatarSetNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class AnimatedAvatarSetView: UIView {
|
||||
private final class ContentView: UIView {
|
||||
private let unclippedView: UIImageView
|
||||
private let clippedView: UIImageView
|
||||
|
||||
private var size: CGSize
|
||||
private var spacing: CGFloat
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
init(context: AccountContext, peer: EnginePeer?, placeholderColor: UIColor, synchronousLoad: Bool, size: CGSize, spacing: CGFloat) {
|
||||
self.size = size
|
||||
self.spacing = spacing
|
||||
|
||||
self.unclippedView = UIImageView()
|
||||
self.clippedView = UIImageView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubview(self.unclippedView)
|
||||
self.addSubview(self.clippedView)
|
||||
|
||||
if let peer = peer {
|
||||
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
|
||||
let image = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor.lightGray.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})!
|
||||
self.updateImage(image: image, size: size, spacing: spacing)
|
||||
|
||||
let disposable = (signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] imageVersions in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let image = imageVersions?.0
|
||||
if let image = image {
|
||||
strongSelf.updateImage(image: image, size: size, spacing: spacing)
|
||||
}
|
||||
})
|
||||
self.disposable = disposable
|
||||
} else {
|
||||
let image = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id)
|
||||
})!
|
||||
self.updateImage(image: image, size: size, spacing: spacing)
|
||||
}
|
||||
} else {
|
||||
let image = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(placeholderColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})!
|
||||
self.updateImage(image: image, size: size, spacing: spacing)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateImage(image: UIImage, size: CGSize, spacing: CGFloat) {
|
||||
self.unclippedView.image = image
|
||||
self.clippedView.image = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: spacing - size.width, dy: 0.0))
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, isClipped: Bool, animated: Bool) {
|
||||
self.unclippedView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.clippedView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if animated && self.unclippedView.alpha.isZero != self.clippedView.alpha.isZero {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
|
||||
transition.updateAlpha(layer: self.unclippedView.layer, alpha: isClipped ? 0.0 : 1.0)
|
||||
transition.updateAlpha(layer: self.clippedView.layer, alpha: isClipped ? 1.0 : 0.0)
|
||||
} else {
|
||||
self.unclippedView.alpha = isClipped ? 0.0 : 1.0
|
||||
self.clippedView.alpha = isClipped ? 1.0 : 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var contentViews: [AnimatedAvatarSetContext.Content.Item.Key: ContentView] = [:]
|
||||
|
||||
public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), customSpacing: CGFloat? = nil, animated: Bool, synchronousLoad: Bool) -> CGSize {
|
||||
var contentWidth: CGFloat = 0.0
|
||||
let contentHeight: CGFloat = itemSize.height
|
||||
|
||||
let spacing: CGFloat
|
||||
if let customSpacing = customSpacing {
|
||||
spacing = customSpacing
|
||||
} else {
|
||||
spacing = 10.0
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.2, curve: .easeInOut)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
|
||||
var index = 0
|
||||
for i in 0 ..< content.items.count {
|
||||
let (key, item) = content.items[i]
|
||||
|
||||
validKeys.append(key)
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
|
||||
|
||||
let itemView: ContentView
|
||||
if let current = self.contentViews[key] {
|
||||
itemView = current
|
||||
itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: animated)
|
||||
transition.updateFrame(layer: itemView.layer, frame: itemFrame)
|
||||
} else {
|
||||
itemView = ContentView(context: context, peer: item.peer, placeholderColor: item.placeholderColor, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing)
|
||||
self.addSubview(itemView)
|
||||
self.contentViews[key] = itemView
|
||||
itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
|
||||
itemView.frame = itemFrame
|
||||
if animated {
|
||||
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
itemView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
}
|
||||
}
|
||||
itemView.layer.zPosition = CGFloat(100 - i)
|
||||
contentWidth += itemSize.width - spacing
|
||||
index += 1
|
||||
}
|
||||
var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
|
||||
for key in self.contentViews.keys {
|
||||
if !validKeys.contains(key) {
|
||||
removeKeys.append(key)
|
||||
}
|
||||
}
|
||||
for key in removeKeys {
|
||||
guard let itemView = self.contentViews.removeValue(forKey: key) else {
|
||||
continue
|
||||
}
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemView] _ in
|
||||
itemView?.removeFromSuperview()
|
||||
})
|
||||
itemView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
return CGSize(width: contentWidth, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
@ -39,18 +39,21 @@ public final class ReactionButtonComponent: Component {
|
||||
}
|
||||
|
||||
public struct Colors: Equatable {
|
||||
public var background: UInt32
|
||||
public var foreground: UInt32
|
||||
public var stroke: UInt32
|
||||
public var deselectedBackground: UInt32
|
||||
public var selectedBackground: UInt32
|
||||
public var deselectedForeground: UInt32
|
||||
public var selectedForeground: UInt32
|
||||
|
||||
public init(
|
||||
background: UInt32,
|
||||
foreground: UInt32,
|
||||
stroke: UInt32
|
||||
deselectedBackground: UInt32,
|
||||
selectedBackground: UInt32,
|
||||
deselectedForeground: UInt32,
|
||||
selectedForeground: UInt32
|
||||
) {
|
||||
self.background = background
|
||||
self.foreground = foreground
|
||||
self.stroke = stroke
|
||||
self.deselectedBackground = deselectedBackground
|
||||
self.selectedBackground = selectedBackground
|
||||
self.deselectedForeground = deselectedForeground
|
||||
self.selectedForeground = selectedForeground
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,6 +102,7 @@ public final class ReactionButtonComponent: Component {
|
||||
public final class View: UIButton, ComponentTaggedView {
|
||||
public let iconView: UIImageView
|
||||
private let textView: ComponentHostView<Empty>
|
||||
private let measureTextView: ComponentHostView<Empty>
|
||||
|
||||
private var currentComponent: ReactionButtonComponent?
|
||||
|
||||
@ -111,6 +115,9 @@ public final class ReactionButtonComponent: Component {
|
||||
self.textView = ComponentHostView<Empty>()
|
||||
self.textView.isUserInteractionEnabled = false
|
||||
|
||||
self.measureTextView = ComponentHostView<Empty>()
|
||||
self.measureTextView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.iconView)
|
||||
@ -148,11 +155,11 @@ public final class ReactionButtonComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: ReactionButtonComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let sideInsets: CGFloat = 10.0
|
||||
let sideInsets: CGFloat = 8.0
|
||||
let height: CGFloat = 30.0
|
||||
let spacing: CGFloat = 2.0
|
||||
let spacing: CGFloat = 4.0
|
||||
|
||||
let defaultImageSize = CGSize(width: 20.0, height: 20.0)
|
||||
let defaultImageSize = CGSize(width: 22.0, height: 22.0)
|
||||
|
||||
let imageSize: CGSize
|
||||
if self.currentComponent?.reaction != component.reaction {
|
||||
@ -179,43 +186,55 @@ public final class ReactionButtonComponent: Component {
|
||||
|
||||
self.iconView.frame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
|
||||
|
||||
let textSize: CGSize
|
||||
if self.currentComponent?.count != component.count || self.currentComponent?.colors != component.colors {
|
||||
textSize = self.textView.update(
|
||||
let text = "\(component.count)"
|
||||
var measureText = ""
|
||||
for _ in 0 ..< text.count {
|
||||
measureText.append("0")
|
||||
}
|
||||
|
||||
let minTextWidth = self.measureTextView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: measureText,
|
||||
font: Font.regular(11.0),
|
||||
color: .black
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
).width + 2.0
|
||||
|
||||
let actualTextSize: CGSize
|
||||
if self.currentComponent?.count != component.count || self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected {
|
||||
actualTextSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: "\(component.count)",
|
||||
font: Font.regular(13.0),
|
||||
color: UIColor(argb: component.colors.foreground)
|
||||
text: text,
|
||||
font: Font.regular(11.0),
|
||||
color: UIColor(argb: component.isSelected ? component.colors.selectedForeground : component.colors.deselectedForeground)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
} else {
|
||||
textSize = self.textView.bounds.size
|
||||
}
|
||||
|
||||
if self.currentComponent?.colors != component.colors {
|
||||
self.backgroundColor = UIColor(argb: component.colors.background)
|
||||
actualTextSize = self.textView.bounds.size
|
||||
}
|
||||
let layoutTextSize = CGSize(width: max(actualTextSize.width, minTextWidth), height: actualTextSize.height)
|
||||
|
||||
if self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected {
|
||||
if component.isSelected {
|
||||
self.layer.borderColor = UIColor(argb: component.colors.stroke).cgColor
|
||||
self.layer.borderWidth = 1.5
|
||||
self.backgroundColor = UIColor(argb: component.colors.selectedBackground)
|
||||
} else {
|
||||
self.layer.borderColor = nil
|
||||
self.layer.borderWidth = 0.0
|
||||
self.backgroundColor = UIColor(argb: component.colors.deselectedBackground)
|
||||
}
|
||||
}
|
||||
|
||||
self.layer.cornerRadius = height / 2.0
|
||||
|
||||
self.textView.frame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - textSize.height) / 2.0)), size: textSize)
|
||||
self.textView.frame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - actualTextSize.height) / 2.0)), size: actualTextSize)
|
||||
|
||||
self.currentComponent = component
|
||||
|
||||
return CGSize(width: imageSize.width + spacing + textSize.width + sideInsets * 2.0, height: height)
|
||||
return CGSize(width: imageSize.width + spacing + layoutTextSize.width + sideInsets * 2.0, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,7 +291,20 @@ public final class ReactionButtonsLayoutContainer {
|
||||
var removedViews: [ComponentHostView<Empty>] = []
|
||||
|
||||
var validIds = Set<String>()
|
||||
for reaction in reactions {
|
||||
for reaction in reactions.sorted(by: { lhs, rhs in
|
||||
var lhsCount = lhs.count
|
||||
if lhs.isSelected {
|
||||
lhsCount -= 1
|
||||
}
|
||||
var rhsCount = rhs.count
|
||||
if rhs.isSelected {
|
||||
rhsCount -= 1
|
||||
}
|
||||
if lhsCount != rhsCount {
|
||||
return lhsCount > rhsCount
|
||||
}
|
||||
return lhs.reaction.value < rhs.reaction.value
|
||||
}) {
|
||||
validIds.insert(reaction.reaction.value)
|
||||
|
||||
let view: ComponentHostView<Empty>
|
||||
|
@ -360,6 +360,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
strongSelf.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
|
||||
if let reactionContextNode = strongSelf.reactionContextNode {
|
||||
let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view)
|
||||
let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction
|
||||
if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue {
|
||||
strongSelf.highlightedReaction = highlightedReaction
|
||||
strongSelf.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -374,6 +383,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
strongSelf.highlightedActionNode = nil
|
||||
highlightedActionNode.performAction()
|
||||
}
|
||||
if let highlightedReaction = strongSelf.highlightedReaction {
|
||||
strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction)
|
||||
}
|
||||
} else {
|
||||
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
||||
strongSelf.highlightedActionNode = nil
|
||||
@ -417,6 +429,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
strongSelf.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
|
||||
if let reactionContextNode = strongSelf.reactionContextNode {
|
||||
let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view)
|
||||
let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction
|
||||
if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue {
|
||||
strongSelf.highlightedReaction = highlightedReaction
|
||||
strongSelf.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -431,6 +452,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
strongSelf.highlightedActionNode = nil
|
||||
highlightedActionNode.performAction()
|
||||
}
|
||||
|
||||
if let highlightedReaction = strongSelf.highlightedReaction {
|
||||
strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction)
|
||||
}
|
||||
} else {
|
||||
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
||||
strongSelf.highlightedActionNode = nil
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import ObjCRuntimeUtils
|
||||
|
||||
public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
|
||||
case linear
|
||||
@ -388,11 +389,15 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
|
||||
func animatePositionWithKeyframes(node: ASDisplayNode, keyframes: [AnyObject], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animatePositionWithKeyframes(layer: node.layer, keyframes: keyframes, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
}
|
||||
|
||||
func animatePositionWithKeyframes(layer: CALayer, 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: removeOnCompletion, completion: { value in
|
||||
layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in
|
||||
completion?(value)
|
||||
})
|
||||
}
|
||||
@ -851,6 +856,28 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func animateTransformScale(layer: CALayer, from fromScale: CGPoint, to toScale: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let calculatedFrom: CGPoint
|
||||
let calculatedTo: CGPoint
|
||||
|
||||
calculatedFrom = fromScale
|
||||
calculatedTo = toScale
|
||||
|
||||
layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = view.layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
@ -1424,9 +1451,12 @@ public protocol ControlledTransitionAnimator: AnyObject {
|
||||
func finishAnimation()
|
||||
|
||||
func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?)
|
||||
func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?)
|
||||
func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?)
|
||||
func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?)
|
||||
func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?)
|
||||
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?)
|
||||
func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?)
|
||||
}
|
||||
|
||||
protocol AnyValueProviding {
|
||||
@ -1609,87 +1639,46 @@ final class ControlledTransitionProperty {
|
||||
|
||||
let layer: CALayer
|
||||
let path: String
|
||||
let keyPath: AnyKeyPath
|
||||
var fromValue: AnyValue
|
||||
let toValue: AnyValue
|
||||
private(set) var lastValue: AnyValue
|
||||
private let completion: ((Bool) -> Void)?
|
||||
|
||||
private var animator: AnyObject?
|
||||
|
||||
init<T: Equatable>(layer: CALayer, path: String, keyPath: ReferenceWritableKeyPath<CALayer, T>, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding {
|
||||
init<T: Equatable>(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding {
|
||||
self.layer = layer
|
||||
self.path = path
|
||||
self.keyPath = keyPath
|
||||
self.fromValue = fromValue.anyValue
|
||||
self.toValue = toValue.anyValue
|
||||
self.lastValue = self.fromValue
|
||||
self.completion = completion
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
layer[keyPath: keyPath] = fromValue
|
||||
let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: {
|
||||
layer[keyPath: keyPath] = toValue
|
||||
})
|
||||
self.animator = animator
|
||||
animator.pauseAnimation()
|
||||
layer[keyPath: keyPath] = toValue
|
||||
}
|
||||
|
||||
self.update(at: 0.0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if #available(iOS 10.0, *) {
|
||||
if let animator = self.animator as? UIViewPropertyAnimator {
|
||||
animator.stopAnimation(true)
|
||||
self.animator = nil
|
||||
}
|
||||
}
|
||||
self.layer.removeAnimation(forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
|
||||
}
|
||||
|
||||
func update(at fraction: CGFloat) {
|
||||
let value = self.fromValue.interpolate(toValue, fraction)
|
||||
self.lastValue = value
|
||||
//self.write(self.layer, value)
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
if let animator = self.animator as? UIViewPropertyAnimator {
|
||||
animator.fractionComplete = fraction
|
||||
}
|
||||
}
|
||||
|
||||
/*let animation = CABasicAnimation()
|
||||
let animation = CABasicAnimation(keyPath: self.path)
|
||||
animation.speed = 0.0
|
||||
animation.beginTime = CACurrentMediaTime() + 1000.0
|
||||
animation.timeOffset = 0.01
|
||||
animation.duration = 1.0
|
||||
animation.fillMode = .both
|
||||
animation.fromValue = value.nsValue
|
||||
animation.toValue = self.toValue.nsValue
|
||||
animation.keyPath = self.path
|
||||
if let previousAnimation = self.layer.animation(forKey: self.animationKey) {
|
||||
self.layer.removeAnimation(forKey: self.animationKey)
|
||||
let _ = previousAnimation
|
||||
}
|
||||
self.layer.add(animation, forKey: self.animationKey)*/
|
||||
animation.toValue = value.nsValue
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
animation.isRemovedOnCompletion = false
|
||||
self.layer.add(animation, forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
|
||||
}
|
||||
|
||||
func complete(atEnd: Bool) {
|
||||
if #available(iOS 10.0, *) {
|
||||
if let animator = self.animator as? UIViewPropertyAnimator {
|
||||
animator.stopAnimation(true)
|
||||
/*if atEnd {
|
||||
animator.finishAnimation(at: .current)
|
||||
}*/
|
||||
self.animator = nil
|
||||
}
|
||||
}
|
||||
|
||||
self.completion?(atEnd)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ControlledTransition {
|
||||
@available(iOS 10.0, *)
|
||||
public final class NativeAnimator: ControlledTransitionAnimator {
|
||||
public let duration: Double
|
||||
private let curve: ContainedViewLayoutTransitionCurve
|
||||
@ -1713,7 +1702,7 @@ public final class ControlledTransition {
|
||||
for j in 0 ..< other.animations.count {
|
||||
let otherAnimation = other.animations[j]
|
||||
|
||||
if animation.layer === otherAnimation.layer && animation.keyPath == otherAnimation.keyPath {
|
||||
if animation.layer === otherAnimation.layer && animation.path == otherAnimation.path {
|
||||
if animation.toValue == otherAnimation.toValue {
|
||||
removeAnimationIndices.append(i)
|
||||
} else {
|
||||
@ -1723,7 +1712,8 @@ public final class ControlledTransition {
|
||||
}
|
||||
|
||||
for j in removeOtherAnimationIndices.reversed() {
|
||||
other.animations.remove(at: j).complete(atEnd: false)
|
||||
let otherAnimation = other.animations.remove(at: j)
|
||||
otherAnimation.complete(atEnd: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1762,7 +1752,9 @@ public final class ControlledTransition {
|
||||
private func add(animation: ControlledTransitionProperty) {
|
||||
for i in 0 ..< self.animations.count {
|
||||
let otherAnimation = self.animations[i]
|
||||
if otherAnimation.layer === animation.layer && otherAnimation.keyPath == animation.keyPath {
|
||||
if otherAnimation.layer === animation.layer && otherAnimation.path == animation.path {
|
||||
let currentAnimation = self.animations[i]
|
||||
currentAnimation.complete(atEnd: false)
|
||||
self.animations.remove(at: i)
|
||||
break
|
||||
}
|
||||
@ -1771,25 +1763,56 @@ public final class ControlledTransition {
|
||||
}
|
||||
|
||||
public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) {
|
||||
if layer.opacity == Float(alpha) {
|
||||
return
|
||||
}
|
||||
let fromValue = layer.presentation()?.opacity ?? layer.opacity
|
||||
//layer.opacity = Float(alpha)
|
||||
layer.opacity = Float(alpha)
|
||||
self.add(animation: ControlledTransitionProperty(
|
||||
layer: layer,
|
||||
path: "opacity",
|
||||
keyPath: \.opacity,
|
||||
fromValue: fromValue,
|
||||
toValue: Float(alpha),
|
||||
completion: completion
|
||||
))
|
||||
}
|
||||
|
||||
public func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) {
|
||||
let t = layer.presentation()?.transform ?? layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
|
||||
if currentScale == scale {
|
||||
return
|
||||
}
|
||||
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
self.add(animation: ControlledTransitionProperty(
|
||||
layer: layer,
|
||||
path: "transform.scale",
|
||||
fromValue: currentScale,
|
||||
toValue: scale,
|
||||
completion: completion
|
||||
))
|
||||
}
|
||||
|
||||
public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) {
|
||||
self.add(animation: ControlledTransitionProperty(
|
||||
layer: layer,
|
||||
path: "transform.scale",
|
||||
fromValue: fromValue,
|
||||
toValue: toValue,
|
||||
completion: completion
|
||||
))
|
||||
}
|
||||
|
||||
public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) {
|
||||
if layer.position == position {
|
||||
return
|
||||
}
|
||||
let fromValue = layer.presentation()?.position ?? layer.position
|
||||
//layer.position = position
|
||||
layer.position = position
|
||||
self.add(animation: ControlledTransitionProperty(
|
||||
layer: layer,
|
||||
path: "position",
|
||||
keyPath: \.position,
|
||||
fromValue: fromValue,
|
||||
toValue: position,
|
||||
completion: completion
|
||||
@ -1797,12 +1820,14 @@ public final class ControlledTransition {
|
||||
}
|
||||
|
||||
public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) {
|
||||
if layer.bounds == bounds {
|
||||
return
|
||||
}
|
||||
let fromValue = layer.presentation()?.bounds ?? layer.bounds
|
||||
//layer.bounds = bounds
|
||||
layer.bounds = bounds
|
||||
self.add(animation: ControlledTransitionProperty(
|
||||
layer: layer,
|
||||
path: "bounds",
|
||||
keyPath: \.bounds,
|
||||
fromValue: fromValue,
|
||||
toValue: bounds,
|
||||
completion: completion
|
||||
@ -1810,20 +1835,21 @@ public final class ControlledTransition {
|
||||
}
|
||||
|
||||
public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) {
|
||||
if layer.frame == frame {
|
||||
self.updatePosition(layer: layer, position: frame.center, completion: completion)
|
||||
self.updateBounds(layer: layer, bounds: CGRect(origin: CGPoint(), size: frame.size), completion: nil)
|
||||
}
|
||||
|
||||
public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) {
|
||||
if layer.cornerRadius == cornerRadius {
|
||||
return
|
||||
}
|
||||
let fromValue = layer.presentation()?.frame ?? layer.frame
|
||||
if let presentation = layer.presentation(), presentation.frame != layer.frame {
|
||||
assert(true)
|
||||
}
|
||||
//layer.frame = frame
|
||||
let fromValue = layer.presentation()?.cornerRadius ?? layer.cornerRadius
|
||||
layer.cornerRadius = cornerRadius
|
||||
self.add(animation: ControlledTransitionProperty(
|
||||
layer: layer,
|
||||
path: "frame",
|
||||
keyPath: \.frame,
|
||||
path: "cornerRadius",
|
||||
fromValue: fromValue,
|
||||
toValue: frame,
|
||||
toValue: cornerRadius,
|
||||
completion: completion
|
||||
))
|
||||
}
|
||||
@ -1859,6 +1885,14 @@ public final class ControlledTransition {
|
||||
self.transition.updateAlpha(layer: layer, alpha: alpha, completion: completion)
|
||||
}
|
||||
|
||||
public func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) {
|
||||
self.transition.updateTransformScale(layer: layer, scale: scale, completion: completion)
|
||||
}
|
||||
|
||||
public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) {
|
||||
self.transition.animateTransformScale(layer: layer, from: CGPoint(x: fromValue, y: fromValue), to: CGPoint(x: toValue, y: toValue), completion: completion)
|
||||
}
|
||||
|
||||
public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) {
|
||||
self.transition.updatePosition(layer: layer, position: position, completion: completion)
|
||||
}
|
||||
@ -1870,6 +1904,10 @@ public final class ControlledTransition {
|
||||
public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) {
|
||||
self.transition.updateFrame(layer: layer, frame: frame, completion: completion)
|
||||
}
|
||||
|
||||
public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) {
|
||||
self.transition.updateCornerRadius(layer: layer, cornerRadius: cornerRadius, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
public let animator: ControlledTransitionAnimator
|
||||
@ -1877,13 +1915,14 @@ public final class ControlledTransition {
|
||||
|
||||
public init(
|
||||
duration: Double,
|
||||
curve: ContainedViewLayoutTransitionCurve
|
||||
curve: ContainedViewLayoutTransitionCurve,
|
||||
interactive: Bool
|
||||
) {
|
||||
self.legacyAnimator = LegacyAnimator(
|
||||
duration: duration,
|
||||
curve: curve
|
||||
)
|
||||
if #available(iOS 10.0, *) {
|
||||
if interactive {
|
||||
self.animator = NativeAnimator(
|
||||
duration: duration,
|
||||
curve: curve
|
||||
@ -1894,10 +1933,8 @@ public final class ControlledTransition {
|
||||
}
|
||||
|
||||
public func merge(with other: ControlledTransition) {
|
||||
if #available(iOS 10.0, *) {
|
||||
if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator {
|
||||
animator.merge(with: otherAnimator)
|
||||
}
|
||||
if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator {
|
||||
animator.merge(with: otherAnimator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1585,7 +1585,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
if updateAnimationIsCrossfade {
|
||||
updateAnimation = .Crossfade
|
||||
} else if updateAnimationIsAnimated {
|
||||
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring)
|
||||
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring, interactive: true)
|
||||
controlledTransition = transition
|
||||
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
|
||||
} else {
|
||||
@ -2048,7 +2048,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
var controlledTransition: ControlledTransition?
|
||||
let updateAnimation: ListViewItemUpdateAnimation
|
||||
if animated {
|
||||
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring)
|
||||
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring, interactive: true)
|
||||
controlledTransition = transition
|
||||
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
|
||||
} else {
|
||||
|
@ -251,6 +251,27 @@ public final class NavigationBackgroundNode: ASDisplayNode {
|
||||
effectView.clipsToBounds = !cornerRadius.isZero
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, cornerRadius: CGFloat = 0.0, animator: ControlledTransitionAnimator) {
|
||||
self.validLayout = (size, cornerRadius)
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
animator.updateFrame(layer: self.backgroundNode.layer, frame: contentFrame, completion: nil)
|
||||
if let effectView = self.effectView, effectView.frame != contentFrame {
|
||||
animator.updateFrame(layer: effectView.layer, frame: contentFrame, completion: nil)
|
||||
if let sublayers = effectView.layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
animator.updateFrame(layer: sublayer, frame: contentFrame, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animator.updateCornerRadius(layer: self.backgroundNode.layer, cornerRadius: cornerRadius, completion: nil)
|
||||
if let effectView = self.effectView {
|
||||
animator.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius, completion: nil)
|
||||
effectView.clipsToBounds = !cornerRadius.isZero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class NavigationBar: ASDisplayNode {
|
||||
|
@ -294,7 +294,7 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer,
|
||||
self.state = .ended
|
||||
case .waitForDoubleTap:
|
||||
self.state = .began
|
||||
let timer = Timer(timeInterval: 0.2, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.tapEvent), userInfo: nil, repeats: false)
|
||||
let timer = Timer(timeInterval: 0.16, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.tapEvent), userInfo: nil, repeats: false)
|
||||
self.timer = timer
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
case let .waitForHold(_, acceptTap):
|
||||
|
@ -13,7 +13,7 @@ public enum ReactionGestureItem {
|
||||
}
|
||||
|
||||
public final class ReactionContextItem {
|
||||
public struct Reaction {
|
||||
public struct Reaction: Equatable {
|
||||
public var rawValue: String
|
||||
|
||||
public init(rawValue: String) {
|
||||
@ -446,44 +446,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
return keyframes
|
||||
}
|
||||
|
||||
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
let itemFrame: CGRect = itemNode.frame
|
||||
let _ = itemFrame
|
||||
|
||||
let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil)
|
||||
|
||||
targetSnapshotView.frame = targetFrame
|
||||
@ -542,67 +510,65 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if itemNode.item.reaction.rawValue != value {
|
||||
continue
|
||||
}
|
||||
if let targetSnapshotView = targetView.snapshotContentTree() {
|
||||
if hideNode {
|
||||
targetView.isHidden = true
|
||||
}
|
||||
|
||||
itemNode.isExtracted = true
|
||||
let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view)
|
||||
let selfTargetRect = self.view.convert(targetView.bounds, from: targetView)
|
||||
|
||||
let expandedScale: CGFloat = 3.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 = expandedFrame.midX < self.bounds.width / 2.0
|
||||
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)
|
||||
|
||||
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: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id)))
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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, targetView: targetView, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: {
|
||||
mainAnimationCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
})
|
||||
return
|
||||
if hideNode {
|
||||
targetView.isHidden = true
|
||||
}
|
||||
|
||||
itemNode.isExtracted = true
|
||||
let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view)
|
||||
let selfTargetRect = self.view.convert(targetView.bounds, from: targetView)
|
||||
|
||||
let expandedScale: CGFloat = 3.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: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0))
|
||||
|
||||
let additionalAnimationNode = AnimatedStickerNode()
|
||||
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
|
||||
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)
|
||||
|
||||
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: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id)))
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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, targetView: targetView, hideNode: hideNode, completion: {
|
||||
mainAnimationCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
completion()
|
||||
}
|
||||
@ -638,6 +604,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func performReactionSelection(reaction: ReactionContextItem.Reaction) {
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.item.reaction == reaction {
|
||||
self.reactionSelected?(itemNode.item)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setHighlightedReaction(_ value: ReactionContextItem.Reaction?) {
|
||||
self.highlightedReaction = value
|
||||
if let (size, insets, anchorRect) = self.validLayout {
|
||||
@ -668,7 +643,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
}
|
||||
|
||||
public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
guard let sourceSnapshotView = targetView.snapshotContentTree(), let targetSnapshotView = targetView.snapshotContentTree() else {
|
||||
guard let sourceSnapshotView = targetView.snapshotContentTree() else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
@ -733,16 +708,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
})
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: {
|
||||
self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: {
|
||||
self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, hideNode: hideNode, completion: {
|
||||
mainAnimationCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
let itemFrame: CGRect = itemNode.frame
|
||||
let _ = itemFrame
|
||||
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil)
|
||||
|
||||
@ -793,3 +770,84 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
transition.animateOffsetAdditive(node: self, offset: -offset.y)
|
||||
}
|
||||
}
|
||||
|
||||
public final class StandaloneDismissReactionAnimation: ASDisplayNode {
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
public func animateReactionDismiss(sourceView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
guard let sourceSnapshotView = sourceView.snapshotContentTree() else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
if hideNode {
|
||||
sourceView.isHidden = true
|
||||
}
|
||||
|
||||
let sourceRect = self.view.convert(sourceView.bounds, from: sourceView)
|
||||
sourceSnapshotView.frame = sourceRect
|
||||
self.view.addSubview(sourceSnapshotView)
|
||||
|
||||
var targetOffset: CGFloat = 120.0
|
||||
if sourceRect.midX > self.bounds.width / 2.0 {
|
||||
targetOffset = -targetOffset
|
||||
}
|
||||
let targetPoint = CGPoint(x: sourceRect.midX + targetOffset, y: sourceRect.midY)
|
||||
|
||||
let hapticFeedback = self.hapticFeedback
|
||||
hapticFeedback.prepareImpact(.soft)
|
||||
|
||||
let keyframes = generateParabollicMotionKeyframes(from: sourceRect.center, to: targetPoint, elevation: 25.0)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .easeInOut)
|
||||
sourceSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.04, delay: 0.18 - 0.04, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak sourceSnapshotView, weak hapticFeedback] _ in
|
||||
sourceSnapshotView?.removeFromSuperview()
|
||||
hapticFeedback?.impact(.soft)
|
||||
completion()
|
||||
})
|
||||
transition.animatePositionWithKeyframes(layer: sourceSnapshotView.layer, keyframes: keyframes, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y)
|
||||
transition.animateOffsetAdditive(node: self, offset: -offset.y)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import MurMurHash32
|
||||
|
||||
func addSynchronizeEmojiKeywordsOperation(transaction: Transaction, inputLanguageCode: String, languageCode: String?, fromVersion: Int32?) {
|
||||
let tag = OperationLogTags.SynchronizeEmojiKeywords
|
||||
let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(1), id: PeerId.Id._internalFromInt64Value(Int64(murMurHashString32(inputLanguageCode))))
|
||||
let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(1), id: PeerId.Id._internalFromInt64Value(Int64(abs(murMurHashString32(inputLanguageCode)))))
|
||||
|
||||
var hasExistingOperation = false
|
||||
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag) { entry -> Bool in
|
||||
|
@ -1074,7 +1074,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.window?.presentInGlobalOverlay(controller)
|
||||
})
|
||||
}
|
||||
}, updateMessageReaction: { [weak self] initialMessage, value in
|
||||
}, updateMessageReaction: { [weak self] initialMessage, reaction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -1088,70 +1088,111 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
var updatedReaction: String? = value
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ReactionsMessageAttribute {
|
||||
for reaction in attribute.reactions {
|
||||
if reaction.value == updatedReaction {
|
||||
if reaction.isSelected {
|
||||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||||
guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else {
|
||||
return
|
||||
}
|
||||
guard item.message.id == message.id else {
|
||||
return
|
||||
}
|
||||
|
||||
var updatedReaction: String?
|
||||
switch reaction {
|
||||
case .default:
|
||||
updatedReaction = item.associatedData.defaultReaction
|
||||
case let .reaction(value):
|
||||
updatedReaction = value
|
||||
}
|
||||
|
||||
var removedReaction: String?
|
||||
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ReactionsMessageAttribute {
|
||||
for listReaction in attribute.reactions {
|
||||
switch reaction {
|
||||
case .default:
|
||||
if listReaction.isSelected {
|
||||
updatedReaction = nil
|
||||
removedReaction = listReaction.value
|
||||
}
|
||||
case let .reaction(value):
|
||||
if listReaction.value == value && listReaction.isSelected {
|
||||
updatedReaction = nil
|
||||
removedReaction = value
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||
if attribute.value != nil {
|
||||
switch reaction {
|
||||
case .default:
|
||||
updatedReaction = nil
|
||||
removedReaction = attribute.value
|
||||
case let .reaction(value):
|
||||
if attribute.value == value {
|
||||
updatedReaction = nil
|
||||
removedReaction = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||
if let current = attribute.value, current == updatedReaction {
|
||||
updatedReaction = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if 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
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let updatedReaction = updatedReaction, let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
|
||||
for reaction in availableReactions.reactions {
|
||||
if reaction.value == updatedReaction {
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem(
|
||||
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
|
||||
stillAnimation: reaction.selectAnimation,
|
||||
listAnimation: reaction.activateAnimation,
|
||||
applicationAnimation: reaction.effectAnimation
|
||||
))
|
||||
|
||||
strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation
|
||||
strongSelf.currentStandaloneReactionItemNode = itemNode
|
||||
|
||||
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
|
||||
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
|
||||
standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in
|
||||
standaloneReactionAnimation?.removeFromSupernode()
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let updatedReaction = updatedReaction {
|
||||
if strongSelf.selectPollOptionFeedback == nil {
|
||||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||||
}
|
||||
strongSelf.selectPollOptionFeedback?.tap()
|
||||
|
||||
itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
|
||||
for reaction in availableReactions.reactions {
|
||||
if reaction.value == updatedReaction {
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem(
|
||||
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
|
||||
stillAnimation: reaction.selectAnimation,
|
||||
listAnimation: reaction.activateAnimation,
|
||||
applicationAnimation: reaction.effectAnimation
|
||||
))
|
||||
|
||||
/*targetView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: 90.0)
|
||||
strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation
|
||||
strongSelf.currentStandaloneReactionItemNode = itemNode
|
||||
|
||||
if let strongSelf = self {
|
||||
if strongSelf.selectPollOptionFeedback == nil {
|
||||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||||
}
|
||||
strongSelf.selectPollOptionFeedback?.tap()
|
||||
}*/
|
||||
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
|
||||
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
|
||||
standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in
|
||||
standaloneReactionAnimation?.removeFromSupernode()
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) {
|
||||
var hideRemovedReaction: Bool = false
|
||||
if let reactions = mergedMessageReactions(attributes: message.attributes) {
|
||||
for reaction in reactions.reactions {
|
||||
if reaction.value == removedReaction {
|
||||
hideRemovedReaction = reaction.count == 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let standaloneDismissAnimation = StandaloneDismissReactionAnimation()
|
||||
standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds
|
||||
strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation)
|
||||
standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in
|
||||
standaloneDismissAnimation?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start()
|
||||
}
|
||||
|
||||
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start()
|
||||
}, activateMessagePinch: { [weak self] sourceNode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
@ -46,12 +46,17 @@ public enum ChatControllerInteractionSwipeAction {
|
||||
case reply
|
||||
}
|
||||
|
||||
public enum ChatControllerInteractionReaction {
|
||||
case `default`
|
||||
case reaction(String)
|
||||
}
|
||||
|
||||
public final class ChatControllerInteraction {
|
||||
let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
|
||||
let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void
|
||||
let openPeerMention: (String) -> Void
|
||||
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void
|
||||
let updateMessageReaction: (Message, String) -> Void
|
||||
let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void
|
||||
let activateMessagePinch: (PinchSourceContainerNode) -> Void
|
||||
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||
let navigateToMessage: (MessageId, MessageId) -> Void
|
||||
@ -148,7 +153,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, String) -> Void,
|
||||
updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void,
|
||||
activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void,
|
||||
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
||||
navigateToMessage: @escaping (MessageId, MessageId) -> Void,
|
||||
|
@ -38,8 +38,6 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
|
||||
self.addSubnode(self.backgroundBlurNode)
|
||||
self.addSubnode(self.accessibilityArea)
|
||||
|
||||
//self.backgroundBlurNode.view.mask = backgroundMaskNode.view
|
||||
|
||||
self.accessibilityArea.activate = { [weak self] in
|
||||
self?.buttonPressed()
|
||||
@ -85,7 +83,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) {
|
||||
class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) {
|
||||
let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||
|
||||
return { context, theme, bubbleCorners, strings, message, button, constrainedWidth, position in
|
||||
@ -142,12 +140,15 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
return (titleSize.size.width + sideInset + sideInset, { width in
|
||||
return (CGSize(width: width, height: 42.0), {
|
||||
return (CGSize(width: width, height: 42.0), { animation in
|
||||
var animation = animation
|
||||
|
||||
let node: ChatMessageActionButtonNode
|
||||
if let maybeNode = maybeNode {
|
||||
node = maybeNode
|
||||
} else {
|
||||
node = ChatMessageActionButtonNode()
|
||||
animation = .None
|
||||
}
|
||||
|
||||
node.button = button
|
||||
@ -160,10 +161,10 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
node.backgroundMaskNode.image = backgroundMaskImage
|
||||
node.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0))
|
||||
animation.animator.updateFrame(layer: node.backgroundMaskNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil)
|
||||
|
||||
node.backgroundBlurNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0))
|
||||
node.backgroundBlurNode.update(size: node.backgroundBlurNode.bounds.size, cornerRadius: bubbleCorners.auxiliaryRadius, transition: .immediate)
|
||||
animation.animator.updateFrame(layer: node.backgroundBlurNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil)
|
||||
node.backgroundBlurNode.update(size: node.backgroundBlurNode.bounds.size, cornerRadius: bubbleCorners.auxiliaryRadius, animator: animation.animator)
|
||||
node.backgroundBlurNode.updateColor(color: selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper), transition: .immediate)
|
||||
|
||||
if iconImage != nil {
|
||||
@ -185,11 +186,17 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
node.addSubnode(titleNode)
|
||||
titleNode.isUserInteractionEnabled = false
|
||||
}
|
||||
titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size)
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size)
|
||||
titleNode.layer.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
animation.animator.updatePosition(layer: titleNode.layer, position: titleFrame.center, completion: nil)
|
||||
|
||||
node.buttonView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
|
||||
node.iconNode?.frame = CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0)
|
||||
if let buttonView = node.buttonView {
|
||||
animation.animator.updateFrame(layer: buttonView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil)
|
||||
}
|
||||
if let iconNode = node.iconNode {
|
||||
animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil)
|
||||
}
|
||||
|
||||
node.accessibilityArea.accessibilityLabel = title
|
||||
node.accessibilityArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
|
||||
@ -225,7 +232,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) {
|
||||
class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) {
|
||||
let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? []
|
||||
|
||||
return { context, theme, chatBubbleCorners, strings, replyMarkup, message, constrainedWidth in
|
||||
@ -234,14 +241,14 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
|
||||
var overallMinimumRowWidth: CGFloat = 0.0
|
||||
|
||||
var finalizeRowLayouts: [[((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))]] = []
|
||||
var finalizeRowLayouts: [[((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))]] = []
|
||||
|
||||
var rowIndex = 0
|
||||
var buttonIndex = 0
|
||||
for row in replyMarkup.rows {
|
||||
var maximumRowButtonWidth: CGFloat = 0.0
|
||||
let maximumButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count)))
|
||||
var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))] = []
|
||||
var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))] = []
|
||||
var rowButtonIndex = 0
|
||||
for button in row.buttons {
|
||||
let buttonPosition: MessageBubbleActionButtonPosition
|
||||
@ -259,7 +266,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
buttonPosition = .middle
|
||||
}
|
||||
|
||||
let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode)))
|
||||
let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)))
|
||||
if buttonIndex < currentButtonLayouts.count {
|
||||
prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, message, button, maximumButtonWidth, buttonPosition)
|
||||
} else {
|
||||
@ -280,7 +287,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
return (min(constrainedWidth, overallMinimumRowWidth), { constrainedWidth in
|
||||
var buttonFramesAndApply: [(CGRect, () -> ChatMessageActionButtonNode)] = []
|
||||
var buttonFramesAndApply: [(CGRect, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)] = []
|
||||
|
||||
var verticalRowOffset: CGFloat = 0.0
|
||||
verticalRowOffset += buttonSpacing
|
||||
@ -303,7 +310,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
verticalRowOffset = max(0.0, verticalRowOffset - buttonSpacing)
|
||||
}
|
||||
|
||||
return (CGSize(width: constrainedWidth, height: verticalRowOffset), { animated in
|
||||
return (CGSize(width: constrainedWidth, height: verticalRowOffset), { animation in
|
||||
let node: ChatMessageActionButtonsNode
|
||||
if let maybeNode = maybeNode {
|
||||
node = maybeNode
|
||||
@ -314,13 +321,15 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
var updatedButtons: [ChatMessageActionButtonNode] = []
|
||||
var index = 0
|
||||
for (buttonFrame, buttonApply) in buttonFramesAndApply {
|
||||
let buttonNode = buttonApply()
|
||||
buttonNode.frame = buttonFrame
|
||||
let buttonNode = buttonApply(animation)
|
||||
updatedButtons.append(buttonNode)
|
||||
if buttonNode.supernode == nil {
|
||||
node.addSubnode(buttonNode)
|
||||
buttonNode.pressed = node.buttonPressedWrapper
|
||||
buttonNode.longTapped = node.buttonLongTappedWrapper
|
||||
buttonNode.frame = buttonFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
@ -345,12 +354,6 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
}
|
||||
node.buttonNodes = updatedButtons
|
||||
|
||||
if animated {
|
||||
/*UIView.transition(with: node.view, duration: 0.2, options: [.transitionCrossDissolve], animations: {
|
||||
|
||||
}, completion: nil)*/
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
})
|
||||
|
@ -199,6 +199,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
private var replyBackgroundNode: NavigationBackgroundNode?
|
||||
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
@ -341,7 +342,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
}
|
||||
return .waitForSingleTap
|
||||
return .waitForDoubleTap
|
||||
}
|
||||
recognizer.longTap = { [weak self] point, recognizer in
|
||||
guard let strongSelf = self else {
|
||||
@ -701,6 +702,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
let imageLayout = self.imageNode.asyncLayout()
|
||||
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||
|
||||
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
||||
|
||||
@ -896,7 +898,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .standalone,
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -1021,22 +1023,53 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
var maxContentWidth = imageSize.width
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||
if let replyMarkup = replyMarkup {
|
||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
actionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
||||
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
let reactions: ReactionsMessageAttribute
|
||||
if shouldDisplayInlineDateReactions(message: item.message) {
|
||||
reactions = ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
} else {
|
||||
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
}
|
||||
var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))?
|
||||
if !reactions.reactions.isEmpty {
|
||||
let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right
|
||||
|
||||
let maxReactionsWidth = params.width - totalInset
|
||||
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: reactions,
|
||||
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
|
||||
constrainedWidth: maxReactionsWidth
|
||||
))
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
reactionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?
|
||||
if let reactionButtonsFinalize = reactionButtonsFinalize {
|
||||
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
var layoutSize = CGSize(width: params.width, height: contentHeight)
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
||||
}
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
layoutSize.height += reactionButtonsSizeAndApply.0.height
|
||||
}
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
|
||||
if let strongSelf = self {
|
||||
@ -1122,8 +1155,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.shareButtonNode = nil
|
||||
}
|
||||
|
||||
dateAndStatusApply(.None)
|
||||
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize)
|
||||
let dateAndStatusFrame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize)
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||
dateAndStatusApply(animation)
|
||||
|
||||
if needsReplyBackground {
|
||||
if let replyBackgroundNode = strongSelf.replyBackgroundNode {
|
||||
@ -1272,13 +1306,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
var animated = false
|
||||
if let _ = strongSelf.actionButtonsNode {
|
||||
if case .System = animation {
|
||||
animated = true
|
||||
}
|
||||
}
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||
let previousFrame = actionButtonsNode.frame
|
||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: actionButtonsSizeAndApply.0)
|
||||
actionButtonsNode.frame = actionButtonsFrame
|
||||
@ -1305,6 +1333,36 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.actionButtonsNode = nil
|
||||
}
|
||||
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation)
|
||||
let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: reactionButtonsSizeAndApply.0)
|
||||
if reactionButtonsNode !== strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = reactionButtonsNode
|
||||
reactionButtonsNode.reactionSelected = { value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
reactionButtonsNode.frame = reactionButtonsFrame
|
||||
strongSelf.addSubnode(reactionButtonsNode)
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateIn(animation: animation)
|
||||
}
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil)
|
||||
}
|
||||
} else if let reactionButtonsNode = strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = nil
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in
|
||||
reactionButtonsNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
reactionButtonsNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||
strongSelf.dateAndStatusNode.pressed = {
|
||||
guard let strongSelf = self else {
|
||||
@ -1329,7 +1387,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
if let item = self.item, let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: recognizer) {
|
||||
if case .doubleTap = gesture {
|
||||
self.containerNode.cancelGesture()
|
||||
@ -1340,10 +1398,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
case let .optionalAction(f):
|
||||
f()
|
||||
case let .openContextMenu(tapMessage, selectAll, subFrame):
|
||||
self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
|
||||
if canAddMessageReactions(message: item.message) {
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .default)
|
||||
} else {
|
||||
item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
|
||||
}
|
||||
}
|
||||
} else if case .tap = gesture {
|
||||
self.item?.controllerInteraction.clickThroughMessage()
|
||||
item.controllerInteraction.clickThroughMessage()
|
||||
}
|
||||
}
|
||||
default:
|
||||
@ -1916,6 +1978,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
return shareButtonNode.view
|
||||
}
|
||||
|
||||
if let reactionButtonsNode = self.reactionButtonsNode {
|
||||
if let result = reactionButtonsNode.hitTest(self.view.convert(point, to: reactionButtonsNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@ -2225,6 +2293,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
override func targetReactionView(value: String) -> UIView? {
|
||||
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||
return result
|
||||
}
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionView(value: value)
|
||||
}
|
||||
|
@ -363,7 +363,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
|
||||
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)))?
|
||||
|
||||
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)?
|
||||
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)?
|
||||
|
||||
let string = NSMutableAttributedString()
|
||||
var notEmpty = false
|
||||
@ -634,7 +634,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: textStatusType,
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: nil),
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -797,11 +797,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
adjustedLineHeight += statusSizeAndApply.0.height
|
||||
}
|
||||
|
||||
/*var adjustedStatusFrame: CGRect?
|
||||
if statusInText, let statusFrame = statusFrame {
|
||||
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size)
|
||||
}*/
|
||||
|
||||
adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width)
|
||||
|
||||
return (adjustedBoundingSize, { [weak self] animation, synchronousLoads in
|
||||
@ -811,17 +806,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
strongSelf.media = mediaAndFlags?.0
|
||||
strongSelf.theme = presentationData.theme
|
||||
|
||||
var hasAnimation = true
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
switch animation {
|
||||
case .None, .Crossfade:
|
||||
hasAnimation = false
|
||||
case let .System(duration, _):
|
||||
hasAnimation = true
|
||||
transition = .animated(duration: duration, curve: .easeInOut)
|
||||
}
|
||||
let _ = hasAnimation
|
||||
|
||||
strongSelf.lineNode.image = lineImage
|
||||
strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0))
|
||||
strongSelf.lineNode.isHidden = !displayLine
|
||||
@ -864,6 +848,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
strongSelf.openMedia?(mode)
|
||||
}
|
||||
}
|
||||
contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in
|
||||
guard let controllerInteraction = controllerInteraction else {
|
||||
return
|
||||
}
|
||||
controllerInteraction.updateMessageReaction(message, value)
|
||||
}
|
||||
contentImageNode.visibility = strongSelf.visibility != .none
|
||||
}
|
||||
let _ = contentImageApply(animation, synchronousLoads)
|
||||
@ -924,7 +914,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
|
||||
if let (videoLayout, apply) = contentInstantVideoSizeAndApply {
|
||||
contentMediaHeight = videoLayout.contentSize.height
|
||||
let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), transition)
|
||||
let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation)
|
||||
if strongSelf.contentInstantVideoNode !== contentInstantVideoNode {
|
||||
strongSelf.contentInstantVideoNode = contentInstantVideoNode
|
||||
strongSelf.addSubnode(contentInstantVideoNode)
|
||||
@ -1090,6 +1080,24 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func reactionTargetView(value: String) -> UIView? {
|
||||
if !self.statusNode.isHidden {
|
||||
if let result = self.statusNode.reactionView(value: value) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
if let result = self.contentFileNode?.dateAndStatusNode.reactionView(value: value) {
|
||||
return result
|
||||
}
|
||||
if let result = self.contentImageNode?.dateAndStatusNode.reactionView(value: value) {
|
||||
return result
|
||||
}
|
||||
if let result = self.contentInstantVideoNode?.dateAndStatusNode.reactionView(value: value) {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||
return self.contentImageNode?.playMediaWithSound()
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat
|
||||
case .Right:
|
||||
topLeftCorner = .Corner(normalRadius)
|
||||
topRightCorner = .Corner(mergedRadius)
|
||||
case .Both:
|
||||
topLeftCorner = .Corner(mergedRadius)
|
||||
topRightCorner = .Corner(mergedRadius)
|
||||
}
|
||||
}
|
||||
case let .mosaic(position, _):
|
||||
@ -65,6 +68,9 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat
|
||||
case .Left:
|
||||
bottomLeftCorner = .Corner(mergedRadius)
|
||||
bottomRightCorner = .Corner(normalRadius)
|
||||
case .Both:
|
||||
bottomLeftCorner = .Corner(mergedRadius)
|
||||
bottomRightCorner = .Corner(mergedRadius)
|
||||
case let .None(status):
|
||||
let bubbleInsets: UIEdgeInsets
|
||||
if case .color = chatPresentationData.theme.wallpaper {
|
||||
|
@ -37,6 +37,7 @@ enum ChatMessageBubbleMergeStatus {
|
||||
case None(ChatMessageBubbleNoneMergeStatus)
|
||||
case Left
|
||||
case Right
|
||||
case Both
|
||||
}
|
||||
|
||||
enum ChatMessageBubbleRelativePosition {
|
||||
|
@ -46,7 +46,7 @@ private final class ChatMessageBubbleClippingNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], Bool) {
|
||||
private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], Bool, Bool) {
|
||||
var result: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)] = []
|
||||
var skipText = false
|
||||
var messageWithCaptionToAdd: (Message, ChatMessageEntryAttributes)?
|
||||
@ -56,10 +56,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
var previousItemIsFile = false
|
||||
var hasFiles = false
|
||||
|
||||
var needReactions = true
|
||||
|
||||
outer: for (message, itemAttributes) in item.content {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
break outer
|
||||
}
|
||||
}
|
||||
@ -86,6 +89,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
isFile = true
|
||||
hasFiles = true
|
||||
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing)))
|
||||
needReactions = false
|
||||
}
|
||||
} else if let action = media as? TelegramMediaAction {
|
||||
isAction = true
|
||||
@ -94,26 +98,33 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
} else {
|
||||
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
}
|
||||
needReactions = false
|
||||
} else if let _ = media as? TelegramMediaMap {
|
||||
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
} else if let _ = media as? TelegramMediaGame {
|
||||
skipText = true
|
||||
result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
break inner
|
||||
} else if let _ = media as? TelegramMediaInvoice {
|
||||
skipText = true
|
||||
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
break inner
|
||||
} else if let _ = media as? TelegramMediaContact {
|
||||
result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
} else if let _ = media as? TelegramMediaExpiredContent {
|
||||
result.removeAll()
|
||||
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
return (result, false)
|
||||
needReactions = false
|
||||
return (result, false, false)
|
||||
} else if let _ = media as? TelegramMediaPoll {
|
||||
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
} else if let _ = media as? TelegramMediaUnsupported {
|
||||
isUnsupportedMedia = true
|
||||
needReactions = false
|
||||
}
|
||||
previousItemIsFile = isFile
|
||||
}
|
||||
@ -130,6 +141,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
skipText = true
|
||||
} else {
|
||||
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: isFile ? .condensed : .default)))
|
||||
needReactions = false
|
||||
}
|
||||
} else {
|
||||
if case .group = item.content {
|
||||
@ -142,6 +154,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
if let webpage = media as? TelegramMediaWebpage {
|
||||
if case .Loaded = webpage.content {
|
||||
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
}
|
||||
break inner
|
||||
}
|
||||
@ -151,34 +164,39 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
result.removeAll()
|
||||
|
||||
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
}
|
||||
|
||||
if isUnsupportedMedia {
|
||||
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
}
|
||||
}
|
||||
|
||||
if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd {
|
||||
result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
}
|
||||
|
||||
if let additionalContent = item.additionalContent {
|
||||
switch additionalContent {
|
||||
case let .eventLogPreviousMessage(previousMessage):
|
||||
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
case let .eventLogPreviousDescription(previousMessage):
|
||||
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
case let .eventLogPreviousLink(previousMessage):
|
||||
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
}
|
||||
}
|
||||
|
||||
let firstMessage = item.content.firstMessage
|
||||
|
||||
if let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty {
|
||||
if result.last?.1 == ChatMessageWebpageBubbleContentNode.self || result.last?.1 == ChatMessagePollBubbleContentNode.self || result.last?.1 == ChatMessageContactBubbleContentNode.self {
|
||||
result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
|
||||
}
|
||||
let reactionsAreInline = shouldDisplayInlineDateReactions(message: firstMessage)
|
||||
if reactionsAreInline {
|
||||
needReactions = false
|
||||
}
|
||||
|
||||
if !isAction && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) {
|
||||
@ -222,12 +240,25 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
}
|
||||
}
|
||||
|
||||
if !reactionsAreInline, let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty {
|
||||
if result.last?.1 == ChatMessageWebpageBubbleContentNode.self ||
|
||||
result.last?.1 == ChatMessagePollBubbleContentNode.self ||
|
||||
result.last?.1 == ChatMessageContactBubbleContentNode.self {
|
||||
result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
} else if result.last?.1 == ChatMessageCommentFooterContentNode.self {
|
||||
result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1)
|
||||
needReactions = false
|
||||
}
|
||||
}
|
||||
|
||||
var needSeparateContainers = false
|
||||
if case .group = item.content, hasFiles {
|
||||
needSeparateContainers = true
|
||||
needReactions = false
|
||||
}
|
||||
|
||||
return (result, needSeparateContainers)
|
||||
return (result, needSeparateContainers, needReactions)
|
||||
}
|
||||
|
||||
private let chatMessagePeerIdColors: [UIColor] = [
|
||||
@ -428,6 +459,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
private(set) var contentNodes: [ChatMessageBubbleContentNode] = []
|
||||
private var mosaicStatusNode: ChatMessageDateAndStatusNode?
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||
|
||||
private var shareButtonNode: ChatMessageShareButton?
|
||||
|
||||
@ -890,6 +922,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
||||
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
|
||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||
|
||||
let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode)
|
||||
|
||||
@ -911,6 +944,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
forwardInfoLayout: forwardInfoLayout,
|
||||
replyInfoLayout: replyInfoLayout,
|
||||
actionButtonsLayout: actionButtonsLayout,
|
||||
reactionButtonsLayout: reactionButtonsLayout,
|
||||
mosaicStatusLayout: mosaicStatusLayout,
|
||||
layoutConstants: layoutConstants,
|
||||
currentItem: currentItem,
|
||||
@ -926,7 +960,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
||||
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
|
||||
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
||||
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)),
|
||||
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)),
|
||||
reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)),
|
||||
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
|
||||
layoutConstants: ChatMessageItemLayoutConstants,
|
||||
currentItem: ChatMessageItem?,
|
||||
@ -1160,7 +1195,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, BubbleItemAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))))] = []
|
||||
var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?
|
||||
|
||||
let (contentNodeMessagesAndClasses, needSeparateContainers) = contentNodeMessagesAndClassesForItem(item)
|
||||
let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item)
|
||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
||||
|
||||
@ -1239,9 +1274,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
var contentPropertiesAndLayouts: [(CGSize?, ChatMessageBubbleContentProperties, ChatMessageBubblePreparePosition, BubbleItemAttributes, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void)), UInt32?, Bool?)] = []
|
||||
|
||||
let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
|
||||
let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
|
||||
|
||||
var backgroundHiding: ChatMessageBubbleContentBackgroundHiding?
|
||||
var hasSolidWallpaper = false
|
||||
switch item.presentationData.theme.wallpaper {
|
||||
@ -1383,6 +1415,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
index += 1
|
||||
}
|
||||
|
||||
let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
|
||||
var bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing)
|
||||
|
||||
let bubbleReactions: ReactionsMessageAttribute
|
||||
if needReactions {
|
||||
bubbleReactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
} else {
|
||||
bubbleReactions = ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
}
|
||||
if !bubbleReactions.reactions.isEmpty {
|
||||
bottomNodeMergeStatus = .Both
|
||||
}
|
||||
|
||||
var currentCredibilityIconImage: UIImage?
|
||||
|
||||
var initialDisplayHeader = true
|
||||
@ -1538,7 +1583,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .standalone,
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -1714,13 +1759,27 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
var maxContentWidth: CGFloat = headerSize.width
|
||||
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||
if let replyMarkup = replyMarkup {
|
||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth)
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
actionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))?
|
||||
if !bubbleReactions.reactions.isEmpty {
|
||||
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: bubbleReactions,
|
||||
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
|
||||
constrainedWidth: maximumNodeWidth
|
||||
))
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
reactionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
for i in 0 ..< contentPropertiesAndLayouts.count {
|
||||
let (_, contentNodeProperties, preparePosition, _, contentNodeLayout, contentGroupId, itemSelection) = contentPropertiesAndLayouts[i]
|
||||
|
||||
@ -1744,7 +1803,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
case let .None(status):
|
||||
if position.contains(.top) && position.contains(.left) {
|
||||
switch status {
|
||||
case .Left:
|
||||
case .Left, .Both:
|
||||
topLeft = .mergedBubble
|
||||
case .Right:
|
||||
topLeft = .none(tail: false)
|
||||
@ -1759,7 +1818,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
switch status {
|
||||
case .Left:
|
||||
topRight = .none(tail: false)
|
||||
case .Right:
|
||||
case .Right, .Both:
|
||||
topRight = .mergedBubble
|
||||
case .None:
|
||||
topRight = .none(tail: false)
|
||||
@ -1795,7 +1854,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
case let .None(status):
|
||||
if position.contains(.bottom) && position.contains(.left) {
|
||||
switch status {
|
||||
case .Left:
|
||||
case .Left, .Both:
|
||||
bottomLeft = .mergedBubble
|
||||
case .Right:
|
||||
bottomLeft = .none(tail: false)
|
||||
@ -1814,7 +1873,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
switch status {
|
||||
case .Left:
|
||||
bottomRight = .none(tail: false)
|
||||
case .Right:
|
||||
case .Right, .Both:
|
||||
bottomRight = .mergedBubble
|
||||
case let .None(tailStatus):
|
||||
if case .Outgoing = tailStatus {
|
||||
@ -1970,11 +2029,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
contentSize.height += totalContentNodesHeight
|
||||
|
||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
||||
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?
|
||||
if let reactionButtonsFinalize = reactionButtonsFinalize {
|
||||
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
let minimalContentSize: CGSize
|
||||
if hideBackground {
|
||||
minimalContentSize = CGSize(width: 1.0, height: 1.0)
|
||||
@ -2007,6 +2071,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
let bubbleContentWidth = maxContentWidth - layoutConstants.bubble.edgeInset * 2.0 - (layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left)
|
||||
|
||||
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height)
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height
|
||||
}
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
||||
}
|
||||
@ -2032,7 +2099,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
if headerSize.height.isZero && contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false {
|
||||
updatedMergedBottom = .none
|
||||
}
|
||||
if actionButtonsSizeAndApply != nil {
|
||||
if actionButtonsSizeAndApply != nil || reactionButtonsSizeAndApply != nil {
|
||||
updatedMergedTop = .fullyMerged
|
||||
}
|
||||
}
|
||||
@ -2047,6 +2114,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
forwardAuthorSignature: forwardAuthorSignature,
|
||||
accessibilityData: accessibilityData,
|
||||
actionButtonsSizeAndApply: actionButtonsSizeAndApply,
|
||||
reactionButtonsSizeAndApply: reactionButtonsSizeAndApply,
|
||||
updatedMergedTop: updatedMergedTop,
|
||||
updatedMergedBottom: updatedMergedBottom,
|
||||
hideBackground: hideBackground,
|
||||
@ -2087,7 +2155,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
forwardSource: Peer?,
|
||||
forwardAuthorSignature: String?,
|
||||
accessibilityData: ChatMessageAccessibilityData,
|
||||
actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?,
|
||||
actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?,
|
||||
reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?,
|
||||
updatedMergedTop: ChatMessageMerge,
|
||||
updatedMergedBottom: ChatMessageMerge,
|
||||
hideBackground: Bool,
|
||||
@ -2141,7 +2210,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
|
||||
var forceBackgroundSide = false
|
||||
if actionButtonsSizeAndApply != nil {
|
||||
if actionButtonsSizeAndApply != nil || reactionButtonsSizeAndApply != nil {
|
||||
forceBackgroundSide = true
|
||||
} else if case .semanticallyMerged = updatedMergedTop {
|
||||
forceBackgroundSide = true
|
||||
@ -2632,14 +2701,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
||||
if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) {
|
||||
/*strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame)
|
||||
if let type = strongSelf.backgroundNode.type {
|
||||
if case .none = type {
|
||||
} else {
|
||||
strongSelf.clippingNode.clipsToBounds = true
|
||||
}
|
||||
}*/
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
|
||||
animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil)
|
||||
strongSelf.clippingNode.clipsToBounds = true
|
||||
@ -2651,6 +2712,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: animation)
|
||||
animation.animator.updateFrame(layer: strongSelf.backgroundWallpaperNode.layer, frame: backgroundFrame, completion: nil)
|
||||
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: animation.transition)
|
||||
strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: animation.transition)
|
||||
|
||||
if let type = strongSelf.backgroundNode.type {
|
||||
var incomingOffset: CGFloat = 0.0
|
||||
@ -2669,11 +2731,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
}
|
||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||
|
||||
/*if let item = strongSelf.item, let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true)
|
||||
animation.animator.updateFrame(layer: shareButtonNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize), completion: nil)
|
||||
}*/
|
||||
}
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let currentBackgroundFrame = strongSelf.backgroundNode.frame
|
||||
@ -2681,10 +2738,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
animation.animator.updateFrame(layer: shareButtonNode.layer, frame: CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize), completion: nil)
|
||||
}
|
||||
} else {
|
||||
if let _ = strongSelf.backgroundFrameTransition {
|
||||
/*if let _ = strongSelf.backgroundFrameTransition {
|
||||
strongSelf.animateFrameTransition(1.0, backgroundFrame.size.height)
|
||||
strongSelf.backgroundFrameTransition = nil
|
||||
}
|
||||
}*/
|
||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true)
|
||||
@ -2717,17 +2774,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.selectionNode?.frame = selectionFrame
|
||||
strongSelf.selectionNode?.updateLayout(size: selectionFrame.size, leftInset: params.leftInset)
|
||||
|
||||
var reactionButtonsOffset: CGFloat = 0.0
|
||||
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
var animated = false
|
||||
if let _ = strongSelf.actionButtonsNode {
|
||||
if case .System = animation {
|
||||
animated = true
|
||||
}
|
||||
}
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
||||
let previousFrame = actionButtonsNode.frame
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
|
||||
actionButtonsNode.frame = actionButtonsFrame
|
||||
if actionButtonsNode !== strongSelf.actionButtonsNode {
|
||||
strongSelf.actionButtonsNode = actionButtonsNode
|
||||
actionButtonsNode.buttonPressed = { [weak strongSelf] button in
|
||||
@ -2741,16 +2792,47 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
}
|
||||
strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||
actionButtonsNode.frame = actionButtonsFrame
|
||||
} else {
|
||||
if case let .System(duration, _) = animation {
|
||||
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: timingFunction)
|
||||
}
|
||||
animation.animator.updateFrame(layer: actionButtonsNode.layer, frame: actionButtonsFrame, completion: nil)
|
||||
}
|
||||
|
||||
reactionButtonsOffset += actionButtonsSizeAndApply.0.height
|
||||
} else if let actionButtonsNode = strongSelf.actionButtonsNode {
|
||||
actionButtonsNode.removeFromSupernode()
|
||||
strongSelf.actionButtonsNode = nil
|
||||
}
|
||||
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation)
|
||||
let reactionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY + reactionButtonsOffset + 4.0), size: reactionButtonsSizeAndApply.0)
|
||||
if reactionButtonsNode !== strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = reactionButtonsNode
|
||||
reactionButtonsNode.reactionSelected = { [weak strongSelf] value in
|
||||
guard let strongSelf = strongSelf, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
reactionButtonsNode.frame = reactionButtonsFrame
|
||||
strongSelf.addSubnode(reactionButtonsNode)
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateIn(animation: animation)
|
||||
}
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil)
|
||||
}
|
||||
} else if let reactionButtonsNode = strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = nil
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in
|
||||
reactionButtonsNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
reactionButtonsNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
let previousContextContentFrame = strongSelf.mainContextSourceNode.contentRect
|
||||
strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
|
||||
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
|
||||
@ -2876,8 +2958,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
case let .optionalAction(f):
|
||||
f()
|
||||
case let .openContextMenu(tapMessage, selectAll, subFrame):
|
||||
if canAddMessageReactions(message: tapMessage), let defaultReaction = item.associatedData.defaultReaction {
|
||||
item.controllerInteraction.updateMessageReaction(tapMessage, defaultReaction)
|
||||
if canAddMessageReactions(message: tapMessage) {
|
||||
item.controllerInteraction.updateMessageReaction(tapMessage, .default)
|
||||
} else {
|
||||
item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
|
||||
}
|
||||
@ -3713,6 +3795,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
|
||||
override func targetReactionView(value: String) -> UIView? {
|
||||
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||
return result
|
||||
}
|
||||
for contentNode in self.contentNodes {
|
||||
if let result = contentNode.reactionTargetView(value: value) {
|
||||
return result
|
||||
|
@ -105,12 +105,15 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
let displaySeparator: Bool
|
||||
let topOffset: CGFloat
|
||||
let topSeparatorOffset: CGFloat
|
||||
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
|
||||
displaySeparator = false
|
||||
topOffset = 2.0
|
||||
topSeparatorOffset = 0.0
|
||||
} else {
|
||||
displaySeparator = true
|
||||
topOffset = 0.0
|
||||
topOffset = 2.0
|
||||
topSeparatorOffset = 2.0
|
||||
}
|
||||
|
||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
||||
@ -374,7 +377,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
|
||||
strongSelf.separatorNode.isHidden = !displaySeparator
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.strokeInsets.left, y: -3.0), size: CGSize(width: boundingWidth - layoutConstants.bubble.strokeInsets.left - layoutConstants.bubble.strokeInsets.right, height: UIScreenPixel))
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.strokeInsets.left, y: -3.0 + topSeparatorOffset), size: CGSize(width: boundingWidth - layoutConstants.bubble.strokeInsets.left - layoutConstants.bubble.strokeInsets.right, height: UIScreenPixel))
|
||||
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: boundingSize.height))
|
||||
|
||||
|
@ -46,7 +46,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import ReactionButtonListComponent
|
||||
import WebPBinding
|
||||
|
||||
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
||||
if let _ = layer.animation(forKey: "clockFrameAnimation") {
|
||||
@ -45,59 +46,91 @@ private let reactionCountFont = Font.semibold(11.0)
|
||||
private let reactionFont = Font.regular(12.0)
|
||||
|
||||
private final class StatusReactionNode: ASDisplayNode {
|
||||
let selectedImageNode: ASImageNode
|
||||
let iconView: UIImageView
|
||||
|
||||
private let iconImageDisposable = MetaDisposable()
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
private var value: String?
|
||||
private var isSelected: Bool?
|
||||
|
||||
override init() {
|
||||
self.selectedImageNode = ASImageNode()
|
||||
self.selectedImageNode.displaysAsynchronously = false
|
||||
self.iconView = UIImageView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.selectedImageNode)
|
||||
self.view.addSubview(self.iconView)
|
||||
}
|
||||
|
||||
func update(type: ChatMessageDateAndStatusType, value: String, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) {
|
||||
deinit {
|
||||
self.iconImageDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(context: AccountContext, type: ChatMessageDateAndStatusType, value: String, file: TelegramMediaFile?, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) {
|
||||
if self.value != value {
|
||||
self.value = value
|
||||
|
||||
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)
|
||||
let defaultImageSize = CGSize(width: 19.0, height: 19.0)
|
||||
let imageSize: CGSize
|
||||
if let file = file {
|
||||
self.iconImageDisposable.set((context.account.postbox.mediaBox.resourceData(file.resource)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
if let image = WebP.convert(fromWebP: dataValue) {
|
||||
strongSelf.iconView.image = image
|
||||
}
|
||||
}
|
||||
}))
|
||||
imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize
|
||||
} else {
|
||||
imageSize = defaultImageSize
|
||||
}
|
||||
|
||||
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((defaultImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((defaultImageSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
struct ReactionSettings {
|
||||
struct TrailingReactionSettings {
|
||||
var displayInline: Bool
|
||||
var preferAdditionalInset: Bool
|
||||
|
||||
init(preferAdditionalInset: Bool) {
|
||||
init(displayInline: Bool, preferAdditionalInset: Bool) {
|
||||
self.displayInline = displayInline
|
||||
self.preferAdditionalInset = preferAdditionalInset
|
||||
}
|
||||
}
|
||||
|
||||
struct StandaloneReactionSettings {
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
enum LayoutInput {
|
||||
case trailingContent(contentWidth: CGFloat, reactionSettings: ReactionSettings?)
|
||||
case standalone
|
||||
case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?)
|
||||
case standalone(reactionSettings: StandaloneReactionSettings?)
|
||||
|
||||
var displayInlineReactions: Bool {
|
||||
switch self {
|
||||
case let .trailingContent(_, reactionSettings):
|
||||
if let reactionSettings = reactionSettings {
|
||||
return reactionSettings.displayInline
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .standalone(reactionSettings):
|
||||
if let _ = reactionSettings {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Arguments {
|
||||
@ -154,7 +187,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
private var clockMinNode: ASImageNode?
|
||||
private let dateNode: TextNode
|
||||
private var impressionIcon: ASImageNode?
|
||||
private var reactionNodes: [StatusReactionNode] = []
|
||||
private var reactionNodes: [String: StatusReactionNode] = [:]
|
||||
private let reactionButtonsContainer = ReactionButtonsLayoutContainer()
|
||||
private var reactionCountNode: TextNode?
|
||||
private var reactionButtonNode: HighlightTrackingButtonNode?
|
||||
@ -247,22 +280,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
switch arguments.type {
|
||||
case .BubbleIncoming, .ImageIncoming, .FreeIncoming:
|
||||
reactionColors = ReactionButtonComponent.Colors(
|
||||
background: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
foreground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
||||
stroke: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb
|
||||
deselectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
selectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||
deselectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
||||
selectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb
|
||||
)
|
||||
case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing:
|
||||
reactionColors = ReactionButtonComponent.Colors(
|
||||
background: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
foreground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
|
||||
stroke: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb
|
||||
deselectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
selectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||
deselectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
|
||||
selectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb
|
||||
)
|
||||
}
|
||||
|
||||
switch arguments.type {
|
||||
case .BubbleIncoming:
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor
|
||||
leftInset = 10.0
|
||||
leftInset = 5.0
|
||||
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||
clockFrameImage = graphics.clockBubbleIncomingFrameImage
|
||||
@ -278,7 +313,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
case let .BubbleOutgoing(status):
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
outgoingStatus = status
|
||||
leftInset = 10.0
|
||||
leftInset = 5.0
|
||||
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||
clockFrameImage = graphics.clockBubbleOutgoingFrameImage
|
||||
@ -524,13 +559,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||
|
||||
let reactionSize: CGFloat = 14.0
|
||||
let reactionSize: CGFloat = 19.0
|
||||
var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||
let reactionSpacing: CGFloat = -4.0
|
||||
let reactionTrailingSpacing: CGFloat = 4.0
|
||||
let reactionSpacing: CGFloat = 2.0
|
||||
let reactionTrailingSpacing: CGFloat = 6.0
|
||||
|
||||
var reactionInset: CGFloat = 0.0
|
||||
if !"".isEmpty && !arguments.reactions.isEmpty {
|
||||
if arguments.layoutInput.displayInlineReactions, !arguments.reactions.isEmpty {
|
||||
reactionInset = -1.0 + CGFloat(arguments.reactions.count) * reactionSize + CGFloat(arguments.reactions.count - 1) * reactionSpacing + reactionTrailingSpacing
|
||||
|
||||
var count = 0
|
||||
@ -547,9 +582,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
countString = "\(count)"
|
||||
}
|
||||
|
||||
let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += max(10.0, layoutAndApply.0.size.width) + 2.0
|
||||
reactionCountLayoutAndApply = layoutAndApply
|
||||
if count > arguments.reactions.count {
|
||||
let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += layoutAndApply.0.size.width + 4.0
|
||||
reactionCountLayoutAndApply = layoutAndApply
|
||||
}
|
||||
}
|
||||
|
||||
if arguments.replyCount > 0 {
|
||||
@ -599,7 +636,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
transition: .immediate
|
||||
)
|
||||
case let .trailingContent(contentWidth, reactionSettings):
|
||||
if let _ = reactionSettings {
|
||||
if let reactionSettings = reactionSettings, !reactionSettings.displayInline {
|
||||
reactionButtons = reactionButtonsContainer.update(
|
||||
context: arguments.context,
|
||||
action: { value in
|
||||
@ -686,11 +723,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
resultingHeight = 0.0
|
||||
}
|
||||
} else {
|
||||
var additionalVerticalInset: CGFloat = 0.0
|
||||
if let reactionSettings = reactionSettings {
|
||||
if reactionSettings.preferAdditionalInset {
|
||||
verticalReactionsInset = 5.0
|
||||
verticalReactionsInset = 8.0
|
||||
additionalVerticalInset += 1.0
|
||||
} else {
|
||||
verticalReactionsInset = 2.0
|
||||
verticalReactionsInset = 3.0
|
||||
}
|
||||
} else {
|
||||
verticalReactionsInset = 0.0
|
||||
@ -698,12 +737,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
|
||||
resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
|
||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height
|
||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height
|
||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0 + layoutSize.height
|
||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height + 3.0
|
||||
} else {
|
||||
resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
|
||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height
|
||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height
|
||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height + additionalVerticalInset
|
||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -717,7 +756,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
strongSelf.type = arguments.type
|
||||
strongSelf.layoutSize = layoutSize
|
||||
|
||||
var reactionButtonPosition = CGPoint(x: 0.0, y: verticalReactionsInset)
|
||||
var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset)
|
||||
for item in reactionButtons.items {
|
||||
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
||||
reactionButtonPosition.x = 0.0
|
||||
@ -773,7 +812,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
|
||||
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
|
||||
animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
|
||||
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: animation.transition)
|
||||
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, animator: animation.animator)
|
||||
} else {
|
||||
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
|
||||
strongSelf.blurredBackgroundNode = blurredBackgroundNode
|
||||
@ -789,6 +828,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
let _ = dateApply()
|
||||
|
||||
if let currentImpressionIcon = currentImpressionIcon {
|
||||
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
|
||||
currentImpressionIcon.displaysAsynchronously = false
|
||||
if currentImpressionIcon.image !== impressionImage {
|
||||
currentImpressionIcon.image = impressionImage
|
||||
@ -796,8 +836,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
if currentImpressionIcon.supernode == nil {
|
||||
strongSelf.impressionIcon = currentImpressionIcon
|
||||
strongSelf.addSubnode(currentImpressionIcon)
|
||||
currentImpressionIcon.frame = impressionIconFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: currentImpressionIcon.layer, frame: impressionIconFrame, completion: nil)
|
||||
}
|
||||
currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
|
||||
} else if let impressionIcon = strongSelf.impressionIcon {
|
||||
impressionIcon.removeFromSupernode()
|
||||
strongSelf.impressionIcon = nil
|
||||
@ -908,38 +950,49 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left
|
||||
if !"".isEmpty {
|
||||
for i in 0 ..< arguments.reactions.count {
|
||||
if arguments.layoutInput.displayInlineReactions {
|
||||
var validReactions = Set<String>()
|
||||
for reaction in arguments.reactions.sorted(by: { lhs, rhs in
|
||||
if lhs.isSelected != rhs.isSelected {
|
||||
if lhs.isSelected {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return lhs.value < rhs.value
|
||||
}
|
||||
}) {
|
||||
let node: StatusReactionNode
|
||||
var animateNode = true
|
||||
if strongSelf.reactionNodes.count > i {
|
||||
node = strongSelf.reactionNodes[i]
|
||||
if let current = strongSelf.reactionNodes[reaction.value] {
|
||||
node = current
|
||||
} else {
|
||||
animateNode = false
|
||||
node = StatusReactionNode()
|
||||
if strongSelf.reactionNodes.count > i {
|
||||
let previousNode = strongSelf.reactionNodes[i]
|
||||
if animation.isAnimated {
|
||||
previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in
|
||||
previousNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
previousNode.removeFromSupernode()
|
||||
strongSelf.reactionNodes[reaction.value] = node
|
||||
}
|
||||
validReactions.insert(reaction.value)
|
||||
|
||||
var iconFile: TelegramMediaFile?
|
||||
|
||||
if let availableReactions = arguments.availableReactions {
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if availableReaction.value == reaction.value {
|
||||
iconFile = availableReaction.staticIcon
|
||||
break
|
||||
}
|
||||
strongSelf.reactionNodes[i] = node
|
||||
} else {
|
||||
strongSelf.reactionNodes.append(node)
|
||||
}
|
||||
}
|
||||
|
||||
node.update(type: arguments.type, value: arguments.reactions[i].value, isSelected: arguments.reactions[i].isSelected, count: Int(arguments.reactions[i].count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false)
|
||||
node.update(context: arguments.context, type: arguments.type, value: reaction.value, file: iconFile, isSelected: reaction.isSelected, count: Int(reaction.count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false)
|
||||
if node.supernode == nil {
|
||||
strongSelf.addSubnode(node)
|
||||
if animation.isAnimated {
|
||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + verticalInset + 1.0), size: CGSize(width: reactionSize, height: reactionSize))
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + verticalInset - 2.0), size: CGSize(width: reactionSize, height: reactionSize))
|
||||
if animateNode {
|
||||
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
|
||||
} else {
|
||||
@ -950,18 +1003,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
if !arguments.reactions.isEmpty {
|
||||
reactionOffset += reactionTrailingSpacing
|
||||
}
|
||||
|
||||
for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count {
|
||||
let node = strongSelf.reactionNodes.removeLast()
|
||||
if animation.isAnimated {
|
||||
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
node?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
node.removeFromSupernode()
|
||||
|
||||
var removeIds: [String] = []
|
||||
for (id, node) in strongSelf.reactionNodes {
|
||||
if !validReactions.contains(id) {
|
||||
removeIds.append(id)
|
||||
if animation.isAnimated {
|
||||
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
node?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
node.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
strongSelf.reactionNodes.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
if let (layout, apply) = reactionCountLayoutAndApply {
|
||||
@ -974,7 +1033,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
}
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size)
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset - 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size)
|
||||
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
|
||||
reactionOffset += 1.0 + layout.size.width + 4.0
|
||||
} else if let reactionCountNode = strongSelf.reactionCountNode {
|
||||
@ -1068,6 +1127,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func reactionView(value: String) -> UIView? {
|
||||
for (id, node) in self.reactionNodes {
|
||||
if id == value {
|
||||
return node.iconView
|
||||
}
|
||||
}
|
||||
for (_, button) in self.reactionButtonsContainer.buttons {
|
||||
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View {
|
||||
return result.iconView
|
||||
@ -1092,3 +1156,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func shouldDisplayInlineDateReactions(message: Message) -> Bool {
|
||||
if message.id.peerId.namespace == Namespaces.Peer.CloudUser || message.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
private var replyBackgroundNode: NavigationBackgroundNode?
|
||||
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
@ -255,6 +256,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
let currentForwardBackgroundNode = self.forwardBackgroundNode
|
||||
|
||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||
|
||||
let currentItem = self.appliedItem
|
||||
let currentForwardInfo = self.appliedForwardInfo
|
||||
@ -527,22 +529,54 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
}
|
||||
|
||||
var maxContentWidth = normalDisplaySize.width
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||
if let replyMarkup = replyMarkup {
|
||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
actionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
||||
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
let reactions: ReactionsMessageAttribute
|
||||
if shouldDisplayInlineDateReactions(message: item.message) {
|
||||
reactions = ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
} else {
|
||||
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
}
|
||||
|
||||
var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))?
|
||||
if !reactions.reactions.isEmpty {
|
||||
let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right
|
||||
|
||||
let maxReactionsWidth = params.width - totalInset
|
||||
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: reactions,
|
||||
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
|
||||
constrainedWidth: maxReactionsWidth
|
||||
))
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
reactionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?
|
||||
if let reactionButtonsFinalize = reactionButtonsFinalize {
|
||||
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
var layoutSize = CGSize(width: params.width, height: videoLayout.contentSize.height)
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
||||
}
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
layoutSize.height += 6.0 + reactionButtonsSizeAndApply.0.height
|
||||
}
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
|
||||
if let strongSelf = self {
|
||||
@ -577,7 +611,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
let animating = (currentItem != nil && currentPlaying != isPlaying) || strongSelf.animatingHeight
|
||||
if !animating {
|
||||
strongSelf.interactiveVideoNode.frame = videoFrame
|
||||
videoApply(videoLayoutData, transition)
|
||||
videoApply(videoLayoutData, animation)
|
||||
}
|
||||
|
||||
if currentPlaying != isPlaying {
|
||||
@ -750,14 +784,38 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
}
|
||||
}
|
||||
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
var animated = false
|
||||
if let _ = strongSelf.actionButtonsNode {
|
||||
if case .System = animation {
|
||||
animated = true
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation)
|
||||
let reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSizeAndApply.0)
|
||||
if reactionButtonsNode !== strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = reactionButtonsNode
|
||||
reactionButtonsNode.reactionSelected = { value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
reactionButtonsNode.frame = reactionButtonsFrame
|
||||
strongSelf.addSubnode(reactionButtonsNode)
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateIn(animation: animation)
|
||||
}
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil)
|
||||
}
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
||||
} else if let reactionButtonsNode = strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = nil
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in
|
||||
reactionButtonsNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
reactionButtonsNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||
let previousFrame = actionButtonsNode.frame
|
||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY), size: actionButtonsSizeAndApply.0)
|
||||
actionButtonsNode.frame = actionButtonsFrame
|
||||
@ -784,6 +842,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
strongSelf.actionButtonsNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
if let (_, f) = strongSelf.awaitingAppliedReaction {
|
||||
strongSelf.awaitingAppliedReaction = nil
|
||||
|
||||
f()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1196,7 +1260,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
} else {
|
||||
videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0)
|
||||
}
|
||||
videoApply(videoLayoutData, .immediate)
|
||||
videoApply(videoLayoutData, .None)
|
||||
|
||||
if let shareButtonNode = self.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.frame.size
|
||||
@ -1247,6 +1311,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
}
|
||||
|
||||
override func targetReactionView(value: String) -> UIView? {
|
||||
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||
return result
|
||||
}
|
||||
if !self.interactiveVideoNode.dateAndStatusNode.isHidden {
|
||||
return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value)
|
||||
}
|
||||
|
@ -455,7 +455,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)),
|
||||
layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: !shouldDisplayInlineDateReactions(message: message))),
|
||||
constrainedSize: constrainedSize,
|
||||
availableReactions: associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -1132,8 +1132,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) {
|
||||
return result
|
||||
if self.dateAndStatusNode.supernode != nil {
|
||||
if let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
if !self.dateAndStatusNode.frame.height.isZero {
|
||||
if self.dateAndStatusNode.frame.contains(point) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
self.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) {
|
||||
func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void) {
|
||||
let previousFile = self.media
|
||||
|
||||
let currentItem = self.item
|
||||
@ -296,7 +296,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .standalone,
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude),
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -321,7 +321,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
|
||||
let result = ChatMessageInstantVideoItemLayoutResult(contentSize: contentSize, overflowLeft: 0.0, overflowRight: dateAndStatusOverflow ? 0.0 : (max(0.0, floorToScreenPixels(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width)))
|
||||
|
||||
return (result, { [weak self] layoutData, transition in
|
||||
return (result, { [weak self] layoutData, animation in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.videoFrame = displayVideoFrame
|
||||
@ -362,18 +362,18 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
dateAndStatusApply(.None)
|
||||
dateAndStatusApply(animation)
|
||||
switch layoutData {
|
||||
case let .unconstrained(width):
|
||||
let dateAndStatusOrigin: CGPoint
|
||||
if dateAndStatusOverflow {
|
||||
dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0)
|
||||
} else {
|
||||
dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height)
|
||||
}
|
||||
strongSelf.dateAndStatusNode.frame = CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize)
|
||||
case let .constrained(_, right):
|
||||
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize)
|
||||
case let .unconstrained(width):
|
||||
let dateAndStatusOrigin: CGPoint
|
||||
if dateAndStatusOverflow {
|
||||
dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0)
|
||||
} else {
|
||||
dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height)
|
||||
}
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil)
|
||||
case let .constrained(_, right):
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize), completion: nil)
|
||||
}
|
||||
|
||||
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
|
||||
@ -847,11 +847,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode) {
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode) {
|
||||
let makeLayout = node?.asyncLayout()
|
||||
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload in
|
||||
var createdNode: ChatMessageInteractiveInstantVideoNode?
|
||||
let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void)
|
||||
let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void)
|
||||
if let makeLayout = makeLayout {
|
||||
sizeAndApplyLayout = makeLayout(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload)
|
||||
} else {
|
||||
|
@ -91,7 +91,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
private var badgeNode: ChatMessageInteractiveMediaBadge?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
|
||||
private var context: AccountContext?
|
||||
private var message: Message?
|
||||
@ -150,6 +150,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
|
||||
var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in }
|
||||
var activatePinch: ((PinchSourceContainerNode) -> Void)?
|
||||
var updateMessageReaction: ((Message, ChatControllerInteractionReaction) -> Void)?
|
||||
|
||||
override init() {
|
||||
self.pinchContainerNode = PinchSourceContainerNode()
|
||||
@ -267,9 +268,18 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))
|
||||
self.imageNode.view.addGestureRecognizer(tapRecognizer)
|
||||
self.tapRecognizer = tapRecognizer
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))
|
||||
recognizer.tapActionAtPoint = { [weak self] point in
|
||||
guard let strongSelf = self else {
|
||||
return .fail
|
||||
}
|
||||
if !strongSelf.imageNode.bounds.contains(point) {
|
||||
return .fail
|
||||
}
|
||||
return .waitForDoubleTap
|
||||
}
|
||||
self.imageNode.view.addGestureRecognizer(recognizer)
|
||||
self.tapRecognizer = recognizer
|
||||
}
|
||||
|
||||
private func progressPressed(canActivate: Bool) {
|
||||
@ -320,26 +330,33 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
}
|
||||
|
||||
@objc func imageTap(_ recognizer: UITapGestureRecognizer) {
|
||||
@objc func imageTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let point = recognizer.location(in: self.imageNode.view)
|
||||
if let _ = self.attributes?.updatingMedia {
|
||||
if let statusNode = self.statusNode, statusNode.frame.contains(point) {
|
||||
self.progressPressed(canActivate: true)
|
||||
}
|
||||
} else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
|
||||
var videoContentMatch = true
|
||||
if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId {
|
||||
videoContentMatch = self.message?.stableId == stableId && self.media?.id == mediaId
|
||||
}
|
||||
self.activateLocalContent((self.automaticPlayback ?? false) && videoContentMatch ? .automaticPlayback : .default)
|
||||
} else {
|
||||
if let message = self.message, message.flags.isSending {
|
||||
if let statusNode = self.statusNode, statusNode.frame.contains(point) {
|
||||
self.progressPressed(canActivate: true)
|
||||
if let (gesture, point) = recognizer.lastRecognizedGestureAndLocation, let message = self.message {
|
||||
if case .doubleTap = gesture {
|
||||
if canAddMessageReactions(message: message) {
|
||||
self.updateMessageReaction?(message, .default)
|
||||
}
|
||||
} else {
|
||||
self.progressPressed(canActivate: true)
|
||||
if let _ = self.attributes?.updatingMedia {
|
||||
if let statusNode = self.statusNode, statusNode.frame.contains(point) {
|
||||
self.progressPressed(canActivate: true)
|
||||
}
|
||||
} else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
|
||||
var videoContentMatch = true
|
||||
if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId {
|
||||
videoContentMatch = self.message?.stableId == stableId && self.media?.id == mediaId
|
||||
}
|
||||
self.activateLocalContent((self.automaticPlayback ?? false) && videoContentMatch ? .automaticPlayback : .default)
|
||||
} else {
|
||||
if let message = self.message, message.flags.isSending {
|
||||
if let statusNode = self.statusNode, statusNode.frame.contains(point) {
|
||||
self.progressPressed(canActivate: true)
|
||||
}
|
||||
} else {
|
||||
self.progressPressed(canActivate: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -475,7 +492,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
impressionCount: dateAndStatus.viewCount,
|
||||
dateText: dateAndStatus.dateText,
|
||||
type: dateAndStatus.type,
|
||||
layoutInput: .standalone,
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
availableReactions: associatedData.availableReactions,
|
||||
reactions: dateAndStatus.dateReactions,
|
||||
@ -868,15 +885,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
imageApply()
|
||||
|
||||
if let statusApply = statusApply {
|
||||
let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
|
||||
statusApply(.None)
|
||||
strongSelf.dateAndStatusNode.frame = dateAndStatusFrame
|
||||
} else {
|
||||
transition.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||
statusApply(transition)
|
||||
}
|
||||
statusApply(transition)
|
||||
|
||||
let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
|
||||
|
||||
strongSelf.dateAndStatusNode.frame = dateAndStatusFrame
|
||||
strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size)
|
||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||
}
|
||||
|
@ -252,7 +252,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .standalone,
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude),
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
|
@ -51,6 +51,13 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.interactiveImageNode.updateMessageReaction = { [weak self] message, value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(message, value)
|
||||
}
|
||||
|
||||
self.interactiveImageNode.activatePinch = { [weak self] sourceNode in
|
||||
guard let strongSelf = self, let _ = strongSelf.item else {
|
||||
|
@ -1072,7 +1072,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: 100.0, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)),
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
|
@ -19,6 +19,11 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
||||
case freeform
|
||||
}
|
||||
|
||||
enum DisplayAlignment {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
private let container: ReactionButtonsLayoutContainer
|
||||
var reactionSelected: ((String) -> Void)?
|
||||
|
||||
@ -33,22 +38,32 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
||||
presentationData: ChatPresentationData,
|
||||
availableReactions: AvailableReactions?,
|
||||
reactions: ReactionsMessageAttribute,
|
||||
alignment: DisplayAlignment,
|
||||
constrainedWidth: CGFloat,
|
||||
type: DisplayType
|
||||
) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
|
||||
let reactionColors: ReactionButtonComponent.Colors
|
||||
switch type {
|
||||
case .incoming, .freeform:
|
||||
case .incoming:
|
||||
reactionColors = ReactionButtonComponent.Colors(
|
||||
background: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
foreground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
||||
stroke: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb
|
||||
deselectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
selectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||
deselectedForeground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
||||
selectedForeground: presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb
|
||||
)
|
||||
case .outgoing:
|
||||
reactionColors = ReactionButtonComponent.Colors(
|
||||
background: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
foreground: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
|
||||
stroke: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb
|
||||
deselectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||
selectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||
deselectedForeground: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
|
||||
selectedForeground: presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb
|
||||
)
|
||||
case .freeform:
|
||||
reactionColors = ReactionButtonComponent.Colors(
|
||||
deselectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb,
|
||||
selectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb,
|
||||
deselectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb,
|
||||
selectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb
|
||||
)
|
||||
}
|
||||
|
||||
@ -115,29 +130,53 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
||||
let bottomInset: CGFloat = 2.0
|
||||
|
||||
return (proposedWidth: reactionButtonsSize.width, continueLayout: { [weak self] boundingWidth in
|
||||
return (size: CGSize(width: boundingWidth, height: topInset + reactionButtonsSize.height + bottomInset), apply: { animation in
|
||||
let size = CGSize(width: boundingWidth, height: topInset + reactionButtonsSize.height + bottomInset)
|
||||
return (size: size, apply: { animation in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var reactionButtonPosition = CGPoint(x: 0.0, y: topInset)
|
||||
var reactionButtonPosition: CGPoint
|
||||
switch alignment {
|
||||
case .left:
|
||||
reactionButtonPosition = CGPoint(x: -1.0, y: topInset)
|
||||
case .right:
|
||||
reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset)
|
||||
}
|
||||
for item in reactionButtons.items {
|
||||
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
||||
reactionButtonPosition.x = 0.0
|
||||
reactionButtonPosition.y += item.size.height + 6.0
|
||||
switch alignment {
|
||||
case .left:
|
||||
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
||||
reactionButtonPosition.x = 0.0
|
||||
reactionButtonPosition.y += item.size.height + 6.0
|
||||
}
|
||||
case .right:
|
||||
if reactionButtonPosition.x - item.size.width < -1.0 {
|
||||
reactionButtonPosition.x = size.width + 1.0
|
||||
reactionButtonPosition.y += item.size.height + 6.0
|
||||
}
|
||||
}
|
||||
|
||||
let itemFrame: CGRect
|
||||
switch alignment {
|
||||
case .left:
|
||||
itemFrame = CGRect(origin: reactionButtonPosition, size: item.size)
|
||||
reactionButtonPosition.x += item.size.width + 6.0
|
||||
case .right:
|
||||
itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size)
|
||||
reactionButtonPosition.x -= item.size.width + 6.0
|
||||
}
|
||||
|
||||
if item.view.superview == nil {
|
||||
strongSelf.view.addSubview(item.view)
|
||||
item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size)
|
||||
if animation.isAnimated {
|
||||
item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
item.view.frame = itemFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil)
|
||||
animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil)
|
||||
}
|
||||
reactionButtonPosition.x += item.size.width + 6.0
|
||||
}
|
||||
|
||||
for view in reactionButtons.removedViews {
|
||||
@ -163,9 +202,15 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
func animateIn(animation: ListViewItemUpdateAnimation) {
|
||||
for (_, button) in self.container.buttons {
|
||||
button.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
animation.animator.animateScale(layer: button.layer, from: 0.01, to: 1.0, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(animation: ListViewItemUpdateAnimation) {
|
||||
for (_, button) in self.container.buttons {
|
||||
animation.animator.updateScale(layer: button.layer, scale: 0.01, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,7 +229,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,7 +247,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
||||
let topOffset: CGFloat
|
||||
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
|
||||
//displaySeparator = false
|
||||
topOffset = 2.0
|
||||
topOffset = 4.0
|
||||
} else {
|
||||
//displaySeparator = true
|
||||
topOffset = 0.0
|
||||
@ -213,7 +258,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
||||
let buttonsUpdate = buttonsNode.prepareUpdate(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, constrainedWidth: constrainedSize.width, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing)
|
||||
availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, alignment: .left, constrainedWidth: constrainedSize.width, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing)
|
||||
|
||||
return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in
|
||||
var boundingSize = CGSize()
|
||||
@ -250,7 +295,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
self.buttonsNode.animateOut()
|
||||
self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false)))
|
||||
}
|
||||
|
||||
override func animateInsertionIntoBubble(_ duration: Double) {
|
||||
@ -263,18 +308,18 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.buttonsNode.animateOut()
|
||||
self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false)))
|
||||
}
|
||||
|
||||
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
if self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil) != nil {
|
||||
if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil), result !== self.buttonsNode.view {
|
||||
return .ignore
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event) {
|
||||
if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
@ -284,3 +329,96 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
||||
return self.buttonsNode.reactionTargetView(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatMessageReactionButtonsNode: ASDisplayNode {
|
||||
final class Arguments {
|
||||
let context: AccountContext
|
||||
let presentationData: ChatPresentationData
|
||||
let availableReactions: AvailableReactions?
|
||||
let reactions: ReactionsMessageAttribute
|
||||
let isIncoming: Bool
|
||||
let constrainedWidth: CGFloat
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
presentationData: ChatPresentationData,
|
||||
availableReactions: AvailableReactions?,
|
||||
reactions: ReactionsMessageAttribute,
|
||||
isIncoming: Bool,
|
||||
constrainedWidth: CGFloat
|
||||
) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.availableReactions = availableReactions
|
||||
self.reactions = reactions
|
||||
self.isIncoming = isIncoming
|
||||
self.constrainedWidth = constrainedWidth
|
||||
}
|
||||
}
|
||||
|
||||
private let buttonsNode: MessageReactionButtonsNode
|
||||
|
||||
var reactionSelected: ((String) -> Void)?
|
||||
|
||||
override init() {
|
||||
self.buttonsNode = MessageReactionButtonsNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.buttonsNode)
|
||||
self.buttonsNode.reactionSelected = { [weak self] value in
|
||||
self?.reactionSelected?(value)
|
||||
}
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) {
|
||||
return { arguments in
|
||||
let node = maybeNode ?? ChatMessageReactionButtonsNode()
|
||||
|
||||
let buttonsUpdate = node.buttonsNode.prepareUpdate(
|
||||
context: arguments.context,
|
||||
presentationData: arguments.presentationData,
|
||||
availableReactions: arguments.availableReactions,
|
||||
reactions: arguments.reactions,
|
||||
alignment: arguments.isIncoming ? .left : .right,
|
||||
constrainedWidth: arguments.constrainedWidth,
|
||||
type: .freeform
|
||||
)
|
||||
|
||||
return (buttonsUpdate.proposedWidth, { constrainedWidth in
|
||||
let buttonsResult = buttonsUpdate.continueLayout(constrainedWidth)
|
||||
|
||||
return (CGSize(width: constrainedWidth, height: buttonsResult.size.height), { animation in
|
||||
node.buttonsNode.frame = CGRect(origin: CGPoint(), size: buttonsResult.size)
|
||||
buttonsResult.apply(animation)
|
||||
|
||||
return node
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(animation: ListViewItemUpdateAnimation) {
|
||||
self.buttonsNode.animateIn(animation: animation)
|
||||
self.buttonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
func animateOut(animation: ListViewItemUpdateAnimation, completion: @escaping () -> Void) {
|
||||
self.buttonsNode.animateOut(animation: animation)
|
||||
animation.animator.updateAlpha(layer: self.buttonsNode.layer, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
animation.animator.updateFrame(layer: self.buttonsNode.layer, frame: self.buttonsNode.layer.frame.offsetBy(dx: 0.0, dy: -self.buttonsNode.layer.bounds.height / 2.0), completion: nil)
|
||||
}
|
||||
|
||||
func reactionTargetView(value: String) -> UIView? {
|
||||
return self.buttonsNode.reactionTargetView(value: value)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)),
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: false)),
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
|
@ -45,6 +45,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
private var replyBackgroundNode: NavigationBackgroundNode?
|
||||
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
@ -178,7 +179,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
}
|
||||
return .waitForSingleTap
|
||||
return .waitForDoubleTap
|
||||
}
|
||||
recognizer.longTap = { [weak self] point, recognizer in
|
||||
guard let strongSelf = self else {
|
||||
@ -299,6 +300,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
let imageLayout = self.imageNode.asyncLayout()
|
||||
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||
let textLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
||||
@ -490,7 +492,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .standalone,
|
||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -619,22 +621,54 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
var maxContentWidth = imageSize.width
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||
if let replyMarkup = replyMarkup {
|
||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
actionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
||||
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
let reactions: ReactionsMessageAttribute
|
||||
if shouldDisplayInlineDateReactions(message: item.message) {
|
||||
reactions = ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
} else {
|
||||
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: [])
|
||||
}
|
||||
|
||||
var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))?
|
||||
if !reactions.reactions.isEmpty {
|
||||
let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right
|
||||
|
||||
let maxReactionsWidth = params.width - totalInset
|
||||
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: reactions,
|
||||
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
|
||||
constrainedWidth: maxReactionsWidth
|
||||
))
|
||||
maxContentWidth = max(maxContentWidth, minWidth)
|
||||
reactionButtonsFinalize = buttonsLayout
|
||||
}
|
||||
|
||||
var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?
|
||||
if let reactionButtonsFinalize = reactionButtonsFinalize {
|
||||
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
var layoutSize = CGSize(width: params.width, height: contentHeight)
|
||||
if isEmoji && !incoming {
|
||||
layoutSize.height += dateAndStatusSize.height
|
||||
}
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
layoutSize.height += reactionButtonsSizeAndApply.0.height
|
||||
}
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
||||
}
|
||||
@ -740,9 +774,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
||||
|
||||
dateAndStatusApply(.None)
|
||||
|
||||
transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: dateAndStatusFrame)
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||
dateAndStatusApply(animation)
|
||||
|
||||
if let updatedShareButtonNode = updatedShareButtonNode {
|
||||
if updatedShareButtonNode !== strongSelf.shareButtonNode {
|
||||
@ -902,13 +935,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||
var animated = false
|
||||
if let _ = strongSelf.actionButtonsNode {
|
||||
if case .System = animation {
|
||||
animated = true
|
||||
}
|
||||
}
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||
let previousFrame = actionButtonsNode.frame
|
||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: actionButtonsSizeAndApply.0)
|
||||
actionButtonsNode.frame = actionButtonsFrame
|
||||
@ -935,6 +962,36 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.actionButtonsNode = nil
|
||||
}
|
||||
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation)
|
||||
let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: reactionButtonsSizeAndApply.0)
|
||||
if reactionButtonsNode !== strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = reactionButtonsNode
|
||||
reactionButtonsNode.reactionSelected = { value in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
reactionButtonsNode.frame = reactionButtonsFrame
|
||||
strongSelf.addSubnode(reactionButtonsNode)
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateIn(animation: animation)
|
||||
}
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil)
|
||||
}
|
||||
} else if let reactionButtonsNode = strongSelf.reactionButtonsNode {
|
||||
strongSelf.reactionButtonsNode = nil
|
||||
if animation.isAnimated {
|
||||
reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in
|
||||
reactionButtonsNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
reactionButtonsNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||
strongSelf.dateAndStatusNode.pressed = {
|
||||
guard let strongSelf = self else {
|
||||
@ -963,7 +1020,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
if case .doubleTap = gesture {
|
||||
self.containerNode.cancelGesture()
|
||||
}
|
||||
if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: nil) {
|
||||
if let item = self.item, let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: nil) {
|
||||
if case .doubleTap = gesture {
|
||||
self.containerNode.cancelGesture()
|
||||
}
|
||||
@ -973,7 +1030,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
case let .optionalAction(f):
|
||||
f()
|
||||
case let .openContextMenu(tapMessage, selectAll, subFrame):
|
||||
self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
|
||||
if canAddMessageReactions(message: item.message) {
|
||||
item.controllerInteraction.updateMessageReaction(tapMessage, .default)
|
||||
} else {
|
||||
item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
|
||||
}
|
||||
}
|
||||
} else if case .tap = gesture {
|
||||
self.item?.controllerInteraction.clickThroughMessage()
|
||||
@ -1189,6 +1250,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
return shareButtonNode.view
|
||||
}
|
||||
|
||||
if let reactionButtonsNode = self.reactionButtonsNode {
|
||||
if let result = reactionButtonsNode.hitTest(self.view.convert(point, to: reactionButtonsNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@ -1496,6 +1563,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
override func targetReactionView(value: String) -> UIView? {
|
||||
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||
return result
|
||||
}
|
||||
if !self.dateAndStatusNode.isHidden {
|
||||
return self.dateAndStatusNode.reactionView(value: value)
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,6 +292,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
isReplyThread = true
|
||||
}
|
||||
|
||||
let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput
|
||||
dateLayoutInput = .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message), preferAdditionalInset: false))
|
||||
|
||||
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
@ -299,7 +302,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)),
|
||||
layoutInput: dateLayoutInput,
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
|
@ -544,9 +544,6 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override func reactionTargetView(value: String) -> UIView? {
|
||||
if !self.contentNode.statusNode.isHidden {
|
||||
return self.contentNode.statusNode.reactionView(value: value)
|
||||
}
|
||||
return nil
|
||||
return self.contentNode.reactionTargetView(value: value)
|
||||
}
|
||||
}
|
||||
|
@ -941,7 +941,7 @@ private final class ItemView: UIView, SparseItemGridView {
|
||||
let messageItemNode: ListViewItemNode
|
||||
if let current = self.messageItemNode {
|
||||
messageItemNode = current
|
||||
messageItem.updateNode(async: { f in f() }, node: { return current }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring)), completion: { layout, apply in
|
||||
messageItem.updateNode(async: { f in f() }, node: { return current }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring, interactive: false)), completion: { layout, apply in
|
||||
current.contentSize = layout.contentSize
|
||||
current.insets = layout.insets
|
||||
|
||||
@ -972,7 +972,7 @@ private final class ItemView: UIView, SparseItemGridView {
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets) {
|
||||
if let messageItem = self.messageItem, let messageItemNode = self.messageItemNode {
|
||||
messageItem.updateNode(async: { f in f() }, node: { return messageItemNode }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring)), completion: { layout, apply in
|
||||
messageItem.updateNode(async: { f in f() }, node: { return messageItemNode }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring, interactive: false)), completion: { layout, apply in
|
||||
messageItemNode.contentSize = layout.contentSize
|
||||
messageItemNode.insets = layout.insets
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user