Reaction animation updates

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

View File

@ -62,7 +62,6 @@ public extension ContainedViewLayoutTransitionCurve {
}
}
#if os(iOS)
var viewAnimationOptions: UIView.AnimationOptions {
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)
}
}
}
}

View File

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

View File

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

View File

@ -807,13 +807,12 @@ struct ListViewState {
}
}
mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, animation: ListViewItemUpdateAnimation, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) {
mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, isAnimated: Bool, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) {
var i = -1
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))
}

View File

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

View File

@ -83,6 +83,16 @@ public struct ListViewItemLayoutParams {
}
}
private final class ControlledTransitionContext {
let transition: ControlledTransition
let beginAt: Double
init(transition: ControlledTransition, beginAt: Double) {
self.transition = transition
self.beginAt = beginAt
}
}
open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
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) {

View File

@ -503,7 +503,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false)
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -93,6 +93,11 @@ class ChatMessageBackground: ASDisplayNode {
transition.updateFrame(node: self.outlineImageNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0))
}
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -108,7 +108,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
let (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)
}
})
})

View File

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

View File

@ -213,7 +213,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) {
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) {
let currentFile = self.file
let 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
})
})

View File

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

View File

@ -345,7 +345,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
}
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) {
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let currentMessage = self.message
let 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

View File

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

View File

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

View File

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

View File

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

View File

@ -106,7 +106,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
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()

View File

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

View File

@ -273,7 +273,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor))
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()

View File

@ -941,7 +941,7 @@ private final class ItemView: UIView, SparseItemGridView {
let messageItemNode: ListViewItemNode
if let current = self.messageItemNode {
messageItemNode = current
messageItem.updateNode(async: { f in f() }, node: { return current }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2), 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