diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 9fd87574bf..f77887a1b4 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -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(layer: CALayer, keyPath: ReferenceWritableKeyPath, 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) + } + } + } +} diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index c4ee47b208..7385354ba3 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -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?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimation: ListViewItemUpdateAnimation, completion: @escaping (QueueLocalObject, ListViewItemNodeLayout, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimationIsAnimated: Bool, updateAnimationIsCrossfade: Bool, completion: @escaping (QueueLocalObject, ListViewItemNodeLayout, @escaping () -> (Signal?, (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 { diff --git a/submodules/Display/Source/ListViewAnimation.swift b/submodules/Display/Source/ListViewAnimation.swift index 482f466d3d..98ffde5ef7 100644 --- a/submodules/Display/Source/ListViewAnimation.swift +++ b/submodules/Display/Source/ListViewAnimation.swift @@ -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) diff --git a/submodules/Display/Source/ListViewIntermediateState.swift b/submodules/Display/Source/ListViewIntermediateState.swift index 096f485fc3..0edec15664 100644 --- a/submodules/Display/Source/ListViewIntermediateState.swift +++ b/submodules/Display/Source/ListViewIntermediateState.swift @@ -807,13 +807,12 @@ struct ListViewState { } } - mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, animation: ListViewItemUpdateAnimation, apply: @escaping () -> (Signal?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) { + mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, isAnimated: Bool, apply: @escaping () -> (Signal?, (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)) } diff --git a/submodules/Display/Source/ListViewItem.swift b/submodules/Display/Source/ListViewItem.swift index 85713785ee..d3c13fbb52 100644 --- a/submodules/Display/Source/ListViewItem.swift +++ b/submodules/Display/Source/ListViewItem.swift @@ -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 { diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index d23236a0ab..9bc586ca2c 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -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) { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 5536c3e478..60e6bb5404 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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) { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index fdb54662f2..14335c656a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 99c1264e63..478dd24bf5 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index 2a4b008e46..d7a3d850fa 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -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 + ) } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 4eaf554904..9893bb20e7 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -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)) + }) } } diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index f903311bfa..3d44d900cd 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -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: diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index e281a8614f..f575e7adb0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 959555bfd0..06a317b92c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index b635febb2d..b775f281bc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index b275905129..082fd485dd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -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() } diff --git a/submodules/TelegramUI/Sources/ChatMessageBackground.swift b/submodules/TelegramUI/Sources/ChatMessageBackground.swift index da4f57d14e..4b5eee990c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBackground.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBackground.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift index b2f6eb3f9f..268b015150 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 5ed47d2e3a..9ddd088070 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -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) { diff --git a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift index d14942f98c..25d7ffcf18 100644 --- a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift @@ -417,7 +417,3 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { return nil } } - - - - diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 620888a171..3dd03120d7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index e6cdb1e5ab..7af7c35754 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -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 }) diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 30ba4badc4..9920350a64 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -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) } }) }) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 0f676c9eca..468bcec162 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index c6a638689d..484d0b4306 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -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 }) }) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index b0066c044e..71a361faf4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -362,7 +362,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - dateAndStatusApply(false) + dateAndStatusApply(.None) switch layoutData { case let .unconstrained(width): let dateAndStatusOrigin: CGPoint diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 1c965bfcef..cd07dec56c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 16c8909b93..8fa79f911c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 94b5662f31..98cf345e1d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index adf5291082..03b9875778 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift new file mode 100644 index 0000000000..0a5c42f90a --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 804d67eb01..7975bb5cf0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 6413b9b4f3..1a026b098f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index f08fc28726..7d0d81efd2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 0e5811b8b8..4166f86f60 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -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