Reaction improvements

This commit is contained in:
Ali 2021-12-07 23:43:57 +04:00
parent 2397f3c5b1
commit c58b8c33c4
34 changed files with 1510 additions and 568 deletions

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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):

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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
})
})

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -37,6 +37,7 @@ enum ChatMessageBubbleMergeStatus {
case None(ChatMessageBubbleNoneMergeStatus)
case Left
case Right
case Both
}
enum ChatMessageBubbleRelativePosition {

View File

@ -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

View File

@ -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))

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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()
}

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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
}
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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