Reaction animation updates

This commit is contained in:
Ali 2021-12-03 22:19:53 +04:00
parent 41c7863cc9
commit fb4e94d09a
35 changed files with 1299 additions and 331 deletions

View File

@ -62,7 +62,6 @@ public extension ContainedViewLayoutTransitionCurve {
} }
} }
#if os(iOS)
var viewAnimationOptions: UIView.AnimationOptions { var viewAnimationOptions: UIView.AnimationOptions {
switch self { switch self {
case .linear: case .linear:
@ -77,7 +76,6 @@ public extension ContainedViewLayoutTransitionCurve {
return [] return []
} }
} }
#endif
} }
public enum ContainedViewLayoutTransition { public enum ContainedViewLayoutTransition {
@ -1417,3 +1415,402 @@ public extension ContainedViewLayoutTransition {
} }
} }
} }
public protocol ControlledTransitionAnimator: AnyObject {
var duration: Double { get }
func startAnimation()
func setAnimationProgress(_ progress: CGFloat)
func finishAnimation()
func updateAlpha(layer: CALayer, alpha: 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)?)
}
protocol AnyValueProviding {
var anyValue: ControlledTransitionProperty.AnyValue { get }
}
extension CGFloat: AnyValueProviding {
func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat {
let invT = 1.0 - fraction
let result = other * fraction + self * invT
return result
}
var anyValue: ControlledTransitionProperty.AnyValue {
return ControlledTransitionProperty.AnyValue(
value: self,
stringValue: { "\(self)" },
isEqual: { other in
if let otherValue = other.value as? CGFloat {
return self == otherValue
} else {
return false
}
},
interpolate: { other, fraction in
guard let otherValue = other.value as? CGFloat else {
preconditionFailure()
}
return self.interpolate(with: otherValue, fraction: fraction).anyValue
}
)
}
}
extension Float: AnyValueProviding {
func interpolate(with other: Float, fraction: CGFloat) -> Float {
let invT = 1.0 - Float(fraction)
let result = other * Float(fraction) + self * invT
return result
}
var anyValue: ControlledTransitionProperty.AnyValue {
return ControlledTransitionProperty.AnyValue(
value: self,
stringValue: { "\(self)" },
isEqual: { other in
if let otherValue = other.value as? Float {
return self == otherValue
} else {
return false
}
},
interpolate: { other, fraction in
guard let otherValue = other.value as? Float else {
preconditionFailure()
}
return self.interpolate(with: otherValue, fraction: fraction).anyValue
}
)
}
}
extension CGPoint: AnyValueProviding {
func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint {
return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction))
}
var anyValue: ControlledTransitionProperty.AnyValue {
return ControlledTransitionProperty.AnyValue(
value: self,
stringValue: { "\(self)" },
isEqual: { other in
if let otherValue = other.value as? CGPoint {
return self == otherValue
} else {
return false
}
},
interpolate: { other, fraction in
guard let otherValue = other.value as? CGPoint else {
preconditionFailure()
}
return self.interpolate(with: otherValue, fraction: fraction).anyValue
}
)
}
}
extension CGSize: AnyValueProviding {
func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize {
return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction))
}
var anyValue: ControlledTransitionProperty.AnyValue {
return ControlledTransitionProperty.AnyValue(
value: self,
stringValue: { "\(self)" },
isEqual: { other in
if let otherValue = other.value as? CGSize {
return self == otherValue
} else {
return false
}
},
interpolate: { other, fraction in
guard let otherValue = other.value as? CGSize else {
preconditionFailure()
}
return self.interpolate(with: otherValue, fraction: fraction).anyValue
}
)
}
}
extension CGRect: AnyValueProviding {
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction))
}
var anyValue: ControlledTransitionProperty.AnyValue {
return ControlledTransitionProperty.AnyValue(
value: self,
stringValue: { "\(self)" },
isEqual: { other in
if let otherValue = other.value as? CGRect {
return self == otherValue
} else {
return false
}
},
interpolate: { other, fraction in
guard let otherValue = other.value as? CGRect else {
preconditionFailure()
}
return self.interpolate(with: otherValue, fraction: fraction).anyValue
}
)
}
}
final class ControlledTransitionProperty {
final class AnyValue: Equatable, CustomStringConvertible {
let value: Any
let stringValue: () -> String
let isEqual: (AnyValue) -> Bool
let interpolate: (AnyValue, CGFloat) -> AnyValue
init(
value: Any,
stringValue: @escaping () -> String,
isEqual: @escaping (AnyValue) -> Bool,
interpolate: @escaping (AnyValue, CGFloat) -> AnyValue
) {
self.value = value
self.stringValue = stringValue
self.isEqual = isEqual
self.interpolate = interpolate
}
var description: String {
return self.stringValue()
}
static func ==(lhs: AnyValue, rhs: AnyValue) -> Bool {
if lhs.isEqual(rhs) {
return true
} else {
return false
}
}
}
let layer: CALayer
let keyPath: AnyKeyPath
private let write: (CALayer, AnyValue) -> Void
var fromValue: AnyValue
let toValue: AnyValue
private(set) var lastValue: AnyValue
private let completion: ((Bool) -> Void)?
init<T: Equatable>(layer: CALayer, keyPath: ReferenceWritableKeyPath<CALayer, T>, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding {
self.layer = layer
self.keyPath = keyPath
self.write = { layer, value in
layer[keyPath: keyPath] = value.value as! T
}
self.fromValue = fromValue.anyValue
self.toValue = toValue.anyValue
self.lastValue = self.fromValue
self.completion = completion
}
func update(at fraction: CGFloat) {
let value = self.fromValue.interpolate(toValue, fraction)
self.lastValue = value
self.write(self.layer, value)
}
func complete(atEnd: Bool) {
self.completion?(atEnd)
}
}
public final class ControlledTransition {
@available(iOS 10.0, *)
public final class NativeAnimator: ControlledTransitionAnimator {
public let duration: Double
private let curve: ContainedViewLayoutTransitionCurve
private var animations: [ControlledTransitionProperty] = []
init(
duration: Double,
curve: ContainedViewLayoutTransitionCurve
) {
self.duration = duration
self.curve = curve
}
func merge(with other: NativeAnimator) {
var removeAnimationIndices: [Int] = []
for i in 0 ..< self.animations.count {
let animation = self.animations[i]
var removeOtherAnimationIndices: [Int] = []
for j in 0 ..< other.animations.count {
let otherAnimation = other.animations[j]
if animation.layer === otherAnimation.layer && animation.keyPath == otherAnimation.keyPath {
if animation.toValue == otherAnimation.toValue {
removeAnimationIndices.append(i)
} else {
removeOtherAnimationIndices.append(j)
}
}
}
for j in removeOtherAnimationIndices.reversed() {
other.animations.remove(at: j).complete(atEnd: false)
}
}
for i in removeAnimationIndices.reversed() {
self.animations.remove(at: i).complete(atEnd: false)
}
}
public func startAnimation() {
}
public func setAnimationProgress(_ progress: CGFloat) {
let mappedFraction: CGFloat
switch self.curve {
case .spring:
mappedFraction = springAnimationSolver(progress)
case let .custom(c1x, c1y, c2x, c2y):
mappedFraction = bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), progress)
default:
mappedFraction = progress
}
for animation in self.animations {
animation.update(at: mappedFraction)
}
}
public func finishAnimation() {
for animation in self.animations {
animation.update(at: 1.0)
animation.complete(atEnd: true)
}
self.animations.removeAll()
}
public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) {
self.animations.append(ControlledTransitionProperty(
layer: layer,
keyPath: \.opacity,
fromValue: layer.opacity,
toValue: Float(alpha),
completion: completion
))
}
public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) {
self.animations.append(ControlledTransitionProperty(
layer: layer,
keyPath: \.position,
fromValue: layer.position,
toValue: position,
completion: completion
))
}
public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) {
self.animations.append(ControlledTransitionProperty(
layer: layer,
keyPath: \.bounds,
fromValue: layer.bounds,
toValue: bounds,
completion: completion
))
}
public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) {
self.animations.append(ControlledTransitionProperty(
layer: layer,
keyPath: \.frame,
fromValue: layer.frame,
toValue: frame,
completion: completion
))
}
}
public final class LegacyAnimator: ControlledTransitionAnimator {
public let duration: Double
public let transition: ContainedViewLayoutTransition
init(
duration: Double,
curve: ContainedViewLayoutTransitionCurve
) {
self.duration = duration
if duration.isZero {
self.transition = .immediate
} else {
self.transition = .animated(duration: duration, curve: curve)
}
}
public func startAnimation() {
}
public func setAnimationProgress(_ progress: CGFloat) {
}
public func finishAnimation() {
}
public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) {
self.transition.updateAlpha(layer: layer, alpha: alpha, completion: completion)
}
public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) {
self.transition.updatePosition(layer: layer, position: position, completion: completion)
}
public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) {
self.transition.updateBounds(layer: layer, bounds: bounds, completion: completion)
}
public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) {
self.transition.updateFrame(layer: layer, frame: frame, completion: completion)
}
}
public let animator: ControlledTransitionAnimator
public let legacyAnimator: LegacyAnimator
public init(
duration: Double,
curve: ContainedViewLayoutTransitionCurve
) {
self.legacyAnimator = LegacyAnimator(
duration: duration,
curve: curve
)
if #available(iOS 10.0, *) {
self.animator = NativeAnimator(
duration: duration,
curve: curve
)
} else {
self.animator = self.legacyAnimator
}
}
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)
}
}
}
}

View File

@ -1578,8 +1578,24 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
DispatchQueue.main.async(execute: f) DispatchQueue.main.async(execute: f)
} }
private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject<ListViewItemNode>?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimation: ListViewItemUpdateAnimation, completion: @escaping (QueueLocalObject<ListViewItemNode>, ListViewItemNodeLayout, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject<ListViewItemNode>?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimationIsAnimated: Bool, updateAnimationIsCrossfade: Bool, completion: @escaping (QueueLocalObject<ListViewItemNode>, ListViewItemNodeLayout, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
if let previousNode = previousNode { if let previousNode = previousNode {
var controlledTransition: ControlledTransition?
let updateAnimation: ListViewItemUpdateAnimation
if updateAnimationIsCrossfade {
updateAnimation = .Crossfade
} else if updateAnimationIsAnimated {
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring)
controlledTransition = transition
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
} else {
updateAnimation = .None
}
if let controlledTransition = controlledTransition {
previousNode.syncWith({ $0 }).addPendingControlledTransition(transition: controlledTransition)
}
item.updateNode(async: { f in item.updateNode(async: { f in
if synchronous { if synchronous {
f() f()
@ -2017,8 +2033,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if updateAdjacentItemsIndices.isEmpty { if updateAdjacentItemsIndices.isEmpty {
completion(state, operations) completion(state, operations)
} else { } else {
let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None
var updatedUpdateAdjacentItemsIndices = updateAdjacentItemsIndices var updatedUpdateAdjacentItemsIndices = updateAdjacentItemsIndices
let nodeIndex = updateAdjacentItemsIndices.first! let nodeIndex = updateAdjacentItemsIndices.first!
@ -2031,6 +2045,20 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if case let .Node(index, _, referenceNode) = node , index == nodeIndex { if case let .Node(index, _, referenceNode) = node , index == nodeIndex {
if let referenceNode = referenceNode { if let referenceNode = referenceNode {
continueWithoutNode = false continueWithoutNode = false
var controlledTransition: ControlledTransition?
let updateAnimation: ListViewItemUpdateAnimation
if animated {
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring)
controlledTransition = transition
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
} else {
updateAnimation = .None
}
if let controlledTransition = controlledTransition {
referenceNode.syncWith({ $0 }).addPendingControlledTransition(transition: controlledTransition)
}
self.items[index].updateNode(async: { f in self.items[index].updateNode(async: { f in
if synchronous { if synchronous {
f() f()
@ -2086,7 +2114,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let previousNodes = inputPreviousNodes let previousNodes = inputPreviousNodes
var operations = inputOperations var operations = inputOperations
let completion = inputCompletion let completion = inputCompletion
let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None
if state.nodes.count > 1000 { if state.nodes.count > 1000 {
print("state.nodes.count > 1000") print("state.nodes.count > 1000")
@ -2115,8 +2142,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let index = insertionItemIndexAndDirection.0 let index = insertionItemIndexAndDirection.0
let threadId = pthread_self() let threadId = pthread_self()
var tailRecurse = false var tailRecurse = false
self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: self.items[index], previousNode: previousNodes[index], index: index, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: self.items.count == index + 1 ? nil : self.items[index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right, availableHeight: state.visibleSize.height - state.insets.top - state.insets.bottom), updateAnimation: updateAnimation, completion: { (node, layout, apply) in self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: self.items[index], previousNode: previousNodes[index], index: index, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: self.items.count == index + 1 ? nil : self.items[index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right, availableHeight: state.visibleSize.height - state.insets.top - state.insets.bottom), updateAnimationIsAnimated: animated, updateAnimationIsCrossfade: false, completion: { (node, layout, apply) in
if pthread_equal(pthread_self(), threadId) != 0 && !tailRecurse { if pthread_equal(pthread_self(), threadId) != 0 && !tailRecurse {
tailRecurse = true tailRecurse = true
state.insertNode(index, node: node, layout: layout, apply: apply, offsetDirection: insertionItemIndexAndDirection.1, animated: animated && animatedInsertIndices.contains(index), operations: &operations, itemCount: self.items.count) state.insertNode(index, node: node, layout: layout, apply: apply, offsetDirection: insertionItemIndexAndDirection.1, animated: animated && animatedInsertIndices.contains(index), operations: &operations, itemCount: self.items.count)
@ -2151,16 +2177,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} else { } else {
let updateItem = updateIndicesAndItems[0] let updateItem = updateIndicesAndItems[0]
if let previousNode = previousNodes[updateItem.index] { if let previousNode = previousNodes[updateItem.index] {
let updateAnimation: ListViewItemUpdateAnimation self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right, availableHeight: state.visibleSize.height - state.insets.top - state.insets.bottom), updateAnimationIsAnimated: animated, updateAnimationIsCrossfade: crossfade, completion: { _, layout, apply in
if crossfade { state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, isAnimated: animated, apply: apply, operations: &operations)
updateAnimation = .Crossfade
} else if animated {
updateAnimation = .System(duration: insertionAnimationDuration)
} else {
updateAnimation = .None
}
self.nodeForItem(synchronous: synchronous, synchronousLoads: synchronousLoads, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], params: ListViewItemLayoutParams(width: state.visibleSize.width, leftInset: state.insets.left, rightInset: state.insets.right, availableHeight: state.visibleSize.height - state.insets.top - state.insets.bottom), updateAnimation: updateAnimation, completion: { _, layout, apply in
state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, animation: animated ? .System(duration: insertionAnimationDuration) : .None, apply: apply, operations: &operations)
updateIndicesAndItems.remove(at: 0) updateIndicesAndItems.remove(at: 0)
self.updateNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, crossfade: crossfade, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) self.updateNodes(synchronous: synchronous, synchronousLoads: synchronousLoads, crossfade: crossfade, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion)
@ -2656,10 +2674,16 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
}) })
if node.rotated && currentAnimation == nil { if node.rotated {
if currentAnimation == nil {
let insetPart: CGFloat = previousInsets.bottom - layout.insets.bottom let insetPart: CGFloat = previousInsets.bottom - layout.insets.bottom
node.transitionOffset += previousApparentHeight - layout.size.height - insetPart node.transitionOffset += previousApparentHeight - layout.size.height - insetPart
node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
} else {
let insetPart: CGFloat = previousInsets.bottom - layout.insets.bottom
node.transitionOffset = previousApparentHeight - layout.size.height - insetPart
node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)
}
} }
} }
} else { } else {
@ -2708,6 +2732,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
} }
for itemNode in self.itemNodes {
itemNode.beginPendingControlledTransitions(beginAt: timestamp)
}
if hadInserts, let reorderNode = self.reorderNode, reorderNode.supernode != nil { if hadInserts, let reorderNode = self.reorderNode, reorderNode.supernode != nil {
self.view.bringSubviewToFront(reorderNode.view) self.view.bringSubviewToFront(reorderNode.view)
if let verticalScrollIndicator = self.verticalScrollIndicator { if let verticalScrollIndicator = self.verticalScrollIndicator {

View File

@ -7,15 +7,15 @@ public protocol Interpolatable {
} }
private func floorToPixels(_ value: CGFloat) -> CGFloat { private func floorToPixels(_ value: CGFloat) -> CGFloat {
return round(value * 10.0) / 10.0 return value
} }
private func floorToPixels(_ value: CGPoint) -> CGPoint { private func floorToPixels(_ value: CGPoint) -> CGPoint {
return CGPoint(x: round(value.x * 10.0) / 10.0, y: round(value.y * 10.0) / 10.0) return CGPoint(x: floorToPixels(value.x), y: floorToPixels(value.y))
} }
private func floorToPixels(_ value: CGSize) -> CGSize { private func floorToPixels(_ value: CGSize) -> CGSize {
return CGSize(width: round(value.width * 10.0) / 10.0, height: round(value.height * 10.0) / 10.0) return CGSize(width: floorToPixels(value.width), height: floorToPixels(value.height))
} }
private func floorToPixels(_ value: CGRect) -> CGRect { private func floorToPixels(_ value: CGRect) -> CGRect {
@ -23,7 +23,7 @@ private func floorToPixels(_ value: CGRect) -> CGRect {
} }
private func floorToPixels(_ value: UIEdgeInsets) -> UIEdgeInsets { private func floorToPixels(_ value: UIEdgeInsets) -> UIEdgeInsets {
return UIEdgeInsets(top: round(value.top * 10.0) / 10.0, left: round(value.left * 10.0) / 10.0, bottom: round(value.bottom * 10.0) / 10.0, right: round(value.right * 10.0) / 10.0) return UIEdgeInsets(top: floorToPixels(value.top), left: floorToPixels(value.left), bottom: floorToPixels(value.bottom), right: floorToPixels(value.right))
} }
extension CGFloat: Interpolatable { extension CGFloat: Interpolatable {
@ -36,6 +36,12 @@ extension CGFloat: Interpolatable {
return floorToPixels(term) return floorToPixels(term)
} }
} }
static func interpolate(from fromValue: CGFloat, to toValue: CGFloat, at t: CGFloat) -> CGFloat {
let invT: CGFloat = 1.0 - t
let term: CGFloat = toValue * t + fromValue * invT
return term
}
} }
extension UIEdgeInsets: Interpolatable { extension UIEdgeInsets: Interpolatable {
@ -56,6 +62,10 @@ extension CGRect: Interpolatable {
return floorToPixels(CGRect(x: toValue.origin.x * t + fromValue.origin.x * (1.0 - t), y: toValue.origin.y * t + fromValue.origin.y * (1.0 - t), width: toValue.size.width * t + fromValue.size.width * (1.0 - t), height: toValue.size.height * t + fromValue.size.height * (1.0 - t))) return floorToPixels(CGRect(x: toValue.origin.x * t + fromValue.origin.x * (1.0 - t), y: toValue.origin.y * t + fromValue.origin.y * (1.0 - t), width: toValue.size.width * t + fromValue.size.width * (1.0 - t), height: toValue.size.height * t + fromValue.size.height * (1.0 - t)))
} }
} }
static func interpolate(from fromValue: CGRect, to toValue: CGRect, at t: CGFloat) -> CGRect {
return CGRect(origin: CGPoint.interpolate(from: fromValue.origin, to: toValue.origin, at: t), size: CGSize.interpolate(from: fromValue.size, to: toValue.size, at: t))
}
} }
extension CGPoint: Interpolatable { extension CGPoint: Interpolatable {
@ -66,6 +76,16 @@ extension CGPoint: Interpolatable {
return floorToPixels(CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t))) return floorToPixels(CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t)))
} }
} }
static func interpolate(from fromValue: CGPoint, to toValue: CGPoint, at t: CGFloat) -> CGPoint {
return CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t))
}
}
extension CGSize {
static func interpolate(from fromValue: CGSize, to toValue: CGSize, at t: CGFloat) -> CGSize {
return CGSize(width: toValue.width * t + fromValue.width * (1.0 - t), height: toValue.height * t + fromValue.height * (1.0 - t))
}
} }
private let springAnimationIn: CABasicAnimation = { private let springAnimationIn: CABasicAnimation = {
@ -73,7 +93,7 @@ private let springAnimationIn: CABasicAnimation = {
return animation return animation
}() }()
private let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in
if #available(iOS 9.0, *) { if #available(iOS 9.0, *) {
return { t in return { t in
return springAnimationValueAt(springAnimationIn, t) return springAnimationValueAt(springAnimationIn, t)

View File

@ -807,13 +807,12 @@ struct ListViewState {
} }
} }
mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, animation: ListViewItemUpdateAnimation, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) { mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, isAnimated: Bool, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) {
var i = -1 var i = -1
for node in self.nodes { for node in self.nodes {
i += 1 i += 1
if node.index == itemIndex { if node.index == itemIndex {
switch animation { if isAnimated {
case .None, .Crossfade:
let offsetDirection: ListViewInsertionOffsetDirection let offsetDirection: ListViewInsertionOffsetDirection
if let direction = direction { if let direction = direction {
offsetDirection = ListViewInsertionOffsetDirection(direction) offsetDirection = ListViewInsertionOffsetDirection(direction)
@ -852,7 +851,7 @@ struct ListViewState {
} }
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
case .System: } else {
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
} }

View File

@ -4,7 +4,7 @@ import SwiftSignalKit
public enum ListViewItemUpdateAnimation { public enum ListViewItemUpdateAnimation {
case None case None
case System(duration: Double) case System(duration: Double, transition: ControlledTransition)
case Crossfade case Crossfade
public var isAnimated: Bool { public var isAnimated: Bool {
@ -14,6 +14,26 @@ public enum ListViewItemUpdateAnimation {
return true return true
} }
} }
public var animator: ControlledTransitionAnimator {
switch self {
case .None:
return ControlledTransition.LegacyAnimator(duration: 0.0, curve: .linear)
case let .System(_, transition):
return transition.animator
case .Crossfade:
return ControlledTransition.LegacyAnimator(duration: 0.0, curve: .linear)
}
}
public var transition: ContainedViewLayoutTransition {
switch self {
case .None, .Crossfade:
return .immediate
case let .System(_, transition):
return transition.legacyAnimator.transition
}
}
} }
public struct ListViewItemConfigureNodeFlags: OptionSet { public struct ListViewItemConfigureNodeFlags: OptionSet {

View File

@ -83,6 +83,16 @@ public struct ListViewItemLayoutParams {
} }
} }
private final class ControlledTransitionContext {
let transition: ControlledTransition
let beginAt: Double
init(transition: ControlledTransition, beginAt: Double) {
self.transition = transition
self.beginAt = beginAt
}
}
open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
public struct HeaderId: Hashable { public struct HeaderId: Hashable {
public var space: AnyHashable public var space: AnyHashable
@ -126,6 +136,8 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
private final var spring: ListViewItemSpring? private final var spring: ListViewItemSpring?
private final var animations: [(String, ListViewAnimation)] = [] private final var animations: [(String, ListViewAnimation)] = []
private final var pendingControlledTransitions: [ControlledTransition] = []
private final var controlledTransitions: [ControlledTransitionContext] = []
final var tempHeaderSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:] final var tempHeaderSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:]
final var headerSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:] final var headerSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:]
@ -394,6 +406,26 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
i += 1 i += 1
} }
i = 0
var transitionCount = self.controlledTransitions.count
while i < transitionCount {
let transition = self.controlledTransitions[i]
var fraction = (timestamp - transition.beginAt) / transition.transition.animator.duration
fraction = max(0.0, min(1.0, fraction))
transition.transition.animator.setAnimationProgress(CGFloat(fraction))
if timestamp >= transition.beginAt + transition.transition.animator.duration {
transition.transition.animator.finishAnimation()
self.controlledTransitions.remove(at: i)
transitionCount -= 1
i -= 1
} else {
continueAnimations = true
}
i += 1
}
if let accessoryItemNode = self.accessoryItemNode { if let accessoryItemNode = self.accessoryItemNode {
if (accessoryItemNode.animate(timestamp)) { if (accessoryItemNode.animate(timestamp)) {
continueAnimations = true continueAnimations = true
@ -438,6 +470,29 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
} }
self.accessoryItemNode?.removeAllAnimations() self.accessoryItemNode?.removeAllAnimations()
for transition in self.controlledTransitions {
transition.transition.animator.finishAnimation()
}
self.controlledTransitions.removeAll()
}
func addPendingControlledTransition(transition: ControlledTransition) {
self.pendingControlledTransitions.append(transition)
}
func beginPendingControlledTransitions(beginAt: Double) {
for transition in self.pendingControlledTransitions {
self.addControlledTransition(transition: transition, beginAt: beginAt)
}
self.pendingControlledTransitions.removeAll()
}
func addControlledTransition(transition: ControlledTransition, beginAt: Double) {
for controlledTransition in self.controlledTransitions {
transition.merge(with: controlledTransition.transition)
}
self.controlledTransitions.append(ControlledTransitionContext(transition: transition, beginAt: beginAt))
} }
public func addInsetsAnimationToValue(_ value: UIEdgeInsets, duration: Double, beginAt: Double) { public func addInsetsAnimationToValue(_ value: UIEdgeInsets, duration: Double, beginAt: Double) {

View File

@ -503,7 +503,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false)
targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8)
targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.hapticFeedback.tap() strongSelf.hapticFeedback.tap()
} }
@ -514,18 +514,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if hideNode { if hideNode {
targetView.isHidden = false targetView.isHidden = false
targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/
targetSnapshotView?.isHidden = true targetSnapshotView?.isHidden = true
targetScaleCompleted = true targetScaleCompleted = true
intermediateCompletion() intermediateCompletion()
}) //})
} else { } else {
targetScaleCompleted = true targetScaleCompleted = true
intermediateCompletion() intermediateCompletion()
} }
}) })
itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / itemNode.bounds.width, duration: duration, removeOnCompletion: false) itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 1.0) / itemNode.bounds.width, duration: duration, removeOnCompletion: false)
} }
public func willAnimateOutToReaction(value: String) { public func willAnimateOutToReaction(value: String) {
@ -668,7 +668,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
} }
public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
guard let targetSnapshotView = targetView.snapshotContentTree() else { guard let sourceSnapshotView = targetView.snapshotContentTree(), let targetSnapshotView = targetView.snapshotContentTree() else {
completion() completion()
return return
} }
@ -685,12 +685,20 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize)
sourceSnapshotView.frame = selfTargetRect
self.view.addSubview(sourceSnapshotView)
sourceSnapshotView.alpha = 0.0
sourceSnapshotView.layer.animateSpring(from: 1.0 as NSNumber, to: (expandedFrame.width / selfTargetRect.width) as NSNumber, keyPath: "transform.scale", duration: 0.4)
sourceSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, completion: { [weak sourceSnapshotView] _ in
sourceSnapshotView?.removeFromSuperview()
})
self.addSubnode(itemNode) self.addSubnode(itemNode)
itemNode.frame = expandedFrame itemNode.frame = expandedFrame
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: .immediate) itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: .immediate)
itemNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.18) itemNode.layer.animateSpring(from: (selfTargetRect.width / expandedFrame.width) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04)
let additionalAnimationNode = AnimatedStickerNode() let additionalAnimationNode = AnimatedStickerNode()
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
@ -724,7 +732,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
additionalAnimationNode.visibility = true additionalAnimationNode.visibility = true
}) })
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: { 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, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: {
mainAnimationCompleted = true mainAnimationCompleted = true
intermediateCompletion() intermediateCompletion()
@ -755,7 +763,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false)
targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8)
targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.hapticFeedback.tap() strongSelf.hapticFeedback.tap()
} }
@ -766,18 +774,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
if hideNode { if hideNode {
targetView.isHidden = false targetView.isHidden = false
targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/
targetSnapshotView?.isHidden = true targetSnapshotView?.isHidden = true
targetScaleCompleted = true targetScaleCompleted = true
intermediateCompletion() intermediateCompletion()
}) //})
} else { } else {
targetScaleCompleted = true targetScaleCompleted = true
intermediateCompletion() intermediateCompletion()
} }
}) })
itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / itemNode.bounds.width, duration: duration, removeOnCompletion: false) itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 1.0) / itemNode.bounds.width, duration: duration, removeOnCompletion: false)
} }
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {

View File

@ -60,8 +60,6 @@ final class ReactionNode: ASDisplayNode {
super.init() super.init()
//self.backgroundColor = UIColor(white: 0.0, alpha: 0.1)
self.addSubnode(self.staticImageNode) self.addSubnode(self.staticImageNode)
self.addSubnode(self.stillAnimationNode) self.addSubnode(self.stillAnimationNode)
@ -120,11 +118,14 @@ final class ReactionNode: ASDisplayNode {
animationNode.visibility = true animationNode.visibility = true
self.stillAnimationNode.alpha = 0.0 self.stillAnimationNode.alpha = 0.0
if transition.isAnimated {
self.stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in self.stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.stillAnimationNode.visibility = false self?.stillAnimationNode.visibility = false
}) })
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} else {
self.stillAnimationNode.visibility = false
}
} }
if self.validSize != size { if self.validSize != size {
@ -137,6 +138,7 @@ final class ReactionNode: ASDisplayNode {
} }
if !self.didSetupStillAnimation { if !self.didSetupStillAnimation {
if self.animationNode == nil {
self.didSetupStillAnimation = true self.didSetupStillAnimation = true
self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
@ -144,6 +146,7 @@ final class ReactionNode: ASDisplayNode {
self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
self.stillAnimationNode.updateLayout(size: animationFrame.size) self.stillAnimationNode.updateLayout(size: animationFrame.size)
self.stillAnimationNode.visibility = true self.stillAnimationNode.visibility = true
}
} else { } else {
transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center) transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center)
transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width) transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width)

View File

@ -65,7 +65,7 @@ enum AccountStateMutationOperation {
case DeleteMessages([MessageId]) case DeleteMessages([MessageId])
case EditMessage(MessageId, StoreMessage) case EditMessage(MessageId, StoreMessage)
case UpdateMessagePoll(MediaId, Api.Poll?, Api.PollResults) case UpdateMessagePoll(MediaId, Api.Poll?, Api.PollResults)
//case UpdateMessageReactions(MessageId, Api.MessageReactions) case UpdateMessageReactions(MessageId, Api.MessageReactions)
case UpdateMedia(MediaId, Media?) case UpdateMedia(MediaId, Media?)
case ReadInbox(MessageId) case ReadInbox(MessageId)
case ReadOutbox(MessageId, Int32?) case ReadOutbox(MessageId, Int32?)
@ -258,9 +258,9 @@ struct AccountMutableState {
self.addOperation(.UpdateMessagePoll(id, poll, results)) self.addOperation(.UpdateMessagePoll(id, poll, results))
} }
/*mutating func updateMessageReactions(_ messageId: MessageId, reactions: Api.MessageReactions) { mutating func updateMessageReactions(_ messageId: MessageId, reactions: Api.MessageReactions) {
self.addOperation(.UpdateMessageReactions(messageId, reactions)) self.addOperation(.UpdateMessageReactions(messageId, reactions))
}*/ }
mutating func updateMedia(_ id: MediaId, media: Media?) { mutating func updateMedia(_ id: MediaId, media: Media?) {
self.addOperation(.UpdateMedia(id, media)) self.addOperation(.UpdateMedia(id, media))
@ -498,7 +498,7 @@ struct AccountMutableState {
mutating func addOperation(_ operation: AccountStateMutationOperation) { mutating func addOperation(_ operation: AccountStateMutationOperation) {
switch operation { switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout: case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout:
break break
case let .AddMessages(messages, location): case let .AddMessages(messages, location):
for message in messages { for message in messages {

View File

@ -5,7 +5,7 @@ import TelegramApi
extension ReactionsMessageAttribute { extension ReactionsMessageAttribute {
func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute { func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute {
switch reactions { switch reactions {
case let .messageReactions(flags, results, _): case let .messageReactions(flags, results, recentReactions):
let min = (flags & (1 << 0)) != 0 let min = (flags & (1 << 0)) != 0
var reactions = results.map { result -> MessageReaction in var reactions = results.map { result -> MessageReaction in
switch result { switch result {
@ -13,6 +13,18 @@ extension ReactionsMessageAttribute {
return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0) return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0)
} }
} }
let parsedRecentReactions: [ReactionsMessageAttribute.RecentPeer]
if let recentReactions = recentReactions {
parsedRecentReactions = recentReactions.map { recentReaction -> ReactionsMessageAttribute.RecentPeer in
switch recentReaction {
case let .messageUserReaction(userId, reaction):
return ReactionsMessageAttribute.RecentPeer(value: reaction, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)))
}
}
} else {
parsedRecentReactions = []
}
if min { if min {
var currentSelectedReaction: String? var currentSelectedReaction: String?
for reaction in self.reactions { for reaction in self.reactions {
@ -29,7 +41,7 @@ extension ReactionsMessageAttribute {
} }
} }
} }
return ReactionsMessageAttribute(reactions: reactions) return ReactionsMessageAttribute(reactions: reactions, recentPeers: parsedRecentReactions)
} }
} }
} }
@ -47,6 +59,7 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM
if let pending = pending { if let pending = pending {
var reactions = current?.reactions ?? [] var reactions = current?.reactions ?? []
let recentPeers = current?.recentPeers ?? []
if let value = pending.value { if let value = pending.value {
var found = false var found = false
for i in 0 ..< reactions.count { for i in 0 ..< reactions.count {
@ -73,7 +86,7 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM
} }
} }
if !reactions.isEmpty { if !reactions.isEmpty {
return ReactionsMessageAttribute(reactions: reactions) return ReactionsMessageAttribute(reactions: reactions, recentPeers: recentPeers)
} else { } else {
return nil return nil
} }
@ -87,13 +100,28 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM
extension ReactionsMessageAttribute { extension ReactionsMessageAttribute {
convenience init(apiReactions: Api.MessageReactions) { convenience init(apiReactions: Api.MessageReactions) {
switch apiReactions { switch apiReactions {
case let .messageReactions(_, results, _): case let .messageReactions(_, results, recentReactions):
self.init(reactions: results.map { result in let parsedRecentReactions: [ReactionsMessageAttribute.RecentPeer]
if let recentReactions = recentReactions {
parsedRecentReactions = recentReactions.map { recentReaction -> ReactionsMessageAttribute.RecentPeer in
switch recentReaction {
case let .messageUserReaction(userId, reaction):
return ReactionsMessageAttribute.RecentPeer(value: reaction, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)))
}
}
} else {
parsedRecentReactions = []
}
self.init(
reactions: results.map { result in
switch result { switch result {
case let .reactionCount(flags, reaction, count): case let .reactionCount(flags, reaction, count):
return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0) return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0)
} }
}) },
recentPeers: parsedRecentReactions
)
} }
} }
} }

View File

@ -1473,6 +1473,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
return current return current
} }
}) })
case let .updateMessageReactions(peer, msgId, reactions):
updatedState.updateMessageReactions(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), reactions: reactions)
default: default:
break break
} }
@ -2260,7 +2262,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
var currentAddScheduledMessages: OptimizeAddMessagesState? var currentAddScheduledMessages: OptimizeAddMessagesState?
for operation in operations { for operation in operations {
switch operation { switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll/*, .UpdateMessageReactions*/, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout: case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout:
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
} }
@ -3222,6 +3224,31 @@ func replayFinalState(
return state return state
}) })
} }
case let .UpdateMessageReactions(messageId, reactions):
transaction.updateMessage(messageId, update: { currentMessage in
var updatedReactions = ReactionsMessageAttribute(apiReactions: reactions)
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var attributes = currentMessage.attributes
var added = false
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute {
added = true
updatedReactions = attribute.withUpdatedResults(reactions)
if updatedReactions.reactions == attribute.reactions {
return .skip
}
attributes[j] = updatedReactions
break loop
}
}
if !added {
attributes.append(updatedReactions)
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
} }
} }

View File

@ -856,13 +856,16 @@ public final class AccountViewTracker {
switch update { switch update {
case let .updateMessageReactions(peer, msgId, reactions): case let .updateMessageReactions(peer, msgId, reactions):
transaction.updateMessage(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), update: { currentMessage in transaction.updateMessage(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), update: { currentMessage in
var updatedReactions = ReactionsMessageAttribute(apiReactions: reactions)
let updatedReactions = ReactionsMessageAttribute(apiReactions: reactions)
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
var added = false
var attributes = currentMessage.attributes var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count { loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute { if let attribute = attributes[j] as? ReactionsMessageAttribute {
added = true
updatedReactions = attribute.withUpdatedResults(reactions)
if updatedReactions.reactions == attribute.reactions { if updatedReactions.reactions == attribute.reactions {
return .skip return .skip
} }
@ -870,6 +873,9 @@ public final class AccountViewTracker {
break loop break loop
} }
} }
if !added {
attributes.append(updatedReactions)
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
}) })
default: default:

View File

@ -6,6 +6,12 @@ public struct MessageReaction: Equatable, PostboxCoding {
public var isSelected: Bool public var isSelected: Bool
public init(value: String, count: Int32, isSelected: Bool) { public init(value: String, count: Int32, isSelected: Bool) {
var value = value
if value == "❤️" {
value = ""
}
self.value = value self.value = value
self.count = count self.count = count
self.isSelected = isSelected self.isSelected = isSelected
@ -24,19 +30,53 @@ public struct MessageReaction: Equatable, PostboxCoding {
} }
} }
public final class ReactionsMessageAttribute: MessageAttribute { public final class ReactionsMessageAttribute: Equatable, MessageAttribute {
public let reactions: [MessageReaction] public struct RecentPeer: Equatable, PostboxCoding {
public var value: String
public var peerId: PeerId
public init(reactions: [MessageReaction]) { public init(value: String, peerId: PeerId) {
self.value = value
self.peerId = peerId
}
public init(decoder: PostboxDecoder) {
self.value = decoder.decodeStringForKey("v", orElse: "")
self.peerId = PeerId(decoder.decodeInt64ForKey("p", orElse: 0))
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.value, forKey: "v")
encoder.encodeInt64(self.peerId.toInt64(), forKey: "p")
}
}
public let reactions: [MessageReaction]
public let recentPeers: [RecentPeer]
public init(reactions: [MessageReaction], recentPeers: [RecentPeer]) {
self.reactions = reactions self.reactions = reactions
self.recentPeers = recentPeers
} }
required public init(decoder: PostboxDecoder) { required public init(decoder: PostboxDecoder) {
self.reactions = decoder.decodeObjectArrayWithDecoderForKey("r") self.reactions = decoder.decodeObjectArrayWithDecoderForKey("r")
self.recentPeers = decoder.decodeObjectArrayWithDecoderForKey("rp")
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.reactions, forKey: "r") encoder.encodeObjectArray(self.reactions, forKey: "r")
encoder.encodeObjectArray(self.recentPeers, forKey: "rp")
}
public static func ==(lhs: ReactionsMessageAttribute, rhs: ReactionsMessageAttribute) -> Bool {
if lhs.reactions != rhs.reactions {
return false
}
if lhs.recentPeers != rhs.recentPeers {
return false
}
return true
} }
} }

View File

@ -945,6 +945,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
guard let topMessage = messages.first else {
return
}
let _ = combineLatest(queue: .mainQueue(), let _ = combineLatest(queue: .mainQueue(),
contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction),
strongSelf.context.engine.stickers.availableReactions(), strongSelf.context.engine.stickers.availableReactions(),
@ -991,7 +995,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
actions.context = strongSelf.context actions.context = strongSelf.context
if canAddMessageReactions(message: message), let availableReactions = availableReactions { if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions {
for reaction in availableReactions.reactions { for reaction in availableReactions.reactions {
actions.reactionItems.append(ReactionContextItem( actions.reactionItems.append(ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: reaction.value), reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
@ -1006,12 +1010,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.currentContextController = controller strongSelf.currentContextController = controller
controller.reactionSelected = { [weak controller] value in controller.reactionSelected = { [weak controller] value in
guard let strongSelf = self, let message = updatedMessages.first else { guard let strongSelf = self, let message = messages.first else {
return return
} }
var updatedReaction: String? = value.reaction.rawValue var updatedReaction: String? = value.reaction.rawValue
for attribute in messages[0].attributes { for attribute in topMessage.attributes {
if let attribute = attribute as? ReactionsMessageAttribute { if let attribute = attribute as? ReactionsMessageAttribute {
for reaction in attribute.reactions { for reaction in attribute.reactions {
if reaction.value == value.reaction.rawValue { if reaction.value == value.reaction.rawValue {
@ -1047,32 +1051,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = strongSelf let _ = strongSelf
let _ = itemNode let _ = itemNode
let _ = targetView let _ = targetView
/*let targetFrame = targetFilledNode.view.convert(targetFilledNode.bounds, to: itemNode.view).offsetBy(dx: 0.0, dy: itemNode.insets.top)
if #available(iOS 13.0, *), let meshAnimation = strongSelf.context.meshAnimationCache.get(bundleName: "Hearts") {
if let animationView = MeshRenderer() {
let animationFrame = CGRect(origin: CGPoint(x: targetFrame.midX - 200.0 / 2.0, y: targetFrame.midY - 200.0 / 2.0), size: CGSize(width: 200.0, height: 200.0)).offsetBy(dx: -50.0, dy: 0.0)
animationView.frame = animationFrame
var removeNode: (() -> Void)?
animationView.allAnimationsCompleted = {
removeNode?()
}
let overlayMeshAnimationNode = strongSelf.chatDisplayNode.messageTransitionNode.add(decorationView: animationView, itemNode: itemNode)
removeNode = { [weak overlayMeshAnimationNode] in
guard let strongSelf = self, let overlayMeshAnimationNode = overlayMeshAnimationNode else {
return
}
strongSelf.chatDisplayNode.messageTransitionNode.remove(decorationNode: overlayMeshAnimationNode)
}
animationView.add(mesh: meshAnimation, offset: CGPoint())
}
}*/
}) })
} }
}) })
@ -1096,11 +1074,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.window?.presentInGlobalOverlay(controller) strongSelf.window?.presentInGlobalOverlay(controller)
}) })
} }
}, updateMessageReaction: { [weak self] message, value in }, updateMessageReaction: { [weak self] initialMessage, value in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
guard let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(initialMessage.id) else {
return
}
guard let message = messages.first else {
return
}
if !canAddMessageReactions(message: message) { if !canAddMessageReactions(message: message) {
return return
} }

View File

@ -1049,7 +1049,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
var transition: ContainedViewLayoutTransition = .immediate var transition: ContainedViewLayoutTransition = .immediate
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
if let subject = item.associatedData.subject, case .forwardedMessages = subject { if let subject = item.associatedData.subject, case .forwardedMessages = subject {
transition = .animated(duration: duration, curve: .linear) transition = .animated(duration: duration, curve: .linear)
} else { } else {
@ -1122,7 +1122,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.shareButtonNode = nil strongSelf.shareButtonNode = nil
} }
dateAndStatusApply(false) 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) strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize)
if needsReplyBackground { if needsReplyBackground {
@ -1296,7 +1296,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
strongSelf.addSubnode(actionButtonsNode) strongSelf.addSubnode(actionButtonsNode)
} else { } else {
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
} }
} }

View File

@ -360,8 +360,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var textCutout = TextNodeCutout() var textCutout = TextNodeCutout()
var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode)))? var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode)))? var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)))?
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)? var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)?
@ -514,7 +514,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} else if file.isInstantVideo { } else if file.isInstantVideo {
let displaySize = CGSize(width: 212.0, height: 212.0) let displaySize = CGSize(width: 212.0, height: 212.0)
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload) let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, topMessage: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload)
initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight
contentInstantVideoSizeAndApply = (videoLayout, apply) contentInstantVideoSizeAndApply = (videoLayout, apply)
} else if file.isVideo { } else if file.isVideo {
@ -564,7 +564,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
} }
let (_, refineLayout) = contentFileLayout(context, presentationData, message, associatedData, chatLocation, attributes, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, false, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) let (_, refineLayout) = contentFileLayout(context, presentationData, message, message, associatedData, chatLocation, attributes, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, false, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
refineContentFileLayout = refineLayout refineContentFileLayout = refineLayout
} }
} else if let image = media as? TelegramMediaImage { } else if let image = media as? TelegramMediaImage {
@ -625,7 +625,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets())) let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets()))
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if statusInText, let textStatusType = textStatusType { if statusInText, let textStatusType = textStatusType {
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
context: context, context: context,
@ -634,7 +634,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
impressionCount: viewCount, impressionCount: viewCount,
dateText: dateText, dateText: dateText,
type: textStatusType, type: textStatusType,
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false), layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: nil),
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: associatedData.availableReactions, availableReactions: associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactions,
@ -666,14 +666,14 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0)
} }
var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))? var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))?
if let refineContentImageLayout = refineContentImageLayout { if let refineContentImageLayout = refineContentImageLayout {
let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0)) let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0))
finalizeContentImageLayout = finalizeImageLayout finalizeContentImageLayout = finalizeImageLayout
boundingSize.width = max(boundingSize.width, refinedWidth) boundingSize.width = max(boundingSize.width, refinedWidth)
} }
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))? var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))?
if let refineContentFileLayout = refineContentFileLayout { if let refineContentFileLayout = refineContentFileLayout {
let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize) let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize)
finalizeContentFileLayout = finalizeFileLayout finalizeContentFileLayout = finalizeFileLayout
@ -740,7 +740,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize) imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize)
} }
var contentImageSizeAndApply: (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode)? var contentImageSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)?
if let finalizeContentImageLayout = finalizeContentImageLayout { if let finalizeContentImageLayout = finalizeContentImageLayout {
let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right) let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right)
contentImageSizeAndApply = (size, apply) contentImageSizeAndApply = (size, apply)
@ -754,7 +754,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
adjustedLineHeight += imageHeightAddition + 4.0 adjustedLineHeight += imageHeightAddition + 4.0
} }
var contentFileSizeAndApply: (CGSize, (Bool) -> ChatMessageInteractiveFileNode)? var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)?
if let finalizeContentFileLayout = finalizeContentFileLayout { if let finalizeContentFileLayout = finalizeContentFileLayout {
let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right) let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right)
contentFileSizeAndApply = (size, apply) contentFileSizeAndApply = (size, apply)
@ -788,12 +788,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
adjustedBoundingSize.height += 7.0 + size.height adjustedBoundingSize.height += 7.0 + size.height
} }
var statusSizeAndApply: ((CGSize), (Bool) -> Void)? var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)?
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right) statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right)
} }
if let statusSizeAndApply = statusSizeAndApply { if let statusSizeAndApply = statusSizeAndApply {
adjustedBoundingSize.height += statusSizeAndApply.0.height adjustedBoundingSize.height += statusSizeAndApply.0.height
adjustedLineHeight += statusSizeAndApply.0.height
} }
/*var adjustedStatusFrame: CGRect? /*var adjustedStatusFrame: CGRect?
@ -815,7 +816,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
switch animation { switch animation {
case .None, .Crossfade: case .None, .Crossfade:
hasAnimation = false hasAnimation = false
case let .System(duration): case let .System(duration, _):
hasAnimation = true hasAnimation = true
transition = .animated(duration: duration, curve: .easeInOut) transition = .animated(duration: duration, curve: .easeInOut)
} }
@ -851,7 +852,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { if let (contentImageSize, contentImageApply) = contentImageSizeAndApply {
contentMediaHeight = contentImageSize.height contentMediaHeight = contentImageSize.height
let contentImageNode = contentImageApply(transition, synchronousLoads) let contentImageNode = contentImageApply(animation, synchronousLoads)
if strongSelf.contentImageNode !== contentImageNode { if strongSelf.contentImageNode !== contentImageNode {
strongSelf.contentImageNode = contentImageNode strongSelf.contentImageNode = contentImageNode
contentImageNode.activatePinch = { sourceNode in contentImageNode.activatePinch = { sourceNode in
@ -865,7 +866,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
contentImageNode.visibility = strongSelf.visibility != .none contentImageNode.visibility = strongSelf.visibility != .none
} }
let _ = contentImageApply(transition, synchronousLoads) let _ = contentImageApply(animation, synchronousLoads)
let contentImageFrame: CGRect let contentImageFrame: CGRect
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize)
@ -901,7 +902,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
if let (contentFileSize, contentFileApply) = contentFileSizeAndApply { if let (contentFileSize, contentFileApply) = contentFileSizeAndApply {
contentMediaHeight = contentFileSize.height contentMediaHeight = contentFileSize.height
let contentFileNode = contentFileApply(synchronousLoads) let contentFileNode = contentFileApply(synchronousLoads, animation)
if strongSelf.contentFileNode !== contentFileNode { if strongSelf.contentFileNode !== contentFileNode {
strongSelf.contentFileNode = contentFileNode strongSelf.contentFileNode = contentFileNode
strongSelf.addSubnode(contentFileNode) strongSelf.addSubnode(contentFileNode)
@ -949,7 +950,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
strongSelf.addSubnode(strongSelf.statusNode) strongSelf.addSubnode(strongSelf.statusNode)
} }
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0) strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0)
statusSizeAndApply.1(animation.isAnimated) statusSizeAndApply.1(animation)
} else if strongSelf.statusNode.supernode != nil { } else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode() strongSelf.statusNode.removeFromSupernode()
} }

View File

@ -93,6 +93,11 @@ class ChatMessageBackground: ASDisplayNode {
transition.updateFrame(node: self.outlineImageNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)) transition.updateFrame(node: self.outlineImageNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0))
} }
func updateLayout(size: CGSize, transition: ListViewItemUpdateAnimation) {
transition.animator.updateFrame(layer: self.imageNode.layer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0), completion: nil)
transition.animator.updateFrame(layer: self.outlineImageNode.layer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0), completion: nil)
}
func setMaskMode(_ maskMode: Bool) { func setMaskMode(_ maskMode: Bool) {
if let type = self.type, let hasWallpaper = self.hasWallpaper, let highlighted = self.currentHighlighted, let graphics = self.graphics, let backgroundNode = self.backgroundNode { if let type = self.type, let hasWallpaper = self.hasWallpaper, let highlighted = self.currentHighlighted, let graphics = self.graphics, let backgroundNode = self.backgroundNode {
self.setType(type: type, highlighted: highlighted, graphics: graphics, maskMode: maskMode, hasWallpaper: hasWallpaper, transition: .immediate, backgroundNode: backgroundNode) self.setType(type: type, highlighted: highlighted, graphics: graphics, maskMode: maskMode, hasWallpaper: hasWallpaper, transition: .immediate, backgroundNode: backgroundNode)

View File

@ -104,6 +104,7 @@ final class ChatMessageBubbleContentItem {
let context: AccountContext let context: AccountContext
let controllerInteraction: ChatControllerInteraction let controllerInteraction: ChatControllerInteraction
let message: Message let message: Message
let topMessage: Message
let read: Bool let read: Bool
let chatLocation: ChatLocation let chatLocation: ChatLocation
let presentationData: ChatPresentationData let presentationData: ChatPresentationData
@ -112,10 +113,11 @@ final class ChatMessageBubbleContentItem {
let isItemPinned: Bool let isItemPinned: Bool
let isItemEdited: Bool let isItemEdited: Bool
init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) { init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) {
self.context = context self.context = context
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.message = message self.message = message
self.topMessage = topMessage
self.read = read self.read = read
self.chatLocation = chatLocation self.chatLocation = chatLocation
self.presentationData = presentationData self.presentationData = presentationData

View File

@ -174,6 +174,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
} }
let firstMessage = item.content.firstMessage 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)))
}
}
if !isAction && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) { if !isAction && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) {
var hasDiscussion = false var hasDiscussion = false
if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) { if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) {
@ -920,7 +927,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), 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, (Bool) -> ChatMessageActionButtonsNode)),
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode)), mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
layoutConstants: ChatMessageItemLayoutConstants, layoutConstants: ChatMessageItemLayoutConstants,
currentItem: ChatMessageItem?, currentItem: ChatMessageItem?,
currentForwardInfo: (Peer?, String?)?, currentForwardInfo: (Peer?, String?)?,
@ -1330,7 +1337,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition) prepareContentPosition = .linear(top: topPosition, bottom: refinedBottomPosition)
} }
let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes, isItemPinned: isItemPinned, isItemEdited: isItemEdited) let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, topMessage: item.content.firstMessage, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes, isItemPinned: isItemPinned, isItemEdited: isItemEdited)
var itemSelection: Bool? var itemSelection: Bool?
switch content { switch content {
@ -1457,7 +1464,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus) let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus)
var calculatedGroupFramesAndSize: ([(CGRect, MosaicItemPosition)], CGSize)? var calculatedGroupFramesAndSize: ([(CGRect, MosaicItemPosition)], CGSize)?
var mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)? var mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?
if let mosaicRange = mosaicRange { if let mosaicRange = mosaicRange {
let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
@ -2107,7 +2114,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, Bool, (ListViewItemUpdateAnimation, Bool) -> Void)], contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, Bool, (ListViewItemUpdateAnimation, Bool) -> Void)],
contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)], contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)],
mosaicStatusOrigin: CGPoint?, mosaicStatusOrigin: CGPoint?,
mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)?, mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?,
needsShareButton: Bool needsShareButton: Bool
) -> Void { ) -> Void {
guard let strongSelf = selfReference.value else { guard let strongSelf = selfReference.value else {
@ -2123,10 +2130,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
strongSelf.updateAccessibilityData(accessibilityData) strongSelf.updateAccessibilityData(accessibilityData)
var transition: ContainedViewLayoutTransition = .immediate var legacyTransition: ContainedViewLayoutTransition = .immediate
var useDisplayLinkAnimations = false var useDisplayLinkAnimations = false
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
transition = .animated(duration: duration, curve: .spring) legacyTransition = .animated(duration: duration, curve: .spring)
if let subject = item.associatedData.subject, case .forwardedMessages = subject { if let subject = item.associatedData.subject, case .forwardedMessages = subject {
useDisplayLinkAnimations = true useDisplayLinkAnimations = true
@ -2150,9 +2157,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper
if item.presentationData.theme.theme.forceSync { if item.presentationData.theme.theme.forceSync {
transition = .immediate legacyTransition = .immediate
} }
strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: transition, backgroundNode: presentationContext.backgroundNode) strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: legacyTransition, backgroundNode: presentationContext.backgroundNode)
strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode, backgroundNode: presentationContext.backgroundNode) strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode, backgroundNode: presentationContext.backgroundNode)
strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics) strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics)
@ -2178,14 +2185,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let deliveryFailedFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + deliveryFailedInset - deliveryFailedSize.width, y: backgroundFrame.maxY - deliveryFailedSize.height), size: deliveryFailedSize) let deliveryFailedFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + deliveryFailedInset - deliveryFailedSize.width, y: backgroundFrame.maxY - deliveryFailedSize.height), size: deliveryFailedSize)
if isAppearing { if isAppearing {
deliveryFailedNode.frame = deliveryFailedFrame deliveryFailedNode.frame = deliveryFailedFrame
transition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0)) legacyTransition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0))
} else { } else {
transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedFrame) animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedFrame, completion: nil)
} }
} else if let deliveryFailedNode = strongSelf.deliveryFailedNode { } else if let deliveryFailedNode = strongSelf.deliveryFailedNode {
strongSelf.deliveryFailedNode = nil strongSelf.deliveryFailedNode = nil
transition.updateAlpha(node: deliveryFailedNode, alpha: 0.0) animation.animator.updateAlpha(layer: deliveryFailedNode.layer, alpha: 0.0, completion: nil)
transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in
deliveryFailedNode?.removeFromSupernode() deliveryFailedNode?.removeFromSupernode()
}) })
} }
@ -2194,16 +2201,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.nameNode = nameNode strongSelf.nameNode = nameNode
nameNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync nameNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync
let previousNameNodeFrame = nameNode.frame //let previousNameNodeFrame = nameNode.frame
let nameNodeFrame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0) let nameNodeFrame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0)
nameNode.frame = nameNodeFrame
if nameNode.supernode == nil { if nameNode.supernode == nil {
if !nameNode.isNodeLoaded { if !nameNode.isNodeLoaded {
nameNode.isUserInteractionEnabled = false nameNode.isUserInteractionEnabled = false
} }
strongSelf.clippingNode.addSubnode(nameNode) strongSelf.clippingNode.addSubnode(nameNode)
nameNode.frame = nameNodeFrame
} else { } else {
transition.animatePositionAdditive(node: nameNode, offset: CGPoint(x: previousNameNodeFrame.maxX - nameNodeFrame.maxX, y: 0.0)) animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil)
} }
if let credibilityIconImage = currentCredibilityIconImage { if let credibilityIconImage = currentCredibilityIconImage {
@ -2232,9 +2239,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.clippingNode.addSubnode(adminBadgeNode) strongSelf.clippingNode.addSubnode(adminBadgeNode)
adminBadgeNode.frame = adminBadgeFrame adminBadgeNode.frame = adminBadgeFrame
} else { } else {
let previousAdminBadgeFrame = adminBadgeNode.frame //let previousAdminBadgeFrame = adminBadgeNode.frame
adminBadgeNode.frame = adminBadgeFrame animation.animator.updateFrame(layer: adminBadgeNode.layer, frame: adminBadgeFrame, completion: nil)
transition.animatePositionAdditive(node: adminBadgeNode, offset: CGPoint(x: previousAdminBadgeFrame.maxX - adminBadgeFrame.maxX, y: 0.0))
} }
} else { } else {
strongSelf.adminBadgeNode?.removeFromSupernode() strongSelf.adminBadgeNode?.removeFromSupernode()
@ -2269,7 +2275,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
let previousForwardInfoNodeFrame = forwardInfoNode.frame let previousForwardInfoNodeFrame = forwardInfoNode.frame
let forwardInfoFrame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + forwardInfoOriginY), size: CGSize(width: bubbleContentWidth, height: forwardInfoSizeApply.0.height)) let forwardInfoFrame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + forwardInfoOriginY), size: CGSize(width: bubbleContentWidth, height: forwardInfoSizeApply.0.height))
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
if animateFrame { if animateFrame {
if useDisplayLinkAnimations { if useDisplayLinkAnimations {
let animation = ListViewAnimation(from: previousForwardInfoNodeFrame, to: forwardInfoFrame, duration: duration * UIView.animationDurationFactor(), curve: strongSelf.preferredAnimationCurve, beginAt: beginAt, update: { _, frame in let animation = ListViewAnimation(from: previousForwardInfoNodeFrame, to: forwardInfoFrame, duration: duration * UIView.animationDurationFactor(), curve: strongSelf.preferredAnimationCurve, beginAt: beginAt, update: { _, frame in
@ -2309,7 +2315,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
let previousReplyInfoNodeFrame = replyInfoNode.frame let previousReplyInfoNodeFrame = replyInfoNode.frame
replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0) replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0)
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
if animateFrame { if animateFrame {
replyInfoNode.layer.animateFrame(from: previousReplyInfoNodeFrame, to: replyInfoNode.frame, duration: duration, timingFunction: timingFunction) replyInfoNode.layer.animateFrame(from: previousReplyInfoNodeFrame, to: replyInfoNode.frame, duration: duration, timingFunction: timingFunction)
} }
@ -2561,7 +2567,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: useContentOrigin ? contentOrigin.y : 0.0) let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: useContentOrigin ? contentOrigin.y : 0.0)
let previousContentNodeFrame = contentNode.frame let previousContentNodeFrame = contentNode.frame
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
var animateFrame = false var animateFrame = false
var animateAlpha = false var animateAlpha = false
if let addedContentNodes = addedContentNodes { if let addedContentNodes = addedContentNodes {
@ -2581,8 +2587,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}) })
strongSelf.setAnimationForKey("contentNode\(contentNodeIndex)Frame", animation: animation) strongSelf.setAnimationForKey("contentNode\(contentNodeIndex)Frame", animation: animation)
} else { } else {
contentNode.frame = contentNodeFrame animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil)
contentNode.layer.animateFrame(from: previousContentNodeFrame, to: contentNodeFrame, duration: duration, timingFunction: timingFunction)
} }
} else if animateAlpha { } else if animateAlpha {
contentNode.frame = contentNodeFrame contentNode.frame = contentNodeFrame
@ -2600,7 +2605,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
if let mosaicStatusOrigin = mosaicStatusOrigin, let (size, apply) = mosaicStatusSizeAndApply { if let mosaicStatusOrigin = mosaicStatusOrigin, let (size, apply) = mosaicStatusSizeAndApply {
let mosaicStatusNode = apply(transition.isAnimated) let mosaicStatusNode = apply(animation)
if mosaicStatusNode !== strongSelf.mosaicStatusNode { if mosaicStatusNode !== strongSelf.mosaicStatusNode {
strongSelf.mosaicStatusNode?.removeFromSupernode() strongSelf.mosaicStatusNode?.removeFromSupernode()
strongSelf.mosaicStatusNode = mosaicStatusNode strongSelf.mosaicStatusNode = mosaicStatusNode
@ -2627,18 +2632,53 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview { if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) { if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) {
strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame) /*strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame)
if let type = strongSelf.backgroundNode.type { if let type = strongSelf.backgroundNode.type {
if case .none = type { if case .none = type {
} else { } else {
strongSelf.clippingNode.clipsToBounds = true 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
animation.animator.updateBounds(layer: strongSelf.clippingNode.layer, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size), completion: { [weak strongSelf] _ in
let _ = strongSelf
//strongSelf?.clippingNode.clipsToBounds = false
})
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)
if let type = strongSelf.backgroundNode.type {
var incomingOffset: CGFloat = 0.0
switch type {
case .incoming:
incomingOffset = 5.0
default:
break
} }
strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
if !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
}
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 { if let shareButtonNode = strongSelf.shareButtonNode {
let currentBackgroundFrame = strongSelf.backgroundNode.frame let currentBackgroundFrame = strongSelf.backgroundNode.frame
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true) let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true)
shareButtonNode.frame = CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) 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 { } else {
if let _ = strongSelf.backgroundFrameTransition { if let _ = strongSelf.backgroundFrameTransition {
@ -2652,14 +2692,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview { if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview {
transition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame) legacyTransition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: strongSelf.clippingNode, frame: backgroundFrame) legacyTransition.updateFrame(node: strongSelf.clippingNode, frame: backgroundFrame)
transition.updateBounds(node: strongSelf.clippingNode, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size)) legacyTransition.updateBounds(node: strongSelf.clippingNode, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size))
strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition) strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: legacyTransition)
strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition) strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: legacyTransition)
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: transition) strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: legacyTransition)
} else { } else {
strongSelf.backgroundNode.frame = backgroundFrame strongSelf.backgroundNode.frame = backgroundFrame
strongSelf.clippingNode.frame = backgroundFrame strongSelf.clippingNode.frame = backgroundFrame
@ -2702,7 +2742,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea) strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea)
} else { } else {
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: timingFunction) actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: timingFunction)
} }
} }
@ -2780,7 +2820,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue) super.animateFrameTransition(progress, currentValue)
if let backgroundFrameTransition = self.backgroundFrameTransition { /*if let backgroundFrameTransition = self.backgroundFrameTransition {
let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect
self.backgroundNode.frame = backgroundFrame self.backgroundNode.frame = backgroundFrame
@ -2819,7 +2859,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.clippingNode.clipsToBounds = false self.clippingNode.clipsToBounds = false
} }
} }*/
} }
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {

View File

@ -417,7 +417,3 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
return nil return nil
} }
} }

View File

@ -196,7 +196,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
statusType = nil statusType = nil
} }
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType { if let statusType = statusType {
var isReplyThread = false var isReplyThread = false
if case .replyThread = item.chatLocation { if case .replyThread = item.chatLocation {
@ -210,7 +210,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
impressionCount: viewCount, impressionCount: viewCount,
dateText: dateText, dateText: dateText,
type: statusType, type: statusType,
layoutInput: .trailingContent(contentWidth: 1000.0, preferAdditionalInset: true), layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: nil),
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude), constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactions,
@ -305,9 +305,9 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: strongSelf.textNode.frame.maxY + 2.0), size: statusSizeAndApply.0) strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: strongSelf.textNode.frame.maxY + 2.0), size: statusSizeAndApply.0)
if strongSelf.dateAndStatusNode.supernode == nil { if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.dateAndStatusNode) strongSelf.addSubnode(strongSelf.dateAndStatusNode)
statusSizeAndApply.1(false) statusSizeAndApply.1(.None)
} else { } else {
statusSizeAndApply.1(animation.isAnimated) statusSizeAndApply.1(animation)
} }
} else if strongSelf.dateAndStatusNode.supernode != nil { } else if strongSelf.dateAndStatusNode.supernode != nil {
strongSelf.dateAndStatusNode.removeFromSupernode() strongSelf.dateAndStatusNode.removeFromSupernode()

View File

@ -87,8 +87,16 @@ private final class StatusReactionNode: ASDisplayNode {
class ChatMessageDateAndStatusNode: ASDisplayNode { class ChatMessageDateAndStatusNode: ASDisplayNode {
struct ReactionSettings {
var preferAdditionalInset: Bool
init(preferAdditionalInset: Bool) {
self.preferAdditionalInset = preferAdditionalInset
}
}
enum LayoutInput { enum LayoutInput {
case trailingContent(contentWidth: CGFloat, preferAdditionalInset: Bool) case trailingContent(contentWidth: CGFloat, reactionSettings: ReactionSettings?)
case standalone case standalone
} }
@ -193,7 +201,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
} }
func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)) { func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)) {
let dateLayout = TextNode.asyncLayout(self.dateNode) let dateLayout = TextNode.asyncLayout(self.dateNode)
var checkReadNode = self.checkReadNode var checkReadNode = self.checkReadNode
@ -211,8 +219,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode) let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode)
let makeReactionCountLayout = TextNode.asyncLayout(self.reactionCountNode) let makeReactionCountLayout = TextNode.asyncLayout(self.reactionCountNode)
let previousLayoutSize = self.layoutSize
let reactionButtonsContainer = self.reactionButtonsContainer let reactionButtonsContainer = self.reactionButtonsContainer
return { [weak self] arguments in return { [weak self] arguments in
@ -592,7 +598,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
constrainedWidth: arguments.constrainedSize.width, constrainedWidth: arguments.constrainedSize.width,
transition: .immediate transition: .immediate
) )
case let .trailingContent(contentWidth, preferAdditionalInset): case let .trailingContent(contentWidth, reactionSettings):
if let _ = reactionSettings {
reactionButtons = reactionButtonsContainer.update( reactionButtons = reactionButtonsContainer.update(
context: arguments.context, context: arguments.context,
action: { value in action: { value in
@ -626,6 +633,21 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
constrainedWidth: arguments.constrainedSize.width, constrainedWidth: arguments.constrainedSize.width,
transition: .immediate transition: .immediate
) )
} else {
reactionButtons = reactionButtonsContainer.update(
context: arguments.context,
action: { value in
guard let strongSelf = self else {
return
}
strongSelf.reactionSelected?(value)
},
reactions: [],
colors: reactionColors,
constrainedWidth: arguments.constrainedSize.width,
transition: .immediate
)
}
var reactionButtonsSize = CGSize() var reactionButtonsSize = CGSize()
var currentRowWidth: CGFloat = 0.0 var currentRowWidth: CGFloat = 0.0
@ -664,17 +686,22 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
resultingHeight = 0.0 resultingHeight = 0.0
} }
} else { } else {
if preferAdditionalInset { if let reactionSettings = reactionSettings {
if reactionSettings.preferAdditionalInset {
verticalReactionsInset = 5.0 verticalReactionsInset = 5.0
} else { } else {
verticalReactionsInset = 2.0 verticalReactionsInset = 2.0
} }
} else {
verticalReactionsInset = 0.0
}
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width { if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
resultingWidth = max(layoutSize.width, reactionButtonsSize.width) resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height
verticalInset = verticalReactionsInset + reactionButtonsSize.height verticalInset = verticalReactionsInset + reactionButtonsSize.height
} else { } else {
resultingWidth = layoutSize.width + currentRowWidth resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height
resultingHeight = verticalReactionsInset + reactionButtonsSize.height resultingHeight = verticalReactionsInset + reactionButtonsSize.height
} }
@ -682,7 +709,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
return (resultingWidth, { boundingWidth in return (resultingWidth, { boundingWidth in
return (CGSize(width: boundingWidth, height: resultingHeight), { animated in return (CGSize(width: boundingWidth, height: resultingHeight), { animation in
if let strongSelf = self { if let strongSelf = self {
let leftOffset = boundingWidth - layoutSize.width let leftOffset = boundingWidth - layoutSize.width
@ -699,14 +726,28 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if item.view.superview == nil { if item.view.superview == nil {
strongSelf.view.addSubview(item.view) strongSelf.view.addSubview(item.view)
}
item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size) 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)
}
} else {
animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil)
}
reactionButtonPosition.x += item.size.width + 6.0 reactionButtonPosition.x += item.size.width + 6.0
} }
for view in reactionButtons.removedViews { for view in reactionButtons.removedViews {
if animation.isAnimated {
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
view?.removeFromSuperview()
})
} else {
view.removeFromSuperview() view.removeFromSuperview()
} }
}
if backgroundImage != nil { if backgroundImage != nil {
if let currentBackgroundNode = currentBackgroundNode { if let currentBackgroundNode = currentBackgroundNode {
@ -719,11 +760,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
} }
if let backgroundNode = strongSelf.backgroundNode { if let backgroundNode = strongSelf.backgroundNode {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate animation.animator.updateFrame(layer: backgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
if let previousLayoutSize = previousLayoutSize {
backgroundNode.frame = backgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
}
transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize))
} }
} else { } else {
if let backgroundNode = strongSelf.backgroundNode { if let backgroundNode = strongSelf.backgroundNode {
@ -735,12 +772,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if let blurredBackgroundColor = blurredBackgroundColor { if let blurredBackgroundColor = blurredBackgroundColor {
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate) blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
if let previousLayoutSize = previousLayoutSize { blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: animation.transition)
blurredBackgroundNode.frame = blurredBackgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
}
transition.updateFrame(node: blurredBackgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize))
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: transition)
} else { } else {
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1) let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
strongSelf.blurredBackgroundNode = blurredBackgroundNode strongSelf.blurredBackgroundNode = blurredBackgroundNode
@ -771,7 +804,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
strongSelf.impressionIcon = nil strongSelf.impressionIcon = nil
} }
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size) animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
if let clockFrameNode = clockFrameNode { if let clockFrameNode = clockFrameNode {
if strongSelf.clockFrameNode == nil { if strongSelf.clockFrameNode == nil {
@ -781,7 +814,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} else if themeUpdated { } else if themeUpdated {
clockFrameNode.image = clockFrameImage clockFrameNode.image = clockFrameImage
} }
clockFrameNode.position = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset) animation.animator.updatePosition(layer: clockFrameNode.layer, position: CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset), completion: nil)
if let clockFrameNode = strongSelf.clockFrameNode { if let clockFrameNode = strongSelf.clockFrameNode {
maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0) maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0)
} }
@ -798,7 +831,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} else if themeUpdated { } else if themeUpdated {
clockMinNode.image = clockMinImage clockMinNode.image = clockMinImage
} }
clockMinNode.position = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset) animation.animator.updatePosition(layer: clockMinNode.layer, position: CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset), completion: nil)
if let clockMinNode = strongSelf.clockMinNode { if let clockMinNode = strongSelf.clockMinNode {
maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0) maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0)
} }
@ -813,24 +846,26 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
checkSentNode.image = loadedCheckFullImage checkSentNode.image = loadedCheckFullImage
strongSelf.checkSentNode = checkSentNode strongSelf.checkSentNode = checkSentNode
strongSelf.addSubnode(checkSentNode) strongSelf.addSubnode(checkSentNode)
animateSentNode = animated animateSentNode = animation.isAnimated
} else if themeUpdated { } else if themeUpdated {
checkSentNode.image = loadedCheckFullImage checkSentNode.image = loadedCheckFullImage
} }
if let checkSentFrame = checkSentFrame { if let checkSentFrame = checkSentFrame {
if checkSentNode.isHidden { if checkSentNode.isHidden {
animateSentNode = animated animateSentNode = animation.isAnimated
checkSentNode.frame = checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
} else {
animation.animator.updateFrame(layer: checkSentNode.layer, frame: checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset), completion: nil)
} }
checkSentNode.isHidden = false checkSentNode.isHidden = false
checkSentNode.frame = checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
} else { } else {
checkSentNode.isHidden = true checkSentNode.isHidden = true
} }
var animateReadNode = false var animateReadNode = false
if strongSelf.checkReadNode == nil { if strongSelf.checkReadNode == nil {
animateReadNode = animated animateReadNode = animation.isAnimated
checkReadNode.image = loadedCheckPartialImage checkReadNode.image = loadedCheckPartialImage
strongSelf.checkReadNode = checkReadNode strongSelf.checkReadNode = checkReadNode
strongSelf.addSubnode(checkReadNode) strongSelf.addSubnode(checkReadNode)
@ -840,10 +875,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if let checkReadFrame = checkReadFrame { if let checkReadFrame = checkReadFrame {
if checkReadNode.isHidden { if checkReadNode.isHidden {
animateReadNode = animated animateReadNode = animation.isAnimated
checkReadNode.frame = checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
} else {
animation.animator.updateFrame(layer: checkReadNode.layer, frame: checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset), completion: nil)
} }
checkReadNode.isHidden = false checkReadNode.isHidden = false
checkReadNode.frame = checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
} else { } else {
checkReadNode.isHidden = true checkReadNode.isHidden = true
} }
@ -865,13 +902,15 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if !"".isEmpty { if !"".isEmpty {
for i in 0 ..< arguments.reactions.count { for i in 0 ..< arguments.reactions.count {
let node: StatusReactionNode let node: StatusReactionNode
var animateNode = true
if strongSelf.reactionNodes.count > i { if strongSelf.reactionNodes.count > i {
node = strongSelf.reactionNodes[i] node = strongSelf.reactionNodes[i]
} else { } else {
animateNode = false
node = StatusReactionNode() node = StatusReactionNode()
if strongSelf.reactionNodes.count > i { if strongSelf.reactionNodes.count > i {
let previousNode = strongSelf.reactionNodes[i] let previousNode = strongSelf.reactionNodes[i]
if animated { if animation.isAnimated {
previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in
previousNode?.removeFromSupernode() previousNode?.removeFromSupernode()
}) })
@ -887,11 +926,16 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
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(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)
if node.supernode == nil { if node.supernode == nil {
strongSelf.addSubnode(node) strongSelf.addSubnode(node)
if animated { if animation.isAnimated {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} }
} }
node.frame = 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 + 1.0), size: CGSize(width: reactionSize, height: reactionSize))
if animateNode {
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
} else {
node.frame = nodeFrame
}
reactionOffset += reactionSize + reactionSpacing reactionOffset += reactionSize + reactionSpacing
} }
if !arguments.reactions.isEmpty { if !arguments.reactions.isEmpty {
@ -900,10 +944,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count { for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count {
let node = strongSelf.reactionNodes.removeLast() let node = strongSelf.reactionNodes.removeLast()
if animated { if animation.isAnimated {
if let previousLayoutSize = previousLayoutSize {
node.frame = node.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
}
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) 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.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
node?.removeFromSupernode() node?.removeFromSupernode()
@ -920,18 +961,16 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
strongSelf.reactionCountNode?.removeFromSupernode() strongSelf.reactionCountNode?.removeFromSupernode()
strongSelf.addSubnode(node) strongSelf.addSubnode(node)
strongSelf.reactionCountNode = node strongSelf.reactionCountNode = node
if animated { if animation.isAnimated {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} }
} }
node.frame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset + 1.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 reactionOffset += 1.0 + layout.size.width + 4.0
} else if let reactionCountNode = strongSelf.reactionCountNode { } else if let reactionCountNode = strongSelf.reactionCountNode {
strongSelf.reactionCountNode = nil strongSelf.reactionCountNode = nil
if animated { if animation.isAnimated {
if let previousLayoutSize = previousLayoutSize {
reactionCountNode.frame = reactionCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
}
reactionCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountNode] _ in reactionCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountNode] _ in
reactionCountNode?.removeFromSupernode() reactionCountNode?.removeFromSupernode()
}) })
@ -948,18 +987,16 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
if currentRepliesIcon.supernode == nil { if currentRepliesIcon.supernode == nil {
strongSelf.repliesIcon = currentRepliesIcon strongSelf.repliesIcon = currentRepliesIcon
strongSelf.addSubnode(currentRepliesIcon) strongSelf.addSubnode(currentRepliesIcon)
if animated { if animation.isAnimated {
currentRepliesIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) currentRepliesIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} }
} }
currentRepliesIcon.frame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - repliesIconSize.height) / 2.0)), size: repliesIconSize) let repliesIconFrame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - repliesIconSize.height) / 2.0)), size: repliesIconSize)
animation.animator.updateFrame(layer: currentRepliesIcon.layer, frame: repliesIconFrame, completion: nil)
reactionOffset += 9.0 reactionOffset += 9.0
} else if let repliesIcon = strongSelf.repliesIcon { } else if let repliesIcon = strongSelf.repliesIcon {
strongSelf.repliesIcon = nil strongSelf.repliesIcon = nil
if animated { if animation.isAnimated {
if let previousLayoutSize = previousLayoutSize {
repliesIcon.frame = repliesIcon.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
}
repliesIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repliesIcon] _ in repliesIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repliesIcon] _ in
repliesIcon?.removeFromSupernode() repliesIcon?.removeFromSupernode()
}) })
@ -974,18 +1011,16 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
strongSelf.replyCountNode?.removeFromSupernode() strongSelf.replyCountNode?.removeFromSupernode()
strongSelf.addSubnode(node) strongSelf.addSubnode(node)
strongSelf.replyCountNode = node strongSelf.replyCountNode = node
if animated { if animation.isAnimated {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} }
} }
node.frame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) let replyCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size)
animation.animator.updateFrame(layer: node.layer, frame: replyCountFrame, completion: nil)
reactionOffset += 4.0 + layout.size.width reactionOffset += 4.0 + layout.size.width
} else if let replyCountNode = strongSelf.replyCountNode { } else if let replyCountNode = strongSelf.replyCountNode {
strongSelf.replyCountNode = nil strongSelf.replyCountNode = nil
if animated { if animation.isAnimated {
if let previousLayoutSize = previousLayoutSize {
replyCountNode.frame = replyCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
}
replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in
replyCountNode?.removeFromSupernode() replyCountNode?.removeFromSupernode()
}) })
@ -999,11 +1034,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
} }
static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode)) { static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)) {
let currentLayout = node?.asyncLayout() let currentLayout = node?.asyncLayout()
return { arguments in return { arguments in
let resultNode: ChatMessageDateAndStatusNode let resultNode: ChatMessageDateAndStatusNode
let resultSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)) let resultSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))
if let node = node, let currentLayout = currentLayout { if let node = node, let currentLayout = currentLayout {
resultNode = node resultNode = node
resultSuggestedWidthAndContinue = currentLayout(arguments) resultSuggestedWidthAndContinue = currentLayout(arguments)
@ -1014,8 +1049,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
return (resultSuggestedWidthAndContinue.0, { boundingWidth in return (resultSuggestedWidthAndContinue.0, { boundingWidth in
let (size, apply) = resultSuggestedWidthAndContinue.1(boundingWidth) let (size, apply) = resultSuggestedWidthAndContinue.1(boundingWidth)
return (size, { animated in return (size, { animation in
apply(animated) apply(animation)
return resultNode return resultNode
}) })

View File

@ -108,7 +108,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.associatedData, item.chatLocation, item.attributes, item.isItemPinned, item.isItemEdited, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.topMessage, item.associatedData, item.chatLocation, item.attributes, item.isItemPinned, item.isItemEdited, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height))
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -130,13 +130,13 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + bottomInset), { [weak self] _, synchronousLoads in return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + bottomInset), { [weak self] animation, synchronousLoads in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.interactiveFileNode.frame = CGRect(origin: CGPoint(x: layoutConstants.file.bubbleInsets.left, y: layoutConstants.file.bubbleInsets.top), size: fileSize) strongSelf.interactiveFileNode.frame = CGRect(origin: CGPoint(x: layoutConstants.file.bubbleInsets.left, y: layoutConstants.file.bubbleInsets.top), size: fileSize)
fileApply(synchronousLoads) fileApply(synchronousLoads, animation)
} }
}) })
}) })

View File

@ -387,7 +387,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
isReplyThread = true isReplyThread = true
} }
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload) let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.content.firstMessage, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload)
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
@ -775,7 +775,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
} }
strongSelf.addSubnode(actionButtonsNode) strongSelf.addSubnode(actionButtonsNode)
} else { } else {
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
} }
} }
@ -1184,7 +1184,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
effectiveAvatarInset *= (1.0 - scaleProgress) effectiveAvatarInset *= (1.0 - scaleProgress)
displaySize = CGSize(width: initialSize.width + (targetSize.width - initialSize.width) * animationProgress, height: initialSize.height + (targetSize.height - initialSize.height) * animationProgress) displaySize = CGSize(width: initialSize.width + (targetSize.width - initialSize.width) * animationProgress, height: initialSize.height + (targetSize.height - initialSize.height) * animationProgress)
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload) let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.content.firstMessageAttributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, maximumDisplaySize, scaleProgress, .free, self.appliedAutomaticDownload)
let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize)
@ -1198,8 +1198,6 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
} }
videoApply(videoLayoutData, .immediate) videoApply(videoLayoutData, .immediate)
if let shareButtonNode = self.shareButtonNode { if let shareButtonNode = self.shareButtonNode {
let buttonSize = shareButtonNode.frame.size let buttonSize = shareButtonNode.frame.size
shareButtonNode.frame = CGRect(origin: CGPoint(x: min(params.width - buttonSize.width - 8.0, videoFrame.maxX - 7.0), y: videoFrame.maxY - 24.0 - buttonSize.height), size: buttonSize) shareButtonNode.frame = CGRect(origin: CGPoint(x: min(params.width - buttonSize.width - 8.0, videoFrame.maxX - 7.0), y: videoFrame.maxY - 24.0 - buttonSize.height), size: buttonSize)

View File

@ -213,7 +213,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
} }
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) { func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) {
let currentFile = self.file let currentFile = self.file
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
@ -223,7 +223,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let currentMessage = self.message let currentMessage = self.message
return { context, presentationData, message, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in return { context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0)) let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
let descriptionFont = Font.with(size: floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) let descriptionFont = Font.with(size: floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
@ -422,7 +422,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
controlAreaWidth = progressFrame.maxX + 8.0 controlAreaWidth = progressFrame.maxX + 8.0
} }
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = dateAndStatusType { if let statusType = dateAndStatusType {
var edited = false var edited = false
if attributes.updatingMedia != nil { if attributes.updatingMedia != nil {
@ -430,7 +430,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: topMessage.attributes)?.reactions ?? []
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -455,7 +455,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
impressionCount: viewCount, impressionCount: viewCount,
dateText: dateText, dateText: dateText,
type: statusType, type: statusType,
layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, preferAdditionalInset: true), layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)),
constrainedSize: constrainedSize, constrainedSize: constrainedSize,
availableReactions: associatedData.availableReactions, availableReactions: associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactions,
@ -520,7 +520,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height) fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height)
} }
var statusSizeAndApply: (CGSize, (Bool) -> Void)? var statusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> Void)?
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth) statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth)
} }
@ -541,7 +541,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
streamingCacheStatusFrame = CGRect() streamingCacheStatusFrame = CGRect()
} }
return (fittedLayoutSize, { [weak self] synchronousLoads in return (fittedLayoutSize, { [weak self] synchronousLoads, animation in
if let strongSelf = self { if let strongSelf = self {
strongSelf.context = context strongSelf.context = context
strongSelf.presentationData = presentationData strongSelf.presentationData = presentationData
@ -575,11 +575,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
statusReferenceFrame = progressFrame.offsetBy(dx: 0.0, dy: 8.0) statusReferenceFrame = progressFrame.offsetBy(dx: 0.0, dy: 8.0)
} }
if let statusSizeAndApply = statusSizeAndApply { if let statusSizeAndApply = statusSizeAndApply {
let statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0)
if strongSelf.dateAndStatusNode.supernode == nil { if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.dateAndStatusNode.frame = statusFrame
strongSelf.addSubnode(strongSelf.dateAndStatusNode) strongSelf.addSubnode(strongSelf.dateAndStatusNode)
} else {
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: statusFrame, completion: nil)
} }
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0) statusSizeAndApply.1(animation)
statusSizeAndApply.1(false)
} else if strongSelf.dateAndStatusNode.supernode != nil { } else if strongSelf.dateAndStatusNode.supernode != nil {
strongSelf.dateAndStatusNode.removeFromSupernode() strongSelf.dateAndStatusNode.removeFromSupernode()
} }
@ -1057,12 +1060,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize) self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
} }
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) { static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))) {
let currentAsyncLayout = node?.asyncLayout() let currentAsyncLayout = node?.asyncLayout()
return { context, presentationData, message, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in return { context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
var fileNode: ChatMessageInteractiveFileNode var fileNode: ChatMessageInteractiveFileNode
var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void)))
if let node = node, let currentAsyncLayout = currentAsyncLayout { if let node = node, let currentAsyncLayout = currentAsyncLayout {
fileNode = node fileNode = node
@ -1072,7 +1075,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
fileLayout = fileNode.asyncLayout() fileLayout = fileNode.asyncLayout()
} }
let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize) let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize)
return (initialWidth, { constrainedSize in return (initialWidth, { constrainedSize in
let (finalWidth, finalLayout) = continueLayout(constrainedSize) let (finalWidth, finalLayout) = continueLayout(constrainedSize)
@ -1080,8 +1083,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
return (finalWidth, { boundingWidth in return (finalWidth, { boundingWidth in
let (finalSize, apply) = finalLayout(boundingWidth) let (finalSize, apply) = finalLayout(boundingWidth)
return (finalSize, { synchronousLoads in return (finalSize, { synchronousLoads, animation in
apply(synchronousLoads) apply(synchronousLoads, animation)
return fileNode return fileNode
}) })
}) })

View File

@ -362,7 +362,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
} }
} }
dateAndStatusApply(false) dateAndStatusApply(.None)
switch layoutData { switch layoutData {
case let .unconstrained(width): case let .unconstrained(width):
let dateAndStatusOrigin: CGPoint let dateAndStatusOrigin: CGPoint

View File

@ -345,7 +345,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
} }
} }
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let currentMessage = self.message let currentMessage = self.message
let currentMedia = self.media let currentMedia = self.media
let imageLayout = self.imageNode.asyncLayout() let imageLayout = self.imageNode.asyncLayout()
@ -465,7 +465,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
} }
var statusSize = CGSize() var statusSize = CGSize()
var statusApply: ((Bool) -> Void)? var statusApply: ((ListViewItemUpdateAnimation) -> Void)?
if let dateAndStatus = dateAndStatus { if let dateAndStatus = dateAndStatus {
let statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( let statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
@ -854,9 +854,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate) strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate)
strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
} else { } else {
transition.updateFrame(node: strongSelf.pinchContainerNode, frame: imageFrame) transition.animator.updateFrame(layer: strongSelf.pinchContainerNode.layer, frame: imageFrame, completion: nil)
transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(), size: imageFrame.size)) transition.animator.updateFrame(layer: strongSelf.imageNode.layer, frame: CGRect(origin: CGPoint(), size: imageFrame.size), completion: nil)
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: transition) strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: transition.transition)
} }
} else { } else {
@ -871,11 +871,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
if strongSelf.dateAndStatusNode.supernode == nil { if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode) strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
} }
var hasAnimation = true statusApply(transition)
if transition.isAnimated {
hasAnimation = false
}
statusApply(hasAnimation)
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) 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)
@ -1501,12 +1497,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
} }
} }
static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) {
let currentAsyncLayout = node?.asyncLayout() let currentAsyncLayout = node?.asyncLayout()
return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in
var imageNode: ChatMessageInteractiveMediaNode var imageNode: ChatMessageInteractiveMediaNode
var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void)))
if let node = node, let currentAsyncLayout = currentAsyncLayout { if let node = node, let currentAsyncLayout = currentAsyncLayout {
imageNode = node imageNode = node

View File

@ -237,7 +237,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
} }
var statusSize = CGSize() var statusSize = CGSize()
var statusApply: ((Bool) -> Void)? var statusApply: ((ListViewItemUpdateAnimation) -> Void)?
if let statusType = statusType { if let statusType = statusType {
var isReplyThread = false var isReplyThread = false
@ -308,7 +308,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.imageNode.frame = imageFrame strongSelf.imageNode.frame = imageFrame
var transition: ContainedViewLayoutTransition = .immediate var transition: ContainedViewLayoutTransition = .immediate
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
transition = .animated(duration: duration, curve: .spring) transition = .animated(duration: duration, curve: .spring)
} }
@ -336,11 +336,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
if strongSelf.dateAndStatusNode.supernode == nil { if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.dateAndStatusNode) strongSelf.addSubnode(strongSelf.dateAndStatusNode)
} }
var hasAnimation = true statusApply(animation)
if case .None = animation {
hasAnimation = false
}
statusApply(hasAnimation)
strongSelf.dateAndStatusNode.frame = statusFrame.offsetBy(dx: imageFrame.minX, dy: imageFrame.minY) strongSelf.dateAndStatusNode.frame = statusFrame.offsetBy(dx: imageFrame.minX, dy: imageFrame.minY)
} else if strongSelf.dateAndStatusNode.supernode != nil { } else if strongSelf.dateAndStatusNode.supernode != nil {
strongSelf.dateAndStatusNode.removeFromSupernode() strongSelf.dateAndStatusNode.removeFromSupernode()

View File

@ -246,14 +246,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticPlayback = automaticPlayback
let imageFrame = CGRect(origin: CGPoint(x: bubbleInsets.left, y: bubbleInsets.top), size: imageSize) let imageFrame = CGRect(origin: CGPoint(x: bubbleInsets.left, y: bubbleInsets.top), size: imageSize)
var transition: ContainedViewLayoutTransition = .immediate
if case let .System(duration) = animation {
transition = .animated(duration: duration, curve: .spring)
}
transition.updateFrame(node: strongSelf.interactiveImageNode, frame: imageFrame) animation.animator.updateFrame(layer: strongSelf.interactiveImageNode.layer, frame: imageFrame, completion: nil)
imageApply(transition, synchronousLoads) imageApply(animation, synchronousLoads)
if let selection = selection { if let selection = selection {
if let selectionNode = strongSelf.selectionNode { if let selectionNode = strongSelf.selectionNode {

View File

@ -1057,7 +1057,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
statusType = nil statusType = nil
} }
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType { if let statusType = statusType {
var isReplyThread = false var isReplyThread = false
@ -1072,7 +1072,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
impressionCount: viewCount, impressionCount: viewCount,
dateText: dateText, dateText: dateText,
type: statusType, type: statusType,
layoutInput: .trailingContent(contentWidth: 100.0, preferAdditionalInset: true), layoutInput: .trailingContent(contentWidth: 100.0, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)),
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactions,

View File

@ -0,0 +1,286 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import RadialStatusNode
import AnimatedCountLabelNode
import AnimatedAvatarSetNode
import ReactionButtonListComponent
import AccountContext
final class MessageReactionButtonsNode: ASDisplayNode {
enum DisplayType {
case incoming
case outgoing
case freeform
}
private let container: ReactionButtonsLayoutContainer
var reactionSelected: ((String) -> Void)?
override init() {
self.container = ReactionButtonsLayoutContainer()
super.init()
}
func prepareUpdate(
context: AccountContext,
presentationData: ChatPresentationData,
availableReactions: AvailableReactions?,
reactions: ReactionsMessageAttribute,
constrainedWidth: CGFloat,
type: DisplayType
) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
let reactionColors: ReactionButtonComponent.Colors
switch type {
case .incoming, .freeform:
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
)
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
)
}
let reactionButtons = self.container.update(
context: context,
action: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.reactionSelected?(value)
},
reactions: reactions.reactions.map { reaction in
var iconFile: TelegramMediaFile?
if let availableReactions = availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction.value {
iconFile = availableReaction.staticIcon
break
}
}
}
return ReactionButtonsLayoutContainer.Reaction(
reaction: ReactionButtonComponent.Reaction(
value: reaction.value,
iconFile: iconFile
),
count: Int(reaction.count),
isSelected: reaction.isSelected
)
},
colors: reactionColors,
constrainedWidth: constrainedWidth,
transition: .immediate
)
var reactionButtonsSize = CGSize()
var currentRowWidth: CGFloat = 0.0
for item in reactionButtons.items {
if currentRowWidth + item.size.width > constrainedWidth {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0
}
reactionButtonsSize.height += item.size.height
currentRowWidth = 0.0
}
if !currentRowWidth.isZero {
currentRowWidth += 6.0
}
currentRowWidth += item.size.width
}
if !currentRowWidth.isZero && !reactionButtons.items.isEmpty {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0
}
reactionButtonsSize.height += reactionButtons.items[0].size.height
}
let topInset: CGFloat = 0.0
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
guard let strongSelf = self else {
return
}
var reactionButtonPosition = CGPoint(x: 0.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
}
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)
}
} else {
animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil)
}
reactionButtonPosition.x += item.size.width + 6.0
}
for view in reactionButtons.removedViews {
if animation.isAnimated {
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
view?.removeFromSuperview()
})
} else {
view.removeFromSuperview()
}
}
})
})
}
func reactionTargetView(value: String) -> UIView? {
for (_, button) in self.container.buttons {
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View {
return result.iconView
}
}
return nil
}
func animateOut() {
for (_, button) in self.container.buttons {
button.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
}
}
final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode {
private let buttonsNode: MessageReactionButtonsNode
required init() {
self.buttonsNode = MessageReactionButtonsNode()
super.init()
self.addSubnode(self.buttonsNode)
self.buttonsNode.reactionSelected = { [weak self] value in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, value)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let buttonsNode = self.buttonsNode
return { item, layoutConstants, preparePosition, _, constrainedSize in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
//let displaySeparator: Bool
let topOffset: CGFloat
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
//displaySeparator = false
topOffset = 2.0
} else {
//displaySeparator = true
topOffset = 0.0
}
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: [])
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)
return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in
var boundingSize = CGSize()
let buttonsSizeAndApply = buttonsUpdate.continueLayout(boundingWidth - (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right))
boundingSize = buttonsSizeAndApply.size
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += topOffset + 2.0
return (boundingSize, { [weak self] animation, synchronousLoad in
if let strongSelf = self {
strongSelf.item = item
animation.animator.updateFrame(layer: strongSelf.buttonsNode.layer, frame: CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: topOffset - 2.0), size: buttonsSizeAndApply.size), completion: nil)
buttonsSizeAndApply.apply(animation)
let _ = synchronousLoad
}
})
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.buttonsNode.animateOut()
}
override func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), to: CGPoint(), duration: duration, removeOnCompletion: true, additive: true)
}
override func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) {
self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.bounds.height / 2.0), duration: duration, removeOnCompletion: false, additive: true)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion()
})
self.buttonsNode.animateOut()
}
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 {
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) {
return result
}
return nil
}
override func reactionTargetView(value: String) -> UIView? {
return self.buttonsNode.reactionTargetView(value: value)
}
}

View File

@ -106,7 +106,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType { if let statusType = statusType {
var isReplyThread = false var isReplyThread = false
if case .replyThread = item.chatLocation { if case .replyThread = item.chatLocation {
@ -120,7 +120,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
impressionCount: viewCount, impressionCount: viewCount,
dateText: dateText, dateText: dateText,
type: statusType, type: statusType,
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false), layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)),
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactions,
@ -182,9 +182,9 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0)
if strongSelf.statusNode.supernode == nil { if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode) strongSelf.addSubnode(strongSelf.statusNode)
statusSizeAndApply.1(false) statusSizeAndApply.1(.None)
} else { } else {
statusSizeAndApply.1(animation.isAnimated) statusSizeAndApply.1(animation)
} }
} else if strongSelf.statusNode.supernode != nil { } else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode() strongSelf.statusNode.removeFromSupernode()

View File

@ -701,7 +701,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
if let strongSelf = self { if let strongSelf = self {
var transition: ContainedViewLayoutTransition = .immediate var transition: ContainedViewLayoutTransition = .immediate
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
transition = .animated(duration: duration, curve: .spring) transition = .animated(duration: duration, curve: .spring)
} }
@ -740,7 +740,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
dateAndStatusApply(false) dateAndStatusApply(.None)
transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: dateAndStatusFrame) transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: dateAndStatusFrame)
@ -926,7 +926,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
} }
strongSelf.addSubnode(actionButtonsNode) strongSelf.addSubnode(actionButtonsNode)
} else { } else {
if case let .System(duration) = animation { if case let .System(duration, _) = animation {
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
} }
} }

View File

@ -273,7 +273,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor)) let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor))
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType { if let statusType = statusType {
var isReplyThread = false var isReplyThread = false
if case .replyThread = item.chatLocation { if case .replyThread = item.chatLocation {
@ -287,7 +287,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
impressionCount: viewCount, impressionCount: viewCount,
dateText: dateText, dateText: dateText,
type: statusType, type: statusType,
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false), layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)),
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactions,
@ -354,7 +354,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync
let _ = textApply() let _ = textApply()
strongSelf.textNode.frame = textFrame animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil)
if let textSelectionNode = strongSelf.textSelectionNode { if let textSelectionNode = strongSelf.textSelectionNode {
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
textSelectionNode.frame = textFrame textSelectionNode.frame = textFrame
@ -367,12 +367,12 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
if let statusSizeAndApply = statusSizeAndApply { if let statusSizeAndApply = statusSizeAndApply {
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil)
if strongSelf.statusNode.supernode == nil { if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode) strongSelf.addSubnode(strongSelf.statusNode)
statusSizeAndApply.1(false) statusSizeAndApply.1(.None)
} else { } else {
statusSizeAndApply.1(animation.isAnimated) statusSizeAndApply.1(animation)
} }
} else if strongSelf.statusNode.supernode != nil { } else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode() strongSelf.statusNode.removeFromSupernode()

View File

@ -941,7 +941,7 @@ private final class ItemView: UIView, SparseItemGridView {
let messageItemNode: ListViewItemNode let messageItemNode: ListViewItemNode
if let current = self.messageItemNode { if let current = self.messageItemNode {
messageItemNode = current 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), 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)), completion: { layout, apply in
current.contentSize = layout.contentSize current.contentSize = layout.contentSize
current.insets = layout.insets current.insets = layout.insets
@ -972,7 +972,7 @@ private final class ItemView: UIView, SparseItemGridView {
func update(size: CGSize, insets: UIEdgeInsets) { func update(size: CGSize, insets: UIEdgeInsets) {
if let messageItem = self.messageItem, let messageItemNode = self.messageItemNode { 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), 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)), completion: { layout, apply in
messageItemNode.contentSize = layout.contentSize messageItemNode.contentSize = layout.contentSize
messageItemNode.insets = layout.insets messageItemNode.insets = layout.insets