mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reaction animation updates
This commit is contained in:
parent
41c7863cc9
commit
fb4e94d09a
@ -62,7 +62,6 @@ public extension ContainedViewLayoutTransitionCurve {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var viewAnimationOptions: UIView.AnimationOptions {
|
||||
switch self {
|
||||
case .linear:
|
||||
@ -77,7 +76,6 @@ public extension ContainedViewLayoutTransitionCurve {
|
||||
return []
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1578,8 +1578,24 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
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 {
|
||||
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
|
||||
if synchronous {
|
||||
f()
|
||||
@ -2017,8 +2033,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
if updateAdjacentItemsIndices.isEmpty {
|
||||
completion(state, operations)
|
||||
} else {
|
||||
let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None
|
||||
|
||||
var updatedUpdateAdjacentItemsIndices = updateAdjacentItemsIndices
|
||||
|
||||
let nodeIndex = updateAdjacentItemsIndices.first!
|
||||
@ -2031,6 +2045,20 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
if case let .Node(index, _, referenceNode) = node , index == nodeIndex {
|
||||
if let referenceNode = referenceNode {
|
||||
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
|
||||
if synchronous {
|
||||
f()
|
||||
@ -2086,7 +2114,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
let previousNodes = inputPreviousNodes
|
||||
var operations = inputOperations
|
||||
let completion = inputCompletion
|
||||
let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None
|
||||
|
||||
if state.nodes.count > 1000 {
|
||||
print("state.nodes.count > 1000")
|
||||
@ -2115,8 +2142,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
let index = insertionItemIndexAndDirection.0
|
||||
let threadId = pthread_self()
|
||||
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 {
|
||||
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)
|
||||
@ -2151,16 +2177,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
} else {
|
||||
let updateItem = updateIndicesAndItems[0]
|
||||
if let previousNode = previousNodes[updateItem.index] {
|
||||
let updateAnimation: ListViewItemUpdateAnimation
|
||||
if crossfade {
|
||||
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)
|
||||
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
|
||||
state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, isAnimated: animated, apply: apply, operations: &operations)
|
||||
|
||||
updateIndicesAndItems.remove(at: 0)
|
||||
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 {
|
||||
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)
|
||||
if node.rotated {
|
||||
if currentAnimation == nil {
|
||||
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 {
|
||||
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 {
|
||||
@ -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 {
|
||||
self.view.bringSubviewToFront(reorderNode.view)
|
||||
if let verticalScrollIndicator = self.verticalScrollIndicator {
|
||||
|
@ -7,15 +7,15 @@ public protocol Interpolatable {
|
||||
}
|
||||
|
||||
private func floorToPixels(_ value: CGFloat) -> CGFloat {
|
||||
return round(value * 10.0) / 10.0
|
||||
return value
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
@ -23,7 +23,7 @@ private func floorToPixels(_ value: CGRect) -> CGRect {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -36,6 +36,12 @@ extension CGFloat: Interpolatable {
|
||||
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 {
|
||||
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
@ -73,7 +93,7 @@ private let springAnimationIn: CABasicAnimation = {
|
||||
return animation
|
||||
}()
|
||||
|
||||
private let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in
|
||||
let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in
|
||||
if #available(iOS 9.0, *) {
|
||||
return { t in
|
||||
return springAnimationValueAt(springAnimationIn, t)
|
||||
|
@ -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
|
||||
for node in self.nodes {
|
||||
i += 1
|
||||
if node.index == itemIndex {
|
||||
switch animation {
|
||||
case .None, .Crossfade:
|
||||
if isAnimated {
|
||||
let offsetDirection: ListViewInsertionOffsetDirection
|
||||
if let direction = direction {
|
||||
offsetDirection = ListViewInsertionOffsetDirection(direction)
|
||||
@ -852,7 +851,7 @@ struct ListViewState {
|
||||
}
|
||||
|
||||
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
|
||||
case .System:
|
||||
} else {
|
||||
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import SwiftSignalKit
|
||||
|
||||
public enum ListViewItemUpdateAnimation {
|
||||
case None
|
||||
case System(duration: Double)
|
||||
case System(duration: Double, transition: ControlledTransition)
|
||||
case Crossfade
|
||||
|
||||
public var isAnimated: Bool {
|
||||
@ -14,6 +14,26 @@ public enum ListViewItemUpdateAnimation {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
public struct HeaderId: Hashable {
|
||||
public var space: AnyHashable
|
||||
@ -126,6 +136,8 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
|
||||
|
||||
private final var spring: ListViewItemSpring?
|
||||
private final var animations: [(String, ListViewAnimation)] = []
|
||||
private final var pendingControlledTransitions: [ControlledTransition] = []
|
||||
private final var controlledTransitions: [ControlledTransitionContext] = []
|
||||
|
||||
final var tempHeaderSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:]
|
||||
final var headerSpaceAffinities: [ListViewItemNode.HeaderId: Int] = [:]
|
||||
@ -394,6 +406,26 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
|
||||
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 (accessoryItemNode.animate(timestamp)) {
|
||||
continueAnimations = true
|
||||
@ -438,6 +470,29 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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)
|
||||
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 {
|
||||
strongSelf.hapticFeedback.tap()
|
||||
}
|
||||
@ -514,18 +514,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
if hideNode {
|
||||
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
|
||||
targetScaleCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
//})
|
||||
} else {
|
||||
targetScaleCompleted = true
|
||||
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) {
|
||||
@ -668,7 +668,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
}
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
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)
|
||||
itemNode.frame = expandedFrame
|
||||
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: .immediate)
|
||||
|
||||
itemNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.18)
|
||||
itemNode.layer.animateAlpha(from: 0.0, 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.04)
|
||||
|
||||
let additionalAnimationNode = AnimatedStickerNode()
|
||||
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
|
||||
@ -724,7 +732,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
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: {
|
||||
mainAnimationCompleted = true
|
||||
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)
|
||||
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 {
|
||||
strongSelf.hapticFeedback.tap()
|
||||
}
|
||||
@ -766,18 +774,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
|
||||
if hideNode {
|
||||
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
|
||||
targetScaleCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
//})
|
||||
} else {
|
||||
targetScaleCompleted = true
|
||||
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) {
|
||||
|
@ -60,8 +60,6 @@ final class ReactionNode: ASDisplayNode {
|
||||
|
||||
super.init()
|
||||
|
||||
//self.backgroundColor = UIColor(white: 0.0, alpha: 0.1)
|
||||
|
||||
self.addSubnode(self.staticImageNode)
|
||||
|
||||
self.addSubnode(self.stillAnimationNode)
|
||||
@ -120,11 +118,14 @@ final class ReactionNode: ASDisplayNode {
|
||||
animationNode.visibility = true
|
||||
|
||||
self.stillAnimationNode.alpha = 0.0
|
||||
self.stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
||||
self?.stillAnimationNode.visibility = false
|
||||
})
|
||||
|
||||
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
if transition.isAnimated {
|
||||
self.stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
||||
self?.stillAnimationNode.visibility = false
|
||||
})
|
||||
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
} else {
|
||||
self.stillAnimationNode.visibility = false
|
||||
}
|
||||
}
|
||||
|
||||
if self.validSize != size {
|
||||
@ -137,13 +138,15 @@ final class ReactionNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if !self.didSetupStillAnimation {
|
||||
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.position = animationFrame.center
|
||||
self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
|
||||
self.stillAnimationNode.updateLayout(size: animationFrame.size)
|
||||
self.stillAnimationNode.visibility = true
|
||||
if self.animationNode == nil {
|
||||
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.position = animationFrame.center
|
||||
self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
|
||||
self.stillAnimationNode.updateLayout(size: animationFrame.size)
|
||||
self.stillAnimationNode.visibility = true
|
||||
}
|
||||
} else {
|
||||
transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center)
|
||||
transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width)
|
||||
|
@ -65,7 +65,7 @@ enum AccountStateMutationOperation {
|
||||
case DeleteMessages([MessageId])
|
||||
case EditMessage(MessageId, StoreMessage)
|
||||
case UpdateMessagePoll(MediaId, Api.Poll?, Api.PollResults)
|
||||
//case UpdateMessageReactions(MessageId, Api.MessageReactions)
|
||||
case UpdateMessageReactions(MessageId, Api.MessageReactions)
|
||||
case UpdateMedia(MediaId, Media?)
|
||||
case ReadInbox(MessageId)
|
||||
case ReadOutbox(MessageId, Int32?)
|
||||
@ -258,9 +258,9 @@ struct AccountMutableState {
|
||||
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))
|
||||
}*/
|
||||
}
|
||||
|
||||
mutating func updateMedia(_ id: MediaId, media: Media?) {
|
||||
self.addOperation(.UpdateMedia(id, media))
|
||||
@ -498,7 +498,7 @@ struct AccountMutableState {
|
||||
|
||||
mutating func addOperation(_ operation: AccountStateMutationOperation) {
|
||||
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
|
||||
case let .AddMessages(messages, location):
|
||||
for message in messages {
|
||||
|
@ -5,7 +5,7 @@ import TelegramApi
|
||||
extension ReactionsMessageAttribute {
|
||||
func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute {
|
||||
switch reactions {
|
||||
case let .messageReactions(flags, results, _):
|
||||
case let .messageReactions(flags, results, recentReactions):
|
||||
let min = (flags & (1 << 0)) != 0
|
||||
var reactions = results.map { result -> MessageReaction in
|
||||
switch result {
|
||||
@ -13,6 +13,18 @@ extension ReactionsMessageAttribute {
|
||||
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 {
|
||||
var currentSelectedReaction: String?
|
||||
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 {
|
||||
var reactions = current?.reactions ?? []
|
||||
let recentPeers = current?.recentPeers ?? []
|
||||
if let value = pending.value {
|
||||
var found = false
|
||||
for i in 0 ..< reactions.count {
|
||||
@ -73,7 +86,7 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM
|
||||
}
|
||||
}
|
||||
if !reactions.isEmpty {
|
||||
return ReactionsMessageAttribute(reactions: reactions)
|
||||
return ReactionsMessageAttribute(reactions: reactions, recentPeers: recentPeers)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -87,13 +100,28 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM
|
||||
extension ReactionsMessageAttribute {
|
||||
convenience init(apiReactions: Api.MessageReactions) {
|
||||
switch apiReactions {
|
||||
case let .messageReactions(_, results, _):
|
||||
self.init(reactions: results.map { result in
|
||||
switch result {
|
||||
case let .reactionCount(flags, reaction, count):
|
||||
return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0)
|
||||
case let .messageReactions(_, results, recentReactions):
|
||||
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 {
|
||||
case let .reactionCount(flags, reaction, count):
|
||||
return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0)
|
||||
}
|
||||
},
|
||||
recentPeers: parsedRecentReactions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1473,6 +1473,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo
|
||||
return current
|
||||
}
|
||||
})
|
||||
case let .updateMessageReactions(peer, msgId, reactions):
|
||||
updatedState.updateMessageReactions(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), reactions: reactions)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -2260,7 +2262,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
|
||||
var currentAddScheduledMessages: OptimizeAddMessagesState?
|
||||
for operation in operations {
|
||||
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 {
|
||||
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
|
||||
}
|
||||
@ -3222,6 +3224,31 @@ func replayFinalState(
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -856,13 +856,16 @@ public final class AccountViewTracker {
|
||||
switch update {
|
||||
case let .updateMessageReactions(peer, msgId, reactions):
|
||||
transaction.updateMessage(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), update: { currentMessage in
|
||||
|
||||
let updatedReactions = ReactionsMessageAttribute(apiReactions: reactions)
|
||||
var updatedReactions = ReactionsMessageAttribute(apiReactions: reactions)
|
||||
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var added = false
|
||||
var attributes = currentMessage.attributes
|
||||
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
|
||||
}
|
||||
@ -870,6 +873,9 @@ public final class AccountViewTracker {
|
||||
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))
|
||||
})
|
||||
default:
|
||||
|
@ -6,6 +6,12 @@ public struct MessageReaction: Equatable, PostboxCoding {
|
||||
public var isSelected: Bool
|
||||
|
||||
public init(value: String, count: Int32, isSelected: Bool) {
|
||||
var value = value
|
||||
|
||||
if value == "❤️" {
|
||||
value = "❤"
|
||||
}
|
||||
|
||||
self.value = value
|
||||
self.count = count
|
||||
self.isSelected = isSelected
|
||||
@ -24,19 +30,53 @@ public struct MessageReaction: Equatable, PostboxCoding {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ReactionsMessageAttribute: MessageAttribute {
|
||||
public let reactions: [MessageReaction]
|
||||
public final class ReactionsMessageAttribute: Equatable, MessageAttribute {
|
||||
public struct RecentPeer: Equatable, PostboxCoding {
|
||||
public var value: String
|
||||
public var peerId: PeerId
|
||||
|
||||
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 init(reactions: [MessageReaction]) {
|
||||
public let reactions: [MessageReaction]
|
||||
public let recentPeers: [RecentPeer]
|
||||
|
||||
public init(reactions: [MessageReaction], recentPeers: [RecentPeer]) {
|
||||
self.reactions = reactions
|
||||
self.recentPeers = recentPeers
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
self.reactions = decoder.decodeObjectArrayWithDecoderForKey("r")
|
||||
self.recentPeers = decoder.decodeObjectArrayWithDecoderForKey("rp")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -945,6 +945,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
|
||||
guard let topMessage = messages.first else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = combineLatest(queue: .mainQueue(),
|
||||
contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction),
|
||||
strongSelf.context.engine.stickers.availableReactions(),
|
||||
@ -991,7 +995,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
actions.context = strongSelf.context
|
||||
|
||||
if canAddMessageReactions(message: message), let availableReactions = availableReactions {
|
||||
if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions {
|
||||
for reaction in availableReactions.reactions {
|
||||
actions.reactionItems.append(ReactionContextItem(
|
||||
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
|
||||
@ -1006,12 +1010,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.currentContextController = controller
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var updatedReaction: String? = value.reaction.rawValue
|
||||
for attribute in messages[0].attributes {
|
||||
for attribute in topMessage.attributes {
|
||||
if let attribute = attribute as? ReactionsMessageAttribute {
|
||||
for reaction in attribute.reactions {
|
||||
if reaction.value == value.reaction.rawValue {
|
||||
@ -1047,32 +1051,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
let _ = strongSelf
|
||||
let _ = itemNode
|
||||
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)
|
||||
})
|
||||
}
|
||||
}, updateMessageReaction: { [weak self] message, value in
|
||||
}, updateMessageReaction: { [weak self] initialMessage, value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(initialMessage.id) else {
|
||||
return
|
||||
}
|
||||
guard let message = messages.first else {
|
||||
return
|
||||
}
|
||||
if !canAddMessageReactions(message: message) {
|
||||
return
|
||||
}
|
||||
|
@ -1049,7 +1049,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
|
||||
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 {
|
||||
transition = .animated(duration: duration, curve: .linear)
|
||||
} else {
|
||||
@ -1122,7 +1122,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
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)
|
||||
|
||||
if needsReplyBackground {
|
||||
@ -1296,7 +1296,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
strongSelf.addSubnode(actionButtonsNode)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
@ -360,8 +360,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
var textCutout = TextNodeCutout()
|
||||
var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude
|
||||
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode)))?
|
||||
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode)))?
|
||||
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
|
||||
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)))?
|
||||
|
||||
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)?
|
||||
|
||||
@ -514,7 +514,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
} else if file.isInstantVideo {
|
||||
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 (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
|
||||
contentInstantVideoSizeAndApply = (videoLayout, apply)
|
||||
} 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
|
||||
}
|
||||
} 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()))
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))?
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
if statusInText, let textStatusType = textStatusType {
|
||||
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||
context: context,
|
||||
@ -634,7 +634,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: textStatusType,
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false),
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: nil),
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -666,14 +666,14 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
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 {
|
||||
let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0))
|
||||
finalizeContentImageLayout = finalizeImageLayout
|
||||
|
||||
boundingSize.width = max(boundingSize.width, refinedWidth)
|
||||
}
|
||||
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))?
|
||||
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))?
|
||||
if let refineContentFileLayout = refineContentFileLayout {
|
||||
let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize)
|
||||
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)
|
||||
}
|
||||
|
||||
var contentImageSizeAndApply: (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode)?
|
||||
var contentImageSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)?
|
||||
if let finalizeContentImageLayout = finalizeContentImageLayout {
|
||||
let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right)
|
||||
contentImageSizeAndApply = (size, apply)
|
||||
@ -754,7 +754,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
adjustedLineHeight += imageHeightAddition + 4.0
|
||||
}
|
||||
|
||||
var contentFileSizeAndApply: (CGSize, (Bool) -> ChatMessageInteractiveFileNode)?
|
||||
var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)?
|
||||
if let finalizeContentFileLayout = finalizeContentFileLayout {
|
||||
let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right)
|
||||
contentFileSizeAndApply = (size, apply)
|
||||
@ -788,12 +788,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
adjustedBoundingSize.height += 7.0 + size.height
|
||||
}
|
||||
|
||||
var statusSizeAndApply: ((CGSize), (Bool) -> Void)?
|
||||
var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)?
|
||||
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
||||
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right)
|
||||
}
|
||||
if let statusSizeAndApply = statusSizeAndApply {
|
||||
adjustedBoundingSize.height += statusSizeAndApply.0.height
|
||||
adjustedLineHeight += statusSizeAndApply.0.height
|
||||
}
|
||||
|
||||
/*var adjustedStatusFrame: CGRect?
|
||||
@ -815,7 +816,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
switch animation {
|
||||
case .None, .Crossfade:
|
||||
hasAnimation = false
|
||||
case let .System(duration):
|
||||
case let .System(duration, _):
|
||||
hasAnimation = true
|
||||
transition = .animated(duration: duration, curve: .easeInOut)
|
||||
}
|
||||
@ -851,7 +852,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
if let (contentImageSize, contentImageApply) = contentImageSizeAndApply {
|
||||
contentMediaHeight = contentImageSize.height
|
||||
|
||||
let contentImageNode = contentImageApply(transition, synchronousLoads)
|
||||
let contentImageNode = contentImageApply(animation, synchronousLoads)
|
||||
if strongSelf.contentImageNode !== contentImageNode {
|
||||
strongSelf.contentImageNode = contentImageNode
|
||||
contentImageNode.activatePinch = { sourceNode in
|
||||
@ -865,7 +866,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
contentImageNode.visibility = strongSelf.visibility != .none
|
||||
}
|
||||
let _ = contentImageApply(transition, synchronousLoads)
|
||||
let _ = contentImageApply(animation, synchronousLoads)
|
||||
let contentImageFrame: CGRect
|
||||
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
||||
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 {
|
||||
contentMediaHeight = contentFileSize.height
|
||||
|
||||
let contentFileNode = contentFileApply(synchronousLoads)
|
||||
let contentFileNode = contentFileApply(synchronousLoads, animation)
|
||||
if strongSelf.contentFileNode !== contentFileNode {
|
||||
strongSelf.contentFileNode = contentFileNode
|
||||
strongSelf.addSubnode(contentFileNode)
|
||||
@ -949,7 +950,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
strongSelf.addSubnode(strongSelf.statusNode)
|
||||
}
|
||||
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 {
|
||||
strongSelf.statusNode.removeFromSupernode()
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
|
@ -104,6 +104,7 @@ final class ChatMessageBubbleContentItem {
|
||||
let context: AccountContext
|
||||
let controllerInteraction: ChatControllerInteraction
|
||||
let message: Message
|
||||
let topMessage: Message
|
||||
let read: Bool
|
||||
let chatLocation: ChatLocation
|
||||
let presentationData: ChatPresentationData
|
||||
@ -112,10 +113,11 @@ final class ChatMessageBubbleContentItem {
|
||||
let isItemPinned: 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.controllerInteraction = controllerInteraction
|
||||
self.message = message
|
||||
self.topMessage = topMessage
|
||||
self.read = read
|
||||
self.chatLocation = chatLocation
|
||||
self.presentationData = presentationData
|
||||
|
@ -174,6 +174,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
}
|
||||
|
||||
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) {
|
||||
var hasDiscussion = false
|
||||
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),
|
||||
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
||||
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,
|
||||
currentItem: ChatMessageItem?,
|
||||
currentForwardInfo: (Peer?, String?)?,
|
||||
@ -1330,7 +1337,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
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?
|
||||
switch content {
|
||||
@ -1457,7 +1464,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus)
|
||||
|
||||
var calculatedGroupFramesAndSize: ([(CGRect, MosaicItemPosition)], CGSize)?
|
||||
var mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)?
|
||||
var mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?
|
||||
|
||||
if let mosaicRange = mosaicRange {
|
||||
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)],
|
||||
contentContainerNodeFrames: [(UInt32, CGRect, Bool?, CGFloat)],
|
||||
mosaicStatusOrigin: CGPoint?,
|
||||
mosaicStatusSizeAndApply: (CGSize, (Bool) -> ChatMessageDateAndStatusNode)?,
|
||||
mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?,
|
||||
needsShareButton: Bool
|
||||
) -> Void {
|
||||
guard let strongSelf = selfReference.value else {
|
||||
@ -2123,10 +2130,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
|
||||
strongSelf.updateAccessibilityData(accessibilityData)
|
||||
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
var legacyTransition: ContainedViewLayoutTransition = .immediate
|
||||
var useDisplayLinkAnimations = false
|
||||
if case let .System(duration) = animation {
|
||||
transition = .animated(duration: duration, curve: .spring)
|
||||
if case let .System(duration, _) = animation {
|
||||
legacyTransition = .animated(duration: duration, curve: .spring)
|
||||
|
||||
if let subject = item.associatedData.subject, case .forwardedMessages = subject {
|
||||
useDisplayLinkAnimations = true
|
||||
@ -2150,9 +2157,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper
|
||||
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.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)
|
||||
if isAppearing {
|
||||
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 {
|
||||
transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedFrame)
|
||||
animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedFrame, completion: nil)
|
||||
}
|
||||
} else if let deliveryFailedNode = strongSelf.deliveryFailedNode {
|
||||
strongSelf.deliveryFailedNode = nil
|
||||
transition.updateAlpha(node: deliveryFailedNode, alpha: 0.0)
|
||||
transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in
|
||||
animation.animator.updateAlpha(layer: deliveryFailedNode.layer, alpha: 0.0, completion: nil)
|
||||
animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in
|
||||
deliveryFailedNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
@ -2194,16 +2201,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.nameNode = nameNode
|
||||
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)
|
||||
nameNode.frame = nameNodeFrame
|
||||
if nameNode.supernode == nil {
|
||||
if !nameNode.isNodeLoaded {
|
||||
nameNode.isUserInteractionEnabled = false
|
||||
}
|
||||
strongSelf.clippingNode.addSubnode(nameNode)
|
||||
nameNode.frame = nameNodeFrame
|
||||
} 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 {
|
||||
@ -2232,9 +2239,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.clippingNode.addSubnode(adminBadgeNode)
|
||||
adminBadgeNode.frame = adminBadgeFrame
|
||||
} else {
|
||||
let previousAdminBadgeFrame = adminBadgeNode.frame
|
||||
adminBadgeNode.frame = adminBadgeFrame
|
||||
transition.animatePositionAdditive(node: adminBadgeNode, offset: CGPoint(x: previousAdminBadgeFrame.maxX - adminBadgeFrame.maxX, y: 0.0))
|
||||
//let previousAdminBadgeFrame = adminBadgeNode.frame
|
||||
animation.animator.updateFrame(layer: adminBadgeNode.layer, frame: adminBadgeFrame, completion: nil)
|
||||
}
|
||||
} else {
|
||||
strongSelf.adminBadgeNode?.removeFromSupernode()
|
||||
@ -2269,7 +2275,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
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))
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
if animateFrame {
|
||||
if useDisplayLinkAnimations {
|
||||
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
|
||||
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 {
|
||||
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 previousContentNodeFrame = contentNode.frame
|
||||
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
var animateFrame = false
|
||||
var animateAlpha = false
|
||||
if let addedContentNodes = addedContentNodes {
|
||||
@ -2581,8 +2587,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
})
|
||||
strongSelf.setAnimationForKey("contentNode\(contentNodeIndex)Frame", animation: animation)
|
||||
} else {
|
||||
contentNode.frame = contentNodeFrame
|
||||
contentNode.layer.animateFrame(from: previousContentNodeFrame, to: contentNodeFrame, duration: duration, timingFunction: timingFunction)
|
||||
animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil)
|
||||
}
|
||||
} else if animateAlpha {
|
||||
contentNode.frame = contentNodeFrame
|
||||
@ -2600,7 +2605,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
|
||||
if let mosaicStatusOrigin = mosaicStatusOrigin, let (size, apply) = mosaicStatusSizeAndApply {
|
||||
let mosaicStatusNode = apply(transition.isAnimated)
|
||||
let mosaicStatusNode = apply(animation)
|
||||
if mosaicStatusNode !== strongSelf.mosaicStatusNode {
|
||||
strongSelf.mosaicStatusNode?.removeFromSupernode()
|
||||
strongSelf.mosaicStatusNode = mosaicStatusNode
|
||||
@ -2627,18 +2632,53 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
||||
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 case .none = type {
|
||||
} else {
|
||||
strongSelf.clippingNode.clipsToBounds = true
|
||||
}
|
||||
}*/
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
|
||||
animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil)
|
||||
strongSelf.clippingNode.clipsToBounds = true
|
||||
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 {
|
||||
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)
|
||||
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 {
|
||||
if let _ = strongSelf.backgroundFrameTransition {
|
||||
@ -2652,14 +2692,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
|
||||
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)
|
||||
transition.updateBounds(node: strongSelf.clippingNode, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size))
|
||||
legacyTransition.updateFrame(node: strongSelf.clippingNode, frame: backgroundFrame)
|
||||
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.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition)
|
||||
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: transition)
|
||||
strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: legacyTransition)
|
||||
strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: legacyTransition)
|
||||
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: legacyTransition)
|
||||
} else {
|
||||
strongSelf.backgroundNode.frame = backgroundFrame
|
||||
strongSelf.clippingNode.frame = backgroundFrame
|
||||
@ -2702,7 +2742,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
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) {
|
||||
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
|
||||
self.backgroundNode.frame = backgroundFrame
|
||||
|
||||
@ -2819,7 +2859,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
self.clippingNode.clipsToBounds = false
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
|
@ -417,7 +417,3 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -196,7 +196,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
statusType = nil
|
||||
}
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))?
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
if let statusType = statusType {
|
||||
var isReplyThread = false
|
||||
if case .replyThread = item.chatLocation {
|
||||
@ -210,7 +210,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: 1000.0, preferAdditionalInset: true),
|
||||
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: nil),
|
||||
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
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)
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
|
||||
statusSizeAndApply.1(false)
|
||||
statusSizeAndApply.1(.None)
|
||||
} else {
|
||||
statusSizeAndApply.1(animation.isAnimated)
|
||||
statusSizeAndApply.1(animation)
|
||||
}
|
||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||
|
@ -87,8 +87,16 @@ private final class StatusReactionNode: ASDisplayNode {
|
||||
|
||||
|
||||
class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
struct ReactionSettings {
|
||||
var preferAdditionalInset: Bool
|
||||
|
||||
init(preferAdditionalInset: Bool) {
|
||||
self.preferAdditionalInset = preferAdditionalInset
|
||||
}
|
||||
}
|
||||
|
||||
enum LayoutInput {
|
||||
case trailingContent(contentWidth: CGFloat, preferAdditionalInset: Bool)
|
||||
case trailingContent(contentWidth: CGFloat, reactionSettings: ReactionSettings?)
|
||||
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)
|
||||
|
||||
var checkReadNode = self.checkReadNode
|
||||
@ -211,8 +219,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode)
|
||||
let makeReactionCountLayout = TextNode.asyncLayout(self.reactionCountNode)
|
||||
|
||||
let previousLayoutSize = self.layoutSize
|
||||
|
||||
let reactionButtonsContainer = self.reactionButtonsContainer
|
||||
|
||||
return { [weak self] arguments in
|
||||
@ -592,40 +598,56 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
constrainedWidth: arguments.constrainedSize.width,
|
||||
transition: .immediate
|
||||
)
|
||||
case let .trailingContent(contentWidth, preferAdditionalInset):
|
||||
reactionButtons = reactionButtonsContainer.update(
|
||||
context: arguments.context,
|
||||
action: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.reactionSelected?(value)
|
||||
},
|
||||
reactions: arguments.reactions.map { reaction in
|
||||
var iconFile: TelegramMediaFile?
|
||||
|
||||
if let availableReactions = arguments.availableReactions {
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if availableReaction.value == reaction.value {
|
||||
iconFile = availableReaction.staticIcon
|
||||
break
|
||||
case let .trailingContent(contentWidth, reactionSettings):
|
||||
if let _ = reactionSettings {
|
||||
reactionButtons = reactionButtonsContainer.update(
|
||||
context: arguments.context,
|
||||
action: { value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.reactionSelected?(value)
|
||||
},
|
||||
reactions: arguments.reactions.map { reaction in
|
||||
var iconFile: TelegramMediaFile?
|
||||
|
||||
if let availableReactions = arguments.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: arguments.constrainedSize.width,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
return ReactionButtonsLayoutContainer.Reaction(
|
||||
reaction: ReactionButtonComponent.Reaction(
|
||||
value: reaction.value,
|
||||
iconFile: iconFile
|
||||
),
|
||||
count: Int(reaction.count),
|
||||
isSelected: reaction.isSelected
|
||||
)
|
||||
},
|
||||
colors: reactionColors,
|
||||
constrainedWidth: arguments.constrainedSize.width,
|
||||
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 currentRowWidth: CGFloat = 0.0
|
||||
@ -664,17 +686,22 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
resultingHeight = 0.0
|
||||
}
|
||||
} else {
|
||||
if preferAdditionalInset {
|
||||
verticalReactionsInset = 5.0
|
||||
if let reactionSettings = reactionSettings {
|
||||
if reactionSettings.preferAdditionalInset {
|
||||
verticalReactionsInset = 5.0
|
||||
} else {
|
||||
verticalReactionsInset = 2.0
|
||||
}
|
||||
} else {
|
||||
verticalReactionsInset = 2.0
|
||||
verticalReactionsInset = 0.0
|
||||
}
|
||||
|
||||
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
|
||||
resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
|
||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height
|
||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height
|
||||
} else {
|
||||
resultingWidth = layoutSize.width + currentRowWidth
|
||||
resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
|
||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height
|
||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height
|
||||
}
|
||||
@ -682,7 +709,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
return (resultingWidth, { boundingWidth in
|
||||
return (CGSize(width: boundingWidth, height: resultingHeight), { animated in
|
||||
return (CGSize(width: boundingWidth, height: resultingHeight), { animation in
|
||||
if let strongSelf = self {
|
||||
let leftOffset = boundingWidth - layoutSize.width
|
||||
|
||||
@ -699,13 +726,27 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
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)
|
||||
}
|
||||
item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size)
|
||||
reactionButtonPosition.x += item.size.width + 6.0
|
||||
}
|
||||
|
||||
for view in reactionButtons.removedViews {
|
||||
view.removeFromSuperview()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
if backgroundImage != nil {
|
||||
@ -719,11 +760,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
if let backgroundNode = strongSelf.backgroundNode {
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||
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))
|
||||
animation.animator.updateFrame(layer: backgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
|
||||
}
|
||||
} else {
|
||||
if let backgroundNode = strongSelf.backgroundNode {
|
||||
@ -735,12 +772,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
if let blurredBackgroundColor = blurredBackgroundColor {
|
||||
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
|
||||
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||
if let previousLayoutSize = previousLayoutSize {
|
||||
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)
|
||||
animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
|
||||
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: animation.transition)
|
||||
} else {
|
||||
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
|
||||
strongSelf.blurredBackgroundNode = blurredBackgroundNode
|
||||
@ -771,7 +804,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
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 strongSelf.clockFrameNode == nil {
|
||||
@ -781,7 +814,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
} else if themeUpdated {
|
||||
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 {
|
||||
maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0)
|
||||
}
|
||||
@ -798,7 +831,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
} else if themeUpdated {
|
||||
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 {
|
||||
maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0)
|
||||
}
|
||||
@ -813,24 +846,26 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
checkSentNode.image = loadedCheckFullImage
|
||||
strongSelf.checkSentNode = checkSentNode
|
||||
strongSelf.addSubnode(checkSentNode)
|
||||
animateSentNode = animated
|
||||
animateSentNode = animation.isAnimated
|
||||
} else if themeUpdated {
|
||||
checkSentNode.image = loadedCheckFullImage
|
||||
}
|
||||
|
||||
if let checkSentFrame = checkSentFrame {
|
||||
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.frame = checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
|
||||
} else {
|
||||
checkSentNode.isHidden = true
|
||||
}
|
||||
|
||||
var animateReadNode = false
|
||||
if strongSelf.checkReadNode == nil {
|
||||
animateReadNode = animated
|
||||
animateReadNode = animation.isAnimated
|
||||
checkReadNode.image = loadedCheckPartialImage
|
||||
strongSelf.checkReadNode = checkReadNode
|
||||
strongSelf.addSubnode(checkReadNode)
|
||||
@ -840,10 +875,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
if let checkReadFrame = checkReadFrame {
|
||||
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.frame = checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
|
||||
} else {
|
||||
checkReadNode.isHidden = true
|
||||
}
|
||||
@ -865,13 +902,15 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
if !"".isEmpty {
|
||||
for i in 0 ..< arguments.reactions.count {
|
||||
let node: StatusReactionNode
|
||||
var animateNode = true
|
||||
if strongSelf.reactionNodes.count > i {
|
||||
node = strongSelf.reactionNodes[i]
|
||||
} else {
|
||||
animateNode = false
|
||||
node = StatusReactionNode()
|
||||
if strongSelf.reactionNodes.count > 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?.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)
|
||||
if node.supernode == nil {
|
||||
strongSelf.addSubnode(node)
|
||||
if animated {
|
||||
if animation.isAnimated {
|
||||
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
|
||||
}
|
||||
if !arguments.reactions.isEmpty {
|
||||
@ -900,10 +944,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count {
|
||||
let node = strongSelf.reactionNodes.removeLast()
|
||||
if animated {
|
||||
if let previousLayoutSize = previousLayoutSize {
|
||||
node.frame = node.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
|
||||
}
|
||||
if animation.isAnimated {
|
||||
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
node?.removeFromSupernode()
|
||||
@ -920,18 +961,16 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
strongSelf.reactionCountNode?.removeFromSupernode()
|
||||
strongSelf.addSubnode(node)
|
||||
strongSelf.reactionCountNode = node
|
||||
if animated {
|
||||
if animation.isAnimated {
|
||||
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
|
||||
} else if let reactionCountNode = strongSelf.reactionCountNode {
|
||||
strongSelf.reactionCountNode = nil
|
||||
if animated {
|
||||
if let previousLayoutSize = previousLayoutSize {
|
||||
reactionCountNode.frame = reactionCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
|
||||
}
|
||||
if animation.isAnimated {
|
||||
reactionCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountNode] _ in
|
||||
reactionCountNode?.removeFromSupernode()
|
||||
})
|
||||
@ -948,18 +987,16 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
if currentRepliesIcon.supernode == nil {
|
||||
strongSelf.repliesIcon = currentRepliesIcon
|
||||
strongSelf.addSubnode(currentRepliesIcon)
|
||||
if animated {
|
||||
if animation.isAnimated {
|
||||
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
|
||||
} else if let repliesIcon = strongSelf.repliesIcon {
|
||||
strongSelf.repliesIcon = nil
|
||||
if animated {
|
||||
if let previousLayoutSize = previousLayoutSize {
|
||||
repliesIcon.frame = repliesIcon.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
|
||||
}
|
||||
if animation.isAnimated {
|
||||
repliesIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repliesIcon] _ in
|
||||
repliesIcon?.removeFromSupernode()
|
||||
})
|
||||
@ -974,18 +1011,16 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
strongSelf.replyCountNode?.removeFromSupernode()
|
||||
strongSelf.addSubnode(node)
|
||||
strongSelf.replyCountNode = node
|
||||
if animated {
|
||||
if animation.isAnimated {
|
||||
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
|
||||
} else if let replyCountNode = strongSelf.replyCountNode {
|
||||
strongSelf.replyCountNode = nil
|
||||
if animated {
|
||||
if let previousLayoutSize = previousLayoutSize {
|
||||
replyCountNode.frame = replyCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
|
||||
}
|
||||
if animation.isAnimated {
|
||||
replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in
|
||||
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()
|
||||
return { arguments in
|
||||
let resultNode: ChatMessageDateAndStatusNode
|
||||
let resultSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))
|
||||
let resultSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))
|
||||
if let node = node, let currentLayout = currentLayout {
|
||||
resultNode = node
|
||||
resultSuggestedWidthAndContinue = currentLayout(arguments)
|
||||
@ -1014,8 +1049,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
return (resultSuggestedWidthAndContinue.0, { boundingWidth in
|
||||
let (size, apply) = resultSuggestedWidthAndContinue.1(boundingWidth)
|
||||
return (size, { animated in
|
||||
apply(animated)
|
||||
return (size, { animation in
|
||||
apply(animation)
|
||||
|
||||
return resultNode
|
||||
})
|
||||
|
@ -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 (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)
|
||||
|
||||
@ -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 {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.interactiveFileNode.frame = CGRect(origin: CGPoint(x: layoutConstants.file.bubbleInsets.left, y: layoutConstants.file.bubbleInsets.top), size: fileSize)
|
||||
|
||||
fileApply(synchronousLoads)
|
||||
fileApply(synchronousLoads, animation)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -387,7 +387,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
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)
|
||||
|
||||
@ -775,7 +775,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
}
|
||||
strongSelf.addSubnode(actionButtonsNode)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
@ -1184,7 +1184,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
||||
effectiveAvatarInset *= (1.0 - scaleProgress)
|
||||
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 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)
|
||||
|
||||
|
||||
|
||||
if let shareButtonNode = self.shareButtonNode {
|
||||
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)
|
||||
|
@ -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 titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
|
||||
@ -223,7 +223,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
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
|
||||
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])
|
||||
@ -422,7 +422,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
controlAreaWidth = progressFrame.maxX + 8.0
|
||||
}
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))?
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
if let statusType = dateAndStatusType {
|
||||
var edited = false
|
||||
if attributes.updatingMedia != nil {
|
||||
@ -430,7 +430,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
var viewCount: Int?
|
||||
var dateReplies = 0
|
||||
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? []
|
||||
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: topMessage.attributes)?.reactions ?? []
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
edited = !attribute.isHidden
|
||||
@ -455,7 +455,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
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,
|
||||
availableReactions: associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -520,7 +520,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height)
|
||||
}
|
||||
|
||||
var statusSizeAndApply: (CGSize, (Bool) -> Void)?
|
||||
var statusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> Void)?
|
||||
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
||||
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth)
|
||||
}
|
||||
@ -541,7 +541,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
streamingCacheStatusFrame = CGRect()
|
||||
}
|
||||
|
||||
return (fittedLayoutSize, { [weak self] synchronousLoads in
|
||||
return (fittedLayoutSize, { [weak self] synchronousLoads, animation in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context = context
|
||||
strongSelf.presentationData = presentationData
|
||||
@ -575,11 +575,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
statusReferenceFrame = progressFrame.offsetBy(dx: 0.0, dy: 8.0)
|
||||
}
|
||||
if let statusSizeAndApply = statusSizeAndApply {
|
||||
let statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0)
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
|
||||
strongSelf.dateAndStatusNode.frame = statusFrame
|
||||
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(false)
|
||||
statusSizeAndApply.1(animation)
|
||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||
}
|
||||
@ -1057,12 +1060,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
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()
|
||||
|
||||
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 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 {
|
||||
fileNode = node
|
||||
@ -1072,7 +1075,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
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
|
||||
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
|
||||
@ -1080,8 +1083,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return (finalWidth, { boundingWidth in
|
||||
let (finalSize, apply) = finalLayout(boundingWidth)
|
||||
|
||||
return (finalSize, { synchronousLoads in
|
||||
apply(synchronousLoads)
|
||||
return (finalSize, { synchronousLoads, animation in
|
||||
apply(synchronousLoads, animation)
|
||||
return fileNode
|
||||
})
|
||||
})
|
||||
|
@ -362,7 +362,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
dateAndStatusApply(false)
|
||||
dateAndStatusApply(.None)
|
||||
switch layoutData {
|
||||
case let .unconstrained(width):
|
||||
let dateAndStatusOrigin: CGPoint
|
||||
|
@ -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 currentMedia = self.media
|
||||
let imageLayout = self.imageNode.asyncLayout()
|
||||
@ -465,7 +465,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
}
|
||||
|
||||
var statusSize = CGSize()
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
var statusApply: ((ListViewItemUpdateAnimation) -> Void)?
|
||||
|
||||
if let dateAndStatus = dateAndStatus {
|
||||
let statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||
@ -854,9 +854,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate)
|
||||
strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
|
||||
} else {
|
||||
transition.updateFrame(node: strongSelf.pinchContainerNode, frame: imageFrame)
|
||||
transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(), size: imageFrame.size))
|
||||
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: transition)
|
||||
transition.animator.updateFrame(layer: strongSelf.pinchContainerNode.layer, frame: imageFrame, completion: nil)
|
||||
transition.animator.updateFrame(layer: strongSelf.imageNode.layer, frame: CGRect(origin: CGPoint(), size: imageFrame.size), completion: nil)
|
||||
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: transition.transition)
|
||||
|
||||
}
|
||||
} else {
|
||||
@ -871,11 +871,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
|
||||
}
|
||||
var hasAnimation = true
|
||||
if transition.isAnimated {
|
||||
hasAnimation = false
|
||||
}
|
||||
statusApply(hasAnimation)
|
||||
statusApply(transition)
|
||||
|
||||
let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
|
||||
|
||||
@ -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()
|
||||
|
||||
return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in
|
||||
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 {
|
||||
imageNode = node
|
||||
|
@ -237,7 +237,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
var statusSize = CGSize()
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
var statusApply: ((ListViewItemUpdateAnimation) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
var isReplyThread = false
|
||||
@ -308,7 +308,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.imageNode.frame = imageFrame
|
||||
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
transition = .animated(duration: duration, curve: .spring)
|
||||
}
|
||||
|
||||
@ -336,11 +336,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
|
||||
}
|
||||
var hasAnimation = true
|
||||
if case .None = animation {
|
||||
hasAnimation = false
|
||||
}
|
||||
statusApply(hasAnimation)
|
||||
statusApply(animation)
|
||||
strongSelf.dateAndStatusNode.frame = statusFrame.offsetBy(dx: imageFrame.minX, dy: imageFrame.minY)
|
||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||
|
@ -246,14 +246,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.automaticPlayback = automaticPlayback
|
||||
|
||||
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 selectionNode = strongSelf.selectionNode {
|
||||
|
@ -1057,7 +1057,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
statusType = nil
|
||||
}
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))?
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
|
||||
if let statusType = statusType {
|
||||
var isReplyThread = false
|
||||
@ -1072,7 +1072,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: 100.0, preferAdditionalInset: true),
|
||||
layoutInput: .trailingContent(contentWidth: 100.0, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)),
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -106,7 +106,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
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)
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))?
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
if let statusType = statusType {
|
||||
var isReplyThread = false
|
||||
if case .replyThread = item.chatLocation {
|
||||
@ -120,7 +120,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false),
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)),
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -182,9 +182,9 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0)
|
||||
if strongSelf.statusNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.statusNode)
|
||||
statusSizeAndApply.1(false)
|
||||
statusSizeAndApply.1(.None)
|
||||
} else {
|
||||
statusSizeAndApply.1(animation.isAnimated)
|
||||
statusSizeAndApply.1(animation)
|
||||
}
|
||||
} else if strongSelf.statusNode.supernode != nil {
|
||||
strongSelf.statusNode.removeFromSupernode()
|
||||
|
@ -701,7 +701,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
|
||||
if let strongSelf = self {
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
transition = .animated(duration: duration, curve: .spring)
|
||||
}
|
||||
|
||||
@ -740,7 +740,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
||||
|
||||
dateAndStatusApply(false)
|
||||
dateAndStatusApply(.None)
|
||||
|
||||
transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: dateAndStatusFrame)
|
||||
|
||||
@ -926,7 +926,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
strongSelf.addSubnode(actionButtonsNode)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
if case let .System(duration, _) = animation {
|
||||
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))?
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
if let statusType = statusType {
|
||||
var isReplyThread = false
|
||||
if case .replyThread = item.chatLocation {
|
||||
@ -287,7 +287,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false),
|
||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)),
|
||||
constrainedSize: textConstrainedSize,
|
||||
availableReactions: item.associatedData.availableReactions,
|
||||
reactions: dateReactions,
|
||||
@ -354,7 +354,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync
|
||||
let _ = textApply()
|
||||
|
||||
strongSelf.textNode.frame = textFrame
|
||||
animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil)
|
||||
if let textSelectionNode = strongSelf.textSelectionNode {
|
||||
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
|
||||
textSelectionNode.frame = textFrame
|
||||
@ -367,12 +367,12 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
|
||||
|
||||
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 {
|
||||
strongSelf.addSubnode(strongSelf.statusNode)
|
||||
statusSizeAndApply.1(false)
|
||||
statusSizeAndApply.1(.None)
|
||||
} else {
|
||||
statusSizeAndApply.1(animation.isAnimated)
|
||||
statusSizeAndApply.1(animation)
|
||||
}
|
||||
} else if strongSelf.statusNode.supernode != nil {
|
||||
strongSelf.statusNode.removeFromSupernode()
|
||||
|
@ -941,7 +941,7 @@ private final class ItemView: UIView, SparseItemGridView {
|
||||
let messageItemNode: ListViewItemNode
|
||||
if let current = self.messageItemNode {
|
||||
messageItemNode = current
|
||||
messageItem.updateNode(async: { f in f() }, node: { return current }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2), 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.insets = layout.insets
|
||||
|
||||
@ -972,7 +972,7 @@ private final class ItemView: UIView, SparseItemGridView {
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets) {
|
||||
if let messageItem = self.messageItem, let messageItemNode = self.messageItemNode {
|
||||
messageItem.updateNode(async: { f in f() }, node: { return messageItemNode }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2), 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.insets = layout.insets
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user