From 41c7863cc9a9be2da7332f7ddaade2dee1cc45c6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 3 Dec 2021 22:19:45 +0400 Subject: [PATCH 1/8] Fix call peer id --- submodules/TelegramCallsUI/Sources/PresentationCall.swift | 2 +- submodules/TelegramUI/Sources/AppDelegate.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 56cfa62c67..74f2c80a7c 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -613,7 +613,7 @@ public final class PresentationCallImpl: PresentationCall { self.callKitIntegration?.reportIncomingCall( uuid: self.internalId, stableId: stableId, - handle: "\(self.peerId.id)", + handle: "\(self.peerId.id._internalGetInt64Value())", isVideo: sessionState.type == .video, displayTitle: self.peer?.debugDisplayTitle ?? "Unknown", completion: { [weak self] error in diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 1d31cf59a8..58ce4b2f49 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1544,7 +1544,7 @@ private func extractAccountManagerState(records: AccountRecordsView Date: Fri, 3 Dec 2021 22:19:53 +0400 Subject: [PATCH 2/8] Reaction animation updates --- .../ContainedViewLayoutTransition.swift | 401 +++++++++++++++++- submodules/Display/Source/ListView.swift | 68 ++- .../Display/Source/ListViewAnimation.swift | 30 +- .../Source/ListViewIntermediateState.swift | 7 +- submodules/Display/Source/ListViewItem.swift | 22 +- .../Display/Source/ListViewItemNode.swift | 55 +++ .../Sources/ReactionContextNode.swift | 32 +- .../Sources/ReactionSelectionNode.swift | 31 +- .../Account/AccountIntermediateState.swift | 8 +- .../ApiUtils/ReactionsMessageAttribute.swift | 46 +- .../State/AccountStateManagementUtils.swift | 29 +- .../Sources/State/AccountViewTracker.swift | 10 +- .../SyncCore_ReactionsMessageAttribute.swift | 46 +- .../TelegramUI/Sources/ChatController.swift | 45 +- .../ChatMessageAnimatedStickerItemNode.swift | 6 +- .../ChatMessageAttachedContentNode.swift | 33 +- .../Sources/ChatMessageBackground.swift | 5 + .../ChatMessageBubbleContentNode.swift | 4 +- .../Sources/ChatMessageBubbleItemNode.swift | 112 +++-- .../ChatMessageCommentFooterContentNode.swift | 4 - .../ChatMessageContactBubbleContentNode.swift | 8 +- .../ChatMessageDateAndStatusNode.swift | 219 ++++++---- .../ChatMessageFileBubbleContentNode.swift | 6 +- .../ChatMessageInstantVideoItemNode.swift | 8 +- .../ChatMessageInteractiveFileNode.swift | 35 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- .../ChatMessageInteractiveMediaNode.swift | 20 +- .../ChatMessageMapBubbleContentNode.swift | 10 +- .../ChatMessageMediaBubbleContentNode.swift | 8 +- .../ChatMessagePollBubbleContentNode.swift | 4 +- ...hatMessageReactionsFooterContentNode.swift | 286 +++++++++++++ ...atMessageRestrictedBubbleContentNode.swift | 8 +- .../Sources/ChatMessageStickerItemNode.swift | 6 +- .../ChatMessageTextBubbleContentNode.swift | 12 +- .../Panes/PeerInfoVisualMediaPaneNode.swift | 4 +- 35 files changed, 1299 insertions(+), 331 deletions(-) create mode 100644 submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift 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 From 38a7fc80a7981fe11df6f5cc5a4c9d9ed482a468 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 3 Dec 2021 22:25:56 +0400 Subject: [PATCH 3/8] Fix build --- .../TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 1802dd006f..344792bb3b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -285,7 +285,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { spoilerTextLayoutAndApply = nil } - var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))? + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? if let statusType = statusType { var isReplyThread = false if case .replyThread = item.chatLocation { From 97153be27353b86a27a61d4ff57b1331db910724 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 4 Dec 2021 14:37:40 +0400 Subject: [PATCH 4/8] Filter repeated animations --- .../ContainedViewLayoutTransition.swift | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index f77887a1b4..4d5b64f7bc 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1669,7 +1669,7 @@ public final class ControlledTransition { } } - for i in removeAnimationIndices.reversed() { + for i in Set(removeAnimationIndices).sorted().reversed() { self.animations.remove(at: i).complete(atEnd: false) } } @@ -1701,8 +1701,19 @@ public final class ControlledTransition { self.animations.removeAll() } + private func add(animation: ControlledTransitionProperty) { + for i in 0 ..< self.animations.count { + let otherAnimation = self.animations[i] + if otherAnimation.layer === animation.layer && otherAnimation.keyPath == animation.keyPath { + self.animations.remove(at: i) + break + } + } + self.animations.append(animation) + } + public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) { - self.animations.append(ControlledTransitionProperty( + self.add(animation: ControlledTransitionProperty( layer: layer, keyPath: \.opacity, fromValue: layer.opacity, @@ -1712,7 +1723,7 @@ public final class ControlledTransition { } public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) { - self.animations.append(ControlledTransitionProperty( + self.add(animation: ControlledTransitionProperty( layer: layer, keyPath: \.position, fromValue: layer.position, @@ -1722,7 +1733,7 @@ public final class ControlledTransition { } public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) { - self.animations.append(ControlledTransitionProperty( + self.add(animation: ControlledTransitionProperty( layer: layer, keyPath: \.bounds, fromValue: layer.bounds, @@ -1732,7 +1743,7 @@ public final class ControlledTransition { } public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) { - self.animations.append(ControlledTransitionProperty( + self.add(animation: ControlledTransitionProperty( layer: layer, keyPath: \.frame, fromValue: layer.frame, From 56f5a58ede95ef05345307647340fabd6c81a492 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 5 Dec 2021 11:28:52 +0400 Subject: [PATCH 5/8] Slightly improve editing animation --- .../ChatMessageDateAndStatusNode.swift | 33 ++++++++++++------- .../ChatMessageTextBubbleContentNode.swift | 1 - 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 7af7c35754..65075fd2d4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -786,11 +786,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { blurredBackgroundNode.removeFromSupernode() } - strongSelf.dateNode.displaysAsynchronously = !arguments.presentationData.isPreview let _ = dateApply() if let currentImpressionIcon = currentImpressionIcon { - currentImpressionIcon.displaysAsynchronously = !arguments.presentationData.isPreview + currentImpressionIcon.displaysAsynchronously = false if currentImpressionIcon.image !== impressionImage { currentImpressionIcon.image = impressionImage } @@ -807,14 +806,19 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { 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 { + let clockPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset) if strongSelf.clockFrameNode == nil { strongSelf.clockFrameNode = clockFrameNode clockFrameNode.image = clockFrameImage strongSelf.addSubnode(clockFrameNode) - } else if themeUpdated { - clockFrameNode.image = clockFrameImage + + clockFrameNode.position = clockPosition + } else { + if themeUpdated { + clockFrameNode.image = clockFrameImage + } + animation.animator.updatePosition(layer: clockFrameNode.layer, position: clockPosition, completion: nil) } - 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) } @@ -824,14 +828,19 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if let clockMinNode = clockMinNode { + let clockMinPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset) if strongSelf.clockMinNode == nil { strongSelf.clockMinNode = clockMinNode clockMinNode.image = clockMinImage strongSelf.addSubnode(clockMinNode) - } else if themeUpdated { - clockMinNode.image = clockMinImage + + clockMinNode.position = clockMinPosition + } else { + if themeUpdated { + clockMinNode.image = clockMinImage + } + animation.animator.updatePosition(layer: clockMinNode.layer, position: clockMinPosition, completion: nil) } - 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) } @@ -852,13 +861,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if let checkSentFrame = checkSentFrame { + let actualCheckSentFrame = checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset) + if checkSentNode.isHidden { 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 + animation.animator.updateFrame(layer: checkSentNode.layer, frame: actualCheckSentFrame, completion: nil) } else { checkSentNode.isHidden = true } @@ -980,7 +989,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } if let currentRepliesIcon = currentRepliesIcon { - currentRepliesIcon.displaysAsynchronously = !arguments.presentationData.isPreview + currentRepliesIcon.displaysAsynchronously = false if currentRepliesIcon.image !== repliesImage { currentRepliesIcon.image = repliesImage } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 344792bb3b..ede3a8e43d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -363,7 +363,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync let _ = textApply() animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil) From 2397f3c5b176a38a94d194d92d3cdc2bcfaaa7f3 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Dec 2021 16:22:43 +0400 Subject: [PATCH 6/8] Temp --- .../ContainedViewLayoutTransition.swift | 96 +++++++++++++++++-- .../ChatMessageTextBubbleContentNode.swift | 1 + 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 4d5b64f7bc..cb4294c0e2 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1443,6 +1443,7 @@ extension CGFloat: AnyValueProviding { var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, + nsValue: self as NSNumber, stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGFloat { @@ -1471,6 +1472,7 @@ extension Float: AnyValueProviding { var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, + nsValue: self as NSNumber, stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? Float { @@ -1497,6 +1499,7 @@ extension CGPoint: AnyValueProviding { var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, + nsValue: NSValue(cgPoint: self), stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGPoint { @@ -1523,6 +1526,7 @@ extension CGSize: AnyValueProviding { var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, + nsValue: NSValue(cgSize: self), stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGSize { @@ -1549,6 +1553,7 @@ extension CGRect: AnyValueProviding { var anyValue: ControlledTransitionProperty.AnyValue { return ControlledTransitionProperty.AnyValue( value: self, + nsValue: NSValue(cgRect: self), stringValue: { "\(self)" }, isEqual: { other in if let otherValue = other.value as? CGRect { @@ -1570,17 +1575,20 @@ extension CGRect: AnyValueProviding { final class ControlledTransitionProperty { final class AnyValue: Equatable, CustomStringConvertible { let value: Any + let nsValue: NSValue let stringValue: () -> String let isEqual: (AnyValue) -> Bool let interpolate: (AnyValue, CGFloat) -> AnyValue init( value: Any, + nsValue: NSValue, stringValue: @escaping () -> String, isEqual: @escaping (AnyValue) -> Bool, interpolate: @escaping (AnyValue, CGFloat) -> AnyValue ) { self.value = value + self.nsValue = nsValue self.stringValue = stringValue self.isEqual = isEqual self.interpolate = interpolate @@ -1600,32 +1608,82 @@ final class ControlledTransitionProperty { } let layer: CALayer + let path: String 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 { + private var animator: AnyObject? + + init(layer: CALayer, path: String, keyPath: ReferenceWritableKeyPath, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding { self.layer = layer + self.path = path 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 + + if #available(iOS 10.0, *) { + layer[keyPath: keyPath] = fromValue + let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: { + layer[keyPath: keyPath] = toValue + }) + self.animator = animator + animator.pauseAnimation() + layer[keyPath: keyPath] = toValue + } + + self.update(at: 0.0) + } + + deinit { + if #available(iOS 10.0, *) { + if let animator = self.animator as? UIViewPropertyAnimator { + animator.stopAnimation(true) + self.animator = nil + } + } } func update(at fraction: CGFloat) { let value = self.fromValue.interpolate(toValue, fraction) self.lastValue = value - self.write(self.layer, value) + //self.write(self.layer, value) + + if #available(iOS 10.0, *) { + if let animator = self.animator as? UIViewPropertyAnimator { + animator.fractionComplete = fraction + } + } + + /*let animation = CABasicAnimation() + animation.speed = 0.0 + animation.duration = 1.0 + animation.fillMode = .both + animation.fromValue = value.nsValue + animation.toValue = self.toValue.nsValue + animation.keyPath = self.path + if let previousAnimation = self.layer.animation(forKey: self.animationKey) { + self.layer.removeAnimation(forKey: self.animationKey) + let _ = previousAnimation + } + self.layer.add(animation, forKey: self.animationKey)*/ } func complete(atEnd: Bool) { + if #available(iOS 10.0, *) { + if let animator = self.animator as? UIViewPropertyAnimator { + animator.stopAnimation(true) + /*if atEnd { + animator.finishAnimation(at: .current) + }*/ + self.animator = nil + } + } + self.completion?(atEnd) } } @@ -1713,40 +1771,58 @@ public final class ControlledTransition { } public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) { + let fromValue = layer.presentation()?.opacity ?? layer.opacity + //layer.opacity = Float(alpha) self.add(animation: ControlledTransitionProperty( layer: layer, + path: "opacity", keyPath: \.opacity, - fromValue: layer.opacity, + fromValue: fromValue, toValue: Float(alpha), completion: completion )) } public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) { + let fromValue = layer.presentation()?.position ?? layer.position + //layer.position = position self.add(animation: ControlledTransitionProperty( layer: layer, + path: "position", keyPath: \.position, - fromValue: layer.position, + fromValue: fromValue, toValue: position, completion: completion )) } public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) { + let fromValue = layer.presentation()?.bounds ?? layer.bounds + //layer.bounds = bounds self.add(animation: ControlledTransitionProperty( layer: layer, + path: "bounds", keyPath: \.bounds, - fromValue: layer.bounds, + fromValue: fromValue, toValue: bounds, completion: completion )) } public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) { + if layer.frame == frame { + return + } + let fromValue = layer.presentation()?.frame ?? layer.frame + if let presentation = layer.presentation(), presentation.frame != layer.frame { + assert(true) + } + //layer.frame = frame self.add(animation: ControlledTransitionProperty( layer: layer, + path: "frame", keyPath: \.frame, - fromValue: layer.frame, + fromValue: fromValue, toValue: frame, completion: completion )) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index ede3a8e43d..a320abae7e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -365,6 +365,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let _ = textApply() animation.animator.updateFrame(layer: strongSelf.textNode.layer, frame: textFrame, completion: nil) + //strongSelf.textNode.frame = textFrame if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { let spoilerTextNode = spoilerTextApply() From 764257a99ea0ff0cd105ca478b7e2433d524bd6f Mon Sep 17 00:00:00 2001 From: overtake Date: Tue, 7 Dec 2021 18:46:11 +0400 Subject: [PATCH 7/8] fix crash --- .../Sources/State/SynchronizeEmojiKeywordsOperation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift index a8a57dd5af..a99610cfc9 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift @@ -5,7 +5,7 @@ import MurMurHash32 func addSynchronizeEmojiKeywordsOperation(transaction: Transaction, inputLanguageCode: String, languageCode: String?, fromVersion: Int32?) { let tag = OperationLogTags.SynchronizeEmojiKeywords - let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(1), id: PeerId.Id._internalFromInt64Value(Int64(murMurHashString32(inputLanguageCode)))) + let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(1), id: PeerId.Id._internalFromInt64Value(Int64(abs(murMurHashString32(inputLanguageCode))))) var hasExistingOperation = false transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag) { entry -> Bool in From c58b8c33c42b9d4b670e8f167be9d8759479b8aa Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 7 Dec 2021 23:43:57 +0400 Subject: [PATCH 8/8] Reaction improvements --- .../Sources/AnimatedAvatarSetNode.swift | 169 +++++++++++ .../Sources/ReactionButtonListComponent.swift | 92 ++++-- .../ContextUI/Sources/ContextController.swift | 25 ++ .../ContainedViewLayoutTransition.swift | 187 ++++++++----- submodules/Display/Source/ListView.swift | 4 +- submodules/Display/Source/NavigationBar.swift | 21 ++ ...pLongTapOrDoubleTapGestureRecognizer.swift | 2 +- .../Sources/ReactionContextNode.swift | 262 +++++++++++------- .../SynchronizeEmojiKeywordsOperation.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 147 ++++++---- .../Sources/ChatControllerInteraction.swift | 9 +- .../ChatMessageActionButtonsNode.swift | 51 ++-- .../ChatMessageAnimatedStickerItemNode.swift | 103 +++++-- .../ChatMessageAttachedContentNode.swift | 46 +-- ...eBubbleContentCalclulateImageCorners.swift | 6 + .../ChatMessageBubbleContentNode.swift | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 187 +++++++++---- .../ChatMessageCommentFooterContentNode.swift | 7 +- .../ChatMessageContactBubbleContentNode.swift | 2 +- .../ChatMessageDateAndStatusNode.swift | 231 +++++++++------ .../ChatMessageFileBubbleContentNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 87 +++++- .../ChatMessageInteractiveFileNode.swift | 13 +- ...atMessageInteractiveInstantVideoNode.swift | 32 +-- .../ChatMessageInteractiveMediaNode.swift | 73 +++-- .../ChatMessageMapBubbleContentNode.swift | 2 +- .../ChatMessageMediaBubbleContentNode.swift | 7 + .../ChatMessagePollBubbleContentNode.swift | 2 +- ...hatMessageReactionsFooterContentNode.swift | 186 +++++++++++-- ...atMessageRestrictedBubbleContentNode.swift | 2 +- .../Sources/ChatMessageStickerItemNode.swift | 102 +++++-- .../ChatMessageTextBubbleContentNode.swift | 7 +- .../ChatMessageWebpageBubbleContentNode.swift | 5 +- .../Panes/PeerInfoVisualMediaPaneNode.swift | 4 +- 34 files changed, 1510 insertions(+), 568 deletions(-) diff --git a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift index a4d269394a..bf82d48648 100644 --- a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift +++ b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift @@ -294,3 +294,172 @@ public final class AnimatedAvatarSetNode: ASDisplayNode { } } } + +public final class AnimatedAvatarSetView: UIView { + private final class ContentView: UIView { + private let unclippedView: UIImageView + private let clippedView: UIImageView + + private var size: CGSize + private var spacing: CGFloat + + private var disposable: Disposable? + + init(context: AccountContext, peer: EnginePeer?, placeholderColor: UIColor, synchronousLoad: Bool, size: CGSize, spacing: CGFloat) { + self.size = size + self.spacing = spacing + + self.unclippedView = UIImageView() + self.clippedView = UIImageView() + + super.init() + + self.addSubview(self.unclippedView) + self.addSubview(self.clippedView) + + if let peer = peer { + if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) { + let image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.lightGray.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })! + self.updateImage(image: image, size: size, spacing: spacing) + + let disposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] imageVersions in + guard let strongSelf = self else { + return + } + let image = imageVersions?.0 + if let image = image { + strongSelf.updateImage(image: image, size: size, spacing: spacing) + } + }) + self.disposable = disposable + } else { + let image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id) + })! + self.updateImage(image: image, size: size, spacing: spacing) + } + } else { + let image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(placeholderColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })! + self.updateImage(image: image, size: size, spacing: spacing) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateImage(image: UIImage, size: CGSize, spacing: CGFloat) { + self.unclippedView.image = image + self.clippedView.image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: spacing - size.width, dy: 0.0)) + }) + } + + deinit { + self.disposable?.dispose() + } + + func updateLayout(size: CGSize, isClipped: Bool, animated: Bool) { + self.unclippedView.frame = CGRect(origin: CGPoint(), size: size) + self.clippedView.frame = CGRect(origin: CGPoint(), size: size) + + if animated && self.unclippedView.alpha.isZero != self.clippedView.alpha.isZero { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(layer: self.unclippedView.layer, alpha: isClipped ? 0.0 : 1.0) + transition.updateAlpha(layer: self.clippedView.layer, alpha: isClipped ? 1.0 : 0.0) + } else { + self.unclippedView.alpha = isClipped ? 0.0 : 1.0 + self.clippedView.alpha = isClipped ? 1.0 : 0.0 + } + } + } + + private var contentViews: [AnimatedAvatarSetContext.Content.Item.Key: ContentView] = [:] + + public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), customSpacing: CGFloat? = nil, animated: Bool, synchronousLoad: Bool) -> CGSize { + var contentWidth: CGFloat = 0.0 + let contentHeight: CGFloat = itemSize.height + + let spacing: CGFloat + if let customSpacing = customSpacing { + spacing = customSpacing + } else { + spacing = 10.0 + } + + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.2, curve: .easeInOut) + } else { + transition = .immediate + } + + var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = [] + var index = 0 + for i in 0 ..< content.items.count { + let (key, item) = content.items[i] + + validKeys.append(key) + + let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize) + + let itemView: ContentView + if let current = self.contentViews[key] { + itemView = current + itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: animated) + transition.updateFrame(layer: itemView.layer, frame: itemFrame) + } else { + itemView = ContentView(context: context, peer: item.peer, placeholderColor: item.placeholderColor, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing) + self.addSubview(itemView) + self.contentViews[key] = itemView + itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: false) + itemView.frame = itemFrame + if animated { + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + itemView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + } + } + itemView.layer.zPosition = CGFloat(100 - i) + contentWidth += itemSize.width - spacing + index += 1 + } + var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = [] + for key in self.contentViews.keys { + if !validKeys.contains(key) { + removeKeys.append(key) + } + } + for key in removeKeys { + guard let itemView = self.contentViews.removeValue(forKey: key) else { + continue + } + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemView] _ in + itemView?.removeFromSuperview() + }) + itemView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } + + return CGSize(width: contentWidth, height: contentHeight) + } +} diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 3998261793..e503c80d2b 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -39,18 +39,21 @@ public final class ReactionButtonComponent: Component { } public struct Colors: Equatable { - public var background: UInt32 - public var foreground: UInt32 - public var stroke: UInt32 + public var deselectedBackground: UInt32 + public var selectedBackground: UInt32 + public var deselectedForeground: UInt32 + public var selectedForeground: UInt32 public init( - background: UInt32, - foreground: UInt32, - stroke: UInt32 + deselectedBackground: UInt32, + selectedBackground: UInt32, + deselectedForeground: UInt32, + selectedForeground: UInt32 ) { - self.background = background - self.foreground = foreground - self.stroke = stroke + self.deselectedBackground = deselectedBackground + self.selectedBackground = selectedBackground + self.deselectedForeground = deselectedForeground + self.selectedForeground = selectedForeground } } @@ -99,6 +102,7 @@ public final class ReactionButtonComponent: Component { public final class View: UIButton, ComponentTaggedView { public let iconView: UIImageView private let textView: ComponentHostView + private let measureTextView: ComponentHostView private var currentComponent: ReactionButtonComponent? @@ -111,6 +115,9 @@ public final class ReactionButtonComponent: Component { self.textView = ComponentHostView() self.textView.isUserInteractionEnabled = false + self.measureTextView = ComponentHostView() + self.measureTextView.isUserInteractionEnabled = false + super.init(frame: CGRect()) self.addSubview(self.iconView) @@ -148,11 +155,11 @@ public final class ReactionButtonComponent: Component { } func update(component: ReactionButtonComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { - let sideInsets: CGFloat = 10.0 + let sideInsets: CGFloat = 8.0 let height: CGFloat = 30.0 - let spacing: CGFloat = 2.0 + let spacing: CGFloat = 4.0 - let defaultImageSize = CGSize(width: 20.0, height: 20.0) + let defaultImageSize = CGSize(width: 22.0, height: 22.0) let imageSize: CGSize if self.currentComponent?.reaction != component.reaction { @@ -179,43 +186,55 @@ public final class ReactionButtonComponent: Component { self.iconView.frame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize) - let textSize: CGSize - if self.currentComponent?.count != component.count || self.currentComponent?.colors != component.colors { - textSize = self.textView.update( + let text = "\(component.count)" + var measureText = "" + for _ in 0 ..< text.count { + measureText.append("0") + } + + let minTextWidth = self.measureTextView.update( + transition: .immediate, + component: AnyComponent(Text( + text: measureText, + font: Font.regular(11.0), + color: .black + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ).width + 2.0 + + let actualTextSize: CGSize + if self.currentComponent?.count != component.count || self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected { + actualTextSize = self.textView.update( transition: .immediate, component: AnyComponent(Text( - text: "\(component.count)", - font: Font.regular(13.0), - color: UIColor(argb: component.colors.foreground) + text: text, + font: Font.regular(11.0), + color: UIColor(argb: component.isSelected ? component.colors.selectedForeground : component.colors.deselectedForeground) )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) } else { - textSize = self.textView.bounds.size - } - - if self.currentComponent?.colors != component.colors { - self.backgroundColor = UIColor(argb: component.colors.background) + actualTextSize = self.textView.bounds.size } + let layoutTextSize = CGSize(width: max(actualTextSize.width, minTextWidth), height: actualTextSize.height) if self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected { if component.isSelected { - self.layer.borderColor = UIColor(argb: component.colors.stroke).cgColor - self.layer.borderWidth = 1.5 + self.backgroundColor = UIColor(argb: component.colors.selectedBackground) } else { - self.layer.borderColor = nil - self.layer.borderWidth = 0.0 + self.backgroundColor = UIColor(argb: component.colors.deselectedBackground) } } self.layer.cornerRadius = height / 2.0 - self.textView.frame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - textSize.height) / 2.0)), size: textSize) + self.textView.frame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - actualTextSize.height) / 2.0)), size: actualTextSize) self.currentComponent = component - return CGSize(width: imageSize.width + spacing + textSize.width + sideInsets * 2.0, height: height) + return CGSize(width: imageSize.width + spacing + layoutTextSize.width + sideInsets * 2.0, height: height) } } @@ -272,7 +291,20 @@ public final class ReactionButtonsLayoutContainer { var removedViews: [ComponentHostView] = [] var validIds = Set() - for reaction in reactions { + for reaction in reactions.sorted(by: { lhs, rhs in + var lhsCount = lhs.count + if lhs.isSelected { + lhsCount -= 1 + } + var rhsCount = rhs.count + if rhs.isSelected { + rhsCount -= 1 + } + if lhsCount != rhsCount { + return lhsCount > rhsCount + } + return lhs.reaction.value < rhs.reaction.value + }) { validIds.insert(reaction.reaction.value) let view: ComponentHostView diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index c87f5a6b59..95fae64665 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -360,6 +360,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi strongSelf.hapticFeedback.tap() } } + + if let reactionContextNode = strongSelf.reactionContextNode { + let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) + let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction + if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { + strongSelf.highlightedReaction = highlightedReaction + strongSelf.hapticFeedback.tap() + } + } } } } @@ -374,6 +383,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi strongSelf.highlightedActionNode = nil highlightedActionNode.performAction() } + if let highlightedReaction = strongSelf.highlightedReaction { + strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) + } } else { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil @@ -417,6 +429,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi strongSelf.hapticFeedback.tap() } } + + if let reactionContextNode = strongSelf.reactionContextNode { + let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) + let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction + if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { + strongSelf.highlightedReaction = highlightedReaction + strongSelf.hapticFeedback.tap() + } + } } } } @@ -431,6 +452,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi strongSelf.highlightedActionNode = nil highlightedActionNode.performAction() } + + if let highlightedReaction = strongSelf.highlightedReaction { + strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) + } } else { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index cb4294c0e2..8163f87fde 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import AsyncDisplayKit +import ObjCRuntimeUtils public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable { case linear @@ -388,11 +389,15 @@ public extension ContainedViewLayoutTransition { } func animatePositionWithKeyframes(node: ASDisplayNode, keyframes: [AnyObject], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animatePositionWithKeyframes(layer: node.layer, keyframes: keyframes, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + } + + func animatePositionWithKeyframes(layer: CALayer, keyframes: [AnyObject], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: completion?(true) case let .animated(duration, curve): - node.layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in + layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in completion?(value) }) } @@ -851,6 +856,28 @@ public extension ContainedViewLayoutTransition { } } + func animateTransformScale(layer: CALayer, from fromScale: CGPoint, to toScale: CGPoint, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let calculatedFrom: CGPoint + let calculatedTo: CGPoint + + calculatedFrom = fromScale + calculatedTo = toScale + + layer.animateScaleX(from: calculatedFrom.x, to: calculatedTo.x, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + layer.animateScaleY(from: calculatedFrom.y, to: calculatedTo.y, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction) + } + } + func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) { let t = view.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) @@ -1424,9 +1451,12 @@ public protocol ControlledTransitionAnimator: AnyObject { func finishAnimation() func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) + func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) + func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) + func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) } protocol AnyValueProviding { @@ -1609,87 +1639,46 @@ final class ControlledTransitionProperty { let layer: CALayer let path: String - let keyPath: AnyKeyPath var fromValue: AnyValue let toValue: AnyValue - private(set) var lastValue: AnyValue private let completion: ((Bool) -> Void)? - private var animator: AnyObject? - - init(layer: CALayer, path: String, keyPath: ReferenceWritableKeyPath, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding { + init(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding { self.layer = layer self.path = path - self.keyPath = keyPath self.fromValue = fromValue.anyValue self.toValue = toValue.anyValue - self.lastValue = self.fromValue self.completion = completion - if #available(iOS 10.0, *) { - layer[keyPath: keyPath] = fromValue - let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: { - layer[keyPath: keyPath] = toValue - }) - self.animator = animator - animator.pauseAnimation() - layer[keyPath: keyPath] = toValue - } - self.update(at: 0.0) } deinit { - if #available(iOS 10.0, *) { - if let animator = self.animator as? UIViewPropertyAnimator { - animator.stopAnimation(true) - self.animator = nil - } - } + self.layer.removeAnimation(forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())") } func update(at fraction: CGFloat) { let value = self.fromValue.interpolate(toValue, fraction) - self.lastValue = value - //self.write(self.layer, value) - if #available(iOS 10.0, *) { - if let animator = self.animator as? UIViewPropertyAnimator { - animator.fractionComplete = fraction - } - } - - /*let animation = CABasicAnimation() + let animation = CABasicAnimation(keyPath: self.path) animation.speed = 0.0 + animation.beginTime = CACurrentMediaTime() + 1000.0 + animation.timeOffset = 0.01 animation.duration = 1.0 animation.fillMode = .both animation.fromValue = value.nsValue - animation.toValue = self.toValue.nsValue - animation.keyPath = self.path - if let previousAnimation = self.layer.animation(forKey: self.animationKey) { - self.layer.removeAnimation(forKey: self.animationKey) - let _ = previousAnimation - } - self.layer.add(animation, forKey: self.animationKey)*/ + animation.toValue = value.nsValue + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.isRemovedOnCompletion = false + self.layer.add(animation, forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())") } func complete(atEnd: Bool) { - if #available(iOS 10.0, *) { - if let animator = self.animator as? UIViewPropertyAnimator { - animator.stopAnimation(true) - /*if atEnd { - animator.finishAnimation(at: .current) - }*/ - self.animator = nil - } - } - self.completion?(atEnd) } } public final class ControlledTransition { - @available(iOS 10.0, *) public final class NativeAnimator: ControlledTransitionAnimator { public let duration: Double private let curve: ContainedViewLayoutTransitionCurve @@ -1713,7 +1702,7 @@ public final class ControlledTransition { for j in 0 ..< other.animations.count { let otherAnimation = other.animations[j] - if animation.layer === otherAnimation.layer && animation.keyPath == otherAnimation.keyPath { + if animation.layer === otherAnimation.layer && animation.path == otherAnimation.path { if animation.toValue == otherAnimation.toValue { removeAnimationIndices.append(i) } else { @@ -1723,7 +1712,8 @@ public final class ControlledTransition { } for j in removeOtherAnimationIndices.reversed() { - other.animations.remove(at: j).complete(atEnd: false) + let otherAnimation = other.animations.remove(at: j) + otherAnimation.complete(atEnd: false) } } @@ -1762,7 +1752,9 @@ public final class ControlledTransition { private func add(animation: ControlledTransitionProperty) { for i in 0 ..< self.animations.count { let otherAnimation = self.animations[i] - if otherAnimation.layer === animation.layer && otherAnimation.keyPath == animation.keyPath { + if otherAnimation.layer === animation.layer && otherAnimation.path == animation.path { + let currentAnimation = self.animations[i] + currentAnimation.complete(atEnd: false) self.animations.remove(at: i) break } @@ -1771,25 +1763,56 @@ public final class ControlledTransition { } public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) { + if layer.opacity == Float(alpha) { + return + } let fromValue = layer.presentation()?.opacity ?? layer.opacity - //layer.opacity = Float(alpha) + layer.opacity = Float(alpha) self.add(animation: ControlledTransitionProperty( layer: layer, path: "opacity", - keyPath: \.opacity, fromValue: fromValue, toValue: Float(alpha), completion: completion )) } + public func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) { + let t = layer.presentation()?.transform ?? layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + if currentScale == scale { + return + } + layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + self.add(animation: ControlledTransitionProperty( + layer: layer, + path: "transform.scale", + fromValue: currentScale, + toValue: scale, + completion: completion + )) + } + + public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) { + self.add(animation: ControlledTransitionProperty( + layer: layer, + path: "transform.scale", + fromValue: fromValue, + toValue: toValue, + completion: completion + )) + } + public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) { + if layer.position == position { + return + } let fromValue = layer.presentation()?.position ?? layer.position - //layer.position = position + layer.position = position self.add(animation: ControlledTransitionProperty( layer: layer, path: "position", - keyPath: \.position, fromValue: fromValue, toValue: position, completion: completion @@ -1797,12 +1820,14 @@ public final class ControlledTransition { } public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) { + if layer.bounds == bounds { + return + } let fromValue = layer.presentation()?.bounds ?? layer.bounds - //layer.bounds = bounds + layer.bounds = bounds self.add(animation: ControlledTransitionProperty( layer: layer, path: "bounds", - keyPath: \.bounds, fromValue: fromValue, toValue: bounds, completion: completion @@ -1810,20 +1835,21 @@ public final class ControlledTransition { } public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) { - if layer.frame == frame { + self.updatePosition(layer: layer, position: frame.center, completion: completion) + self.updateBounds(layer: layer, bounds: CGRect(origin: CGPoint(), size: frame.size), completion: nil) + } + + public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) { + if layer.cornerRadius == cornerRadius { return } - let fromValue = layer.presentation()?.frame ?? layer.frame - if let presentation = layer.presentation(), presentation.frame != layer.frame { - assert(true) - } - //layer.frame = frame + let fromValue = layer.presentation()?.cornerRadius ?? layer.cornerRadius + layer.cornerRadius = cornerRadius self.add(animation: ControlledTransitionProperty( layer: layer, - path: "frame", - keyPath: \.frame, + path: "cornerRadius", fromValue: fromValue, - toValue: frame, + toValue: cornerRadius, completion: completion )) } @@ -1859,6 +1885,14 @@ public final class ControlledTransition { self.transition.updateAlpha(layer: layer, alpha: alpha, completion: completion) } + public func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?) { + self.transition.updateTransformScale(layer: layer, scale: scale, completion: completion) + } + + public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?) { + self.transition.animateTransformScale(layer: layer, from: CGPoint(x: fromValue, y: fromValue), to: CGPoint(x: toValue, y: toValue), completion: completion) + } + public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) { self.transition.updatePosition(layer: layer, position: position, completion: completion) } @@ -1870,6 +1904,10 @@ public final class ControlledTransition { public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) { self.transition.updateFrame(layer: layer, frame: frame, completion: completion) } + + public func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?) { + self.transition.updateCornerRadius(layer: layer, cornerRadius: cornerRadius, completion: completion) + } } public let animator: ControlledTransitionAnimator @@ -1877,13 +1915,14 @@ public final class ControlledTransition { public init( duration: Double, - curve: ContainedViewLayoutTransitionCurve + curve: ContainedViewLayoutTransitionCurve, + interactive: Bool ) { self.legacyAnimator = LegacyAnimator( duration: duration, curve: curve ) - if #available(iOS 10.0, *) { + if interactive { self.animator = NativeAnimator( duration: duration, curve: curve @@ -1894,10 +1933,8 @@ public final class ControlledTransition { } public func merge(with other: ControlledTransition) { - if #available(iOS 10.0, *) { - if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator { - animator.merge(with: otherAnimator) - } + if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator { + animator.merge(with: otherAnimator) } } } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 7385354ba3..303016ee23 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -1585,7 +1585,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if updateAnimationIsCrossfade { updateAnimation = .Crossfade } else if updateAnimationIsAnimated { - let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring) + let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring, interactive: true) controlledTransition = transition updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition) } else { @@ -2048,7 +2048,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture var controlledTransition: ControlledTransition? let updateAnimation: ListViewItemUpdateAnimation if animated { - let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring) + let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring, interactive: true) controlledTransition = transition updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition) } else { diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index ede32ea4d5..ea362bcab4 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -251,6 +251,27 @@ public final class NavigationBackgroundNode: ASDisplayNode { effectView.clipsToBounds = !cornerRadius.isZero } } + + public func update(size: CGSize, cornerRadius: CGFloat = 0.0, animator: ControlledTransitionAnimator) { + self.validLayout = (size, cornerRadius) + + let contentFrame = CGRect(origin: CGPoint(), size: size) + animator.updateFrame(layer: self.backgroundNode.layer, frame: contentFrame, completion: nil) + if let effectView = self.effectView, effectView.frame != contentFrame { + animator.updateFrame(layer: effectView.layer, frame: contentFrame, completion: nil) + if let sublayers = effectView.layer.sublayers { + for sublayer in sublayers { + animator.updateFrame(layer: sublayer, frame: contentFrame, completion: nil) + } + } + } + + animator.updateCornerRadius(layer: self.backgroundNode.layer, cornerRadius: cornerRadius, completion: nil) + if let effectView = self.effectView { + animator.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius, completion: nil) + effectView.clipsToBounds = !cornerRadius.isZero + } + } } open class NavigationBar: ASDisplayNode { diff --git a/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift b/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift index 35d1372542..42e6e2beb2 100644 --- a/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift +++ b/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -294,7 +294,7 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, self.state = .ended case .waitForDoubleTap: self.state = .began - let timer = Timer(timeInterval: 0.2, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.tapEvent), userInfo: nil, repeats: false) + let timer = Timer(timeInterval: 0.16, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.tapEvent), userInfo: nil, repeats: false) self.timer = timer RunLoop.main.add(timer, forMode: .common) case let .waitForHold(_, acceptTap): diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 60e6bb5404..6cf8a9592f 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -13,7 +13,7 @@ public enum ReactionGestureItem { } public final class ReactionContextItem { - public struct Reaction { + public struct Reaction: Equatable { public var rawValue: String public init(rawValue: String) { @@ -446,44 +446,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [AnyObject] { - let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation) - - let x1 = sourcePoint.x - let y1 = sourcePoint.y - let x2 = midPoint.x - let y2 = midPoint.y - let x3 = targetPosition.x - let y3 = targetPosition.y - - var keyframes: [AnyObject] = [] - if abs(y1 - y3) < 5.0 || abs(x1 - x3) < 5.0 { - for i in 0 ..< 10 { - let k = CGFloat(i) / CGFloat(10 - 1) - let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k - let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k - keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) - } - } else { - let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) - let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) - let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) - - for i in 0 ..< 10 { - let k = CGFloat(i) / CGFloat(10 - 1) - let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k - let y = a * x * x + b * x + c - keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) - } + private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else { + completion() + return } - return keyframes - } - - private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) { - let itemFrame: CGRect = itemNode.frame - let _ = itemFrame - let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) targetSnapshotView.frame = targetFrame @@ -542,67 +510,65 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if itemNode.item.reaction.rawValue != value { continue } - if let targetSnapshotView = targetView.snapshotContentTree() { - if hideNode { - targetView.isHidden = true - } - - itemNode.isExtracted = true - let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) - let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) - - let expandedScale: CGFloat = 3.0 - let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale)) - - let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear) - - self.addSubnode(itemNode) - itemNode.frame = selfSourceRect - itemNode.position = expandedFrame.center - transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size)) - itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: transition) - transition.animatePositionWithKeyframes(node: itemNode, keyframes: self.generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0)) - - let additionalAnimationNode = AnimatedStickerNode() - let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 - let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) - .offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) - - additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id))) - additionalAnimationNode.frame = animationFrame - if incomingMessage { - additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) - } - additionalAnimationNode.updateLayout(size: animationFrame.size) - self.addSubnode(additionalAnimationNode) - - var mainAnimationCompleted = false - var additionalAnimationCompleted = false - let intermediateCompletion: () -> Void = { - if mainAnimationCompleted && additionalAnimationCompleted { - completion() - } - } - - additionalAnimationNode.completed = { _ in - additionalAnimationCompleted = true - intermediateCompletion() - } - - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: { - additionalAnimationNode.visibility = true - }) - - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: { - self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: { - mainAnimationCompleted = true - intermediateCompletion() - }) - }) - return + if hideNode { + targetView.isHidden = true } + + itemNode.isExtracted = true + let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) + let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) + + let expandedScale: CGFloat = 3.0 + let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale)) + + let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear) + + self.addSubnode(itemNode) + itemNode.frame = selfSourceRect + itemNode.position = expandedFrame.center + transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size)) + itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: transition) + transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0)) + + let additionalAnimationNode = AnimatedStickerNode() + let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 + let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) + .offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) + + additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id))) + additionalAnimationNode.frame = animationFrame + if incomingMessage { + additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + additionalAnimationNode.updateLayout(size: animationFrame.size) + self.addSubnode(additionalAnimationNode) + + var mainAnimationCompleted = false + var additionalAnimationCompleted = false + let intermediateCompletion: () -> Void = { + if mainAnimationCompleted && additionalAnimationCompleted { + completion() + } + } + + additionalAnimationNode.completed = { _ in + additionalAnimationCompleted = true + intermediateCompletion() + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: { + additionalAnimationNode.visibility = true + }) + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: { + self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { + mainAnimationCompleted = true + intermediateCompletion() + }) + }) + return } completion() } @@ -638,6 +604,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { return nil } + public func performReactionSelection(reaction: ReactionContextItem.Reaction) { + for itemNode in self.itemNodes { + if itemNode.item.reaction == reaction { + self.reactionSelected?(itemNode.item) + break + } + } + } + public func setHighlightedReaction(_ value: ReactionContextItem.Reaction?) { self.highlightedReaction = value if let (size, insets, anchorRect) = self.validLayout { @@ -668,7 +643,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { - guard let sourceSnapshotView = targetView.snapshotContentTree(), let targetSnapshotView = targetView.snapshotContentTree() else { + guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return } @@ -733,16 +708,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode { }) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { - self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: { + self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, hideNode: hideNode, completion: { mainAnimationCompleted = true intermediateCompletion() }) }) } - private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) { - let itemFrame: CGRect = itemNode.frame - let _ = itemFrame + private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else { + completion() + return + } let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) @@ -793,3 +770,84 @@ public final class StandaloneReactionAnimation: ASDisplayNode { transition.animateOffsetAdditive(node: self, offset: -offset.y) } } + +public final class StandaloneDismissReactionAnimation: ASDisplayNode { + private let hapticFeedback = HapticFeedback() + + override public init() { + super.init() + + self.isUserInteractionEnabled = false + } + + public func animateReactionDismiss(sourceView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + guard let sourceSnapshotView = sourceView.snapshotContentTree() else { + completion() + return + } + if hideNode { + sourceView.isHidden = true + } + + let sourceRect = self.view.convert(sourceView.bounds, from: sourceView) + sourceSnapshotView.frame = sourceRect + self.view.addSubview(sourceSnapshotView) + + var targetOffset: CGFloat = 120.0 + if sourceRect.midX > self.bounds.width / 2.0 { + targetOffset = -targetOffset + } + let targetPoint = CGPoint(x: sourceRect.midX + targetOffset, y: sourceRect.midY) + + let hapticFeedback = self.hapticFeedback + hapticFeedback.prepareImpact(.soft) + + let keyframes = generateParabollicMotionKeyframes(from: sourceRect.center, to: targetPoint, elevation: 25.0) + let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .easeInOut) + sourceSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.04, delay: 0.18 - 0.04, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak sourceSnapshotView, weak hapticFeedback] _ in + sourceSnapshotView?.removeFromSuperview() + hapticFeedback?.impact(.soft) + completion() + }) + transition.animatePositionWithKeyframes(layer: sourceSnapshotView.layer, keyframes: keyframes, removeOnCompletion: false) + } + + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y) + transition.animateOffsetAdditive(node: self, offset: -offset.y) + } +} + +private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [AnyObject] { + let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation) + + let x1 = sourcePoint.x + let y1 = sourcePoint.y + let x2 = midPoint.x + let y2 = midPoint.y + let x3 = targetPosition.x + let y3 = targetPosition.y + + var keyframes: [AnyObject] = [] + if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 { + for i in 0 ..< 10 { + let k = CGFloat(i) / CGFloat(10 - 1) + let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k + let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k + keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) + } + } else { + let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + + for i in 0 ..< 10 { + let k = CGFloat(i) / CGFloat(10 - 1) + let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k + let y = a * x * x + b * x + c + keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) + } + } + + return keyframes +} diff --git a/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift index a8a57dd5af..a99610cfc9 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeEmojiKeywordsOperation.swift @@ -5,7 +5,7 @@ import MurMurHash32 func addSynchronizeEmojiKeywordsOperation(transaction: Transaction, inputLanguageCode: String, languageCode: String?, fromVersion: Int32?) { let tag = OperationLogTags.SynchronizeEmojiKeywords - let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(1), id: PeerId.Id._internalFromInt64Value(Int64(murMurHashString32(inputLanguageCode)))) + let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(1), id: PeerId.Id._internalFromInt64Value(Int64(abs(murMurHashString32(inputLanguageCode))))) var hasExistingOperation = false transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag) { entry -> Bool in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 06a317b92c..6064ac4bdd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1074,7 +1074,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(controller) }) } - }, updateMessageReaction: { [weak self] initialMessage, value in + }, updateMessageReaction: { [weak self] initialMessage, reaction in guard let strongSelf = self else { return } @@ -1088,70 +1088,111 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var updatedReaction: String? = value - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - for reaction in attribute.reactions { - if reaction.value == updatedReaction { - if reaction.isSelected { + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { + return + } + guard item.message.id == message.id else { + return + } + + var updatedReaction: String? + switch reaction { + case .default: + updatedReaction = item.associatedData.defaultReaction + case let .reaction(value): + updatedReaction = value + } + + var removedReaction: String? + + for attribute in message.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + for listReaction in attribute.reactions { + switch reaction { + case .default: + if listReaction.isSelected { + updatedReaction = nil + removedReaction = listReaction.value + } + case let .reaction(value): + if listReaction.value == value && listReaction.isSelected { + updatedReaction = nil + removedReaction = value + } + } + } + } else if let attribute = attribute as? PendingReactionsMessageAttribute { + if attribute.value != nil { + switch reaction { + case .default: updatedReaction = nil + removedReaction = attribute.value + case let .reaction(value): + if attribute.value == value { + updatedReaction = nil + removedReaction = value + } } } } - } else if let attribute = attribute as? PendingReactionsMessageAttribute { - if let current = attribute.value, current == updatedReaction { - updatedReaction = nil - } } - } - - if updatedReaction != nil { - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { - if item.message.id == message.id { - itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in - guard let strongSelf = self else { - return - } - if let updatedReaction = updatedReaction, let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { - for reaction in availableReactions.reactions { - if reaction.value == updatedReaction { - let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: reaction.value), - stillAnimation: reaction.selectAnimation, - listAnimation: reaction.activateAnimation, - applicationAnimation: reaction.effectAnimation - )) - - strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation - strongSelf.currentStandaloneReactionItemNode = itemNode - - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - }) - - break - } - } + + if let updatedReaction = updatedReaction { + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.tap() + + itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in + guard let strongSelf = self else { + return + } + if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { + for reaction in availableReactions.reactions { + if reaction.value == updatedReaction { + let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + )) - /*targetView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: 90.0) + strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation + strongSelf.currentStandaloneReactionItemNode = itemNode - if let strongSelf = self { - if strongSelf.selectPollOptionFeedback == nil { - strongSelf.selectPollOptionFeedback = HapticFeedback() - } - strongSelf.selectPollOptionFeedback?.tap() - }*/ + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + }) + + break } - }) + } + } + }) + } else if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { + var hideRemovedReaction: Bool = false + if let reactions = mergedMessageReactions(attributes: message.attributes) { + for reaction in reactions.reactions { + if reaction.value == removedReaction { + hideRemovedReaction = reaction.count == 1 + break + } } } + + let standaloneDismissAnimation = StandaloneDismissReactionAnimation() + standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) + standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in + standaloneDismissAnimation?.removeFromSupernode() + }) } + + let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start() } - - let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start() }, activateMessagePinch: { [weak self] sourceNode in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index cdd31605cc..087df11a59 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -46,12 +46,17 @@ public enum ChatControllerInteractionSwipeAction { case reply } +public enum ChatControllerInteractionReaction { + case `default` + case reaction(String) +} + public final class ChatControllerInteraction { let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void - let updateMessageReaction: (Message, String) -> Void + let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void let activateMessagePinch: (PinchSourceContainerNode) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void @@ -148,7 +153,7 @@ public final class ChatControllerInteraction { openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, - updateMessageReaction: @escaping (Message, String) -> Void, + updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void, activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index ee7a76074c..d41796e3cd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -38,8 +38,6 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { self.addSubnode(self.backgroundBlurNode) self.addSubnode(self.accessibilityArea) - - //self.backgroundBlurNode.view.mask = backgroundMaskNode.view self.accessibilityArea.activate = { [weak self] in self?.buttonPressed() @@ -85,7 +83,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ message: Message, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) { let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode) return { context, theme, bubbleCorners, strings, message, button, constrainedWidth, position in @@ -142,12 +140,15 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } return (titleSize.size.width + sideInset + sideInset, { width in - return (CGSize(width: width, height: 42.0), { + return (CGSize(width: width, height: 42.0), { animation in + var animation = animation + let node: ChatMessageActionButtonNode if let maybeNode = maybeNode { node = maybeNode } else { node = ChatMessageActionButtonNode() + animation = .None } node.button = button @@ -160,10 +161,10 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } node.backgroundMaskNode.image = backgroundMaskImage - node.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)) + animation.animator.updateFrame(layer: node.backgroundMaskNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil) - node.backgroundBlurNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)) - node.backgroundBlurNode.update(size: node.backgroundBlurNode.bounds.size, cornerRadius: bubbleCorners.auxiliaryRadius, transition: .immediate) + animation.animator.updateFrame(layer: node.backgroundBlurNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil) + node.backgroundBlurNode.update(size: node.backgroundBlurNode.bounds.size, cornerRadius: bubbleCorners.auxiliaryRadius, animator: animation.animator) node.backgroundBlurNode.updateColor(color: selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper), transition: .immediate) if iconImage != nil { @@ -185,11 +186,17 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { node.addSubnode(titleNode) titleNode.isUserInteractionEnabled = false } - titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size) + let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size) + titleNode.layer.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + animation.animator.updatePosition(layer: titleNode.layer, position: titleFrame.center, completion: nil) - node.buttonView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)) - node.iconNode?.frame = CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0) + if let buttonView = node.buttonView { + animation.animator.updateFrame(layer: buttonView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil) + } + if let iconNode = node.iconNode { + animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil) + } node.accessibilityArea.accessibilityLabel = title node.accessibilityArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)) @@ -225,7 +232,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ replyMarkup: ReplyMarkupMessageAttribute, _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) { let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] return { context, theme, chatBubbleCorners, strings, replyMarkup, message, constrainedWidth in @@ -234,14 +241,14 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { var overallMinimumRowWidth: CGFloat = 0.0 - var finalizeRowLayouts: [[((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))]] = [] + var finalizeRowLayouts: [[((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))]] = [] var rowIndex = 0 var buttonIndex = 0 for row in replyMarkup.rows { var maximumRowButtonWidth: CGFloat = 0.0 let maximumButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count))) - var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))] = [] + var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))] = [] var rowButtonIndex = 0 for button in row.buttons { let buttonPosition: MessageBubbleActionButtonPosition @@ -259,7 +266,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { buttonPosition = .middle } - let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) + let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) if buttonIndex < currentButtonLayouts.count { prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, message, button, maximumButtonWidth, buttonPosition) } else { @@ -280,7 +287,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } return (min(constrainedWidth, overallMinimumRowWidth), { constrainedWidth in - var buttonFramesAndApply: [(CGRect, () -> ChatMessageActionButtonNode)] = [] + var buttonFramesAndApply: [(CGRect, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)] = [] var verticalRowOffset: CGFloat = 0.0 verticalRowOffset += buttonSpacing @@ -303,7 +310,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { verticalRowOffset = max(0.0, verticalRowOffset - buttonSpacing) } - return (CGSize(width: constrainedWidth, height: verticalRowOffset), { animated in + return (CGSize(width: constrainedWidth, height: verticalRowOffset), { animation in let node: ChatMessageActionButtonsNode if let maybeNode = maybeNode { node = maybeNode @@ -314,13 +321,15 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { var updatedButtons: [ChatMessageActionButtonNode] = [] var index = 0 for (buttonFrame, buttonApply) in buttonFramesAndApply { - let buttonNode = buttonApply() - buttonNode.frame = buttonFrame + let buttonNode = buttonApply(animation) updatedButtons.append(buttonNode) if buttonNode.supernode == nil { node.addSubnode(buttonNode) buttonNode.pressed = node.buttonPressedWrapper buttonNode.longTapped = node.buttonLongTappedWrapper + buttonNode.frame = buttonFrame + } else { + animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil) } index += 1 } @@ -345,12 +354,6 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { } node.buttonNodes = updatedButtons - if animated { - /*UIView.transition(with: node.view, duration: 0.2, options: [.transitionCrossDissolve], animations: { - - }, completion: nil)*/ - } - return node }) }) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index b775f281bc..04a31f9036 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -199,6 +199,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var replyBackgroundNode: NavigationBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? + private var reactionButtonsNode: ChatMessageReactionButtonsNode? private let messageAccessibilityArea: AccessibilityAreaNode @@ -341,7 +342,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } } - return .waitForSingleTap + return .waitForDoubleTap } recognizer.longTap = { [weak self] point, recognizer in guard let strongSelf = self else { @@ -701,6 +702,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let imageLayout = self.imageNode.asyncLayout() let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) + let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode) let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) @@ -896,7 +898,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone, + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactions, @@ -1021,22 +1023,53 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } var maxContentWidth = imageSize.width - var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? + var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } - var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)? + var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)? if let actionButtonsFinalize = actionButtonsFinalize { actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) } + let reactions: ReactionsMessageAttribute + if shouldDisplayInlineDateReactions(message: item.message) { + reactions = ReactionsMessageAttribute(reactions: [], recentPeers: []) + } else { + reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: []) + } + var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? + if !reactions.reactions.isEmpty { + let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right + + let maxReactionsWidth = params.width - totalInset + let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( + context: item.context, + presentationData: item.presentationData, + availableReactions: item.associatedData.availableReactions, + reactions: reactions, + isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), + constrainedWidth: maxReactionsWidth + )) + maxContentWidth = max(maxContentWidth, minWidth) + reactionButtonsFinalize = buttonsLayout + } + + var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)? + if let reactionButtonsFinalize = reactionButtonsFinalize { + reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth) + } + var layoutSize = CGSize(width: params.width, height: contentHeight) if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { layoutSize.height += actionButtonsSizeAndApply.0.height } + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + layoutSize.height += reactionButtonsSizeAndApply.0.height + } return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in if let strongSelf = self { @@ -1122,8 +1155,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.shareButtonNode = nil } - dateAndStatusApply(.None) - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize) + let dateAndStatusFrame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize) + animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) + dateAndStatusApply(animation) if needsReplyBackground { if let replyBackgroundNode = strongSelf.replyBackgroundNode { @@ -1272,13 +1306,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { - var animated = false - if let _ = strongSelf.actionButtonsNode { - if case .System = animation { - animated = true - } - } - let actionButtonsNode = actionButtonsSizeAndApply.1(animated) + let actionButtonsNode = actionButtonsSizeAndApply.1(animation) let previousFrame = actionButtonsNode.frame let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: actionButtonsSizeAndApply.0) actionButtonsNode.frame = actionButtonsFrame @@ -1305,6 +1333,36 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.actionButtonsNode = nil } + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) + let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: reactionButtonsSizeAndApply.0) + if reactionButtonsNode !== strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = reactionButtonsNode + reactionButtonsNode.reactionSelected = { value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) + } + reactionButtonsNode.frame = reactionButtonsFrame + strongSelf.addSubnode(reactionButtonsNode) + if animation.isAnimated { + reactionButtonsNode.animateIn(animation: animation) + } + } else { + animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) + } + } else if let reactionButtonsNode = strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = nil + if animation.isAnimated { + reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in + reactionButtonsNode?.removeFromSupernode() + }) + } else { + reactionButtonsNode.removeFromSupernode() + } + } + if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { strongSelf.dateAndStatusNode.pressed = { guard let strongSelf = self else { @@ -1329,7 +1387,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + if let item = self.item, let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: recognizer) { if case .doubleTap = gesture { self.containerNode.cancelGesture() @@ -1340,10 +1398,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { case let .optionalAction(f): f() case let .openContextMenu(tapMessage, selectAll, subFrame): - self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) + if canAddMessageReactions(message: item.message) { + item.controllerInteraction.updateMessageReaction(item.message, .default) + } else { + item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) + } } } else if case .tap = gesture { - self.item?.controllerInteraction.clickThroughMessage() + item.controllerInteraction.clickThroughMessage() } } default: @@ -1916,6 +1978,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return shareButtonNode.view } + if let reactionButtonsNode = self.reactionButtonsNode { + if let result = reactionButtonsNode.hitTest(self.view.convert(point, to: reactionButtonsNode.view), with: event) { + return result + } + } + return super.hitTest(point, with: event) } @@ -2225,6 +2293,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } override func targetReactionView(value: String) -> UIView? { + if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { + return result + } if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionView(value: value) } diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 082fd485dd..da3525ce53 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -363,7 +363,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)))? - var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)? + var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)? let string = NSMutableAttributedString() var notEmpty = false @@ -634,7 +634,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { impressionCount: viewCount, dateText: dateText, type: textStatusType, - layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: nil), + layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), constrainedSize: textConstrainedSize, availableReactions: associatedData.availableReactions, reactions: dateReactions, @@ -797,11 +797,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { adjustedLineHeight += statusSizeAndApply.0.height } - /*var adjustedStatusFrame: CGRect? - if statusInText, let statusFrame = statusFrame { - adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size) - }*/ - adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width) return (adjustedBoundingSize, { [weak self] animation, synchronousLoads in @@ -811,17 +806,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.media = mediaAndFlags?.0 strongSelf.theme = presentationData.theme - var hasAnimation = true - var transition: ContainedViewLayoutTransition = .immediate - switch animation { - case .None, .Crossfade: - hasAnimation = false - case let .System(duration, _): - hasAnimation = true - transition = .animated(duration: duration, curve: .easeInOut) - } - let _ = hasAnimation - strongSelf.lineNode.image = lineImage strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) strongSelf.lineNode.isHidden = !displayLine @@ -864,6 +848,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.openMedia?(mode) } } + contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in + guard let controllerInteraction = controllerInteraction else { + return + } + controllerInteraction.updateMessageReaction(message, value) + } contentImageNode.visibility = strongSelf.visibility != .none } let _ = contentImageApply(animation, synchronousLoads) @@ -924,7 +914,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (videoLayout, apply) = contentInstantVideoSizeAndApply { contentMediaHeight = videoLayout.contentSize.height - let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), transition) + let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation) if strongSelf.contentInstantVideoNode !== contentInstantVideoNode { strongSelf.contentInstantVideoNode = contentInstantVideoNode strongSelf.addSubnode(contentInstantVideoNode) @@ -1090,6 +1080,24 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } + func reactionTargetView(value: String) -> UIView? { + if !self.statusNode.isHidden { + if let result = self.statusNode.reactionView(value: value) { + return result + } + } + if let result = self.contentFileNode?.dateAndStatusNode.reactionView(value: value) { + return result + } + if let result = self.contentImageNode?.dateAndStatusNode.reactionView(value: value) { + return result + } + if let result = self.contentInstantVideoNode?.dateAndStatusNode.reactionView(value: value) { + return result + } + return nil + } + func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.contentImageNode?.playMediaWithSound() } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift index a99c5992df..63bd5c68c5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentCalclulateImageCorners.swift @@ -27,6 +27,9 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat case .Right: topLeftCorner = .Corner(normalRadius) topRightCorner = .Corner(mergedRadius) + case .Both: + topLeftCorner = .Corner(mergedRadius) + topRightCorner = .Corner(mergedRadius) } } case let .mosaic(position, _): @@ -65,6 +68,9 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat case .Left: bottomLeftCorner = .Corner(mergedRadius) bottomRightCorner = .Corner(normalRadius) + case .Both: + bottomLeftCorner = .Corner(mergedRadius) + bottomRightCorner = .Corner(mergedRadius) case let .None(status): let bubbleInsets: UIEdgeInsets if case .color = chatPresentationData.theme.wallpaper { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift index 268b015150..9f56b3f135 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift @@ -37,6 +37,7 @@ enum ChatMessageBubbleMergeStatus { case None(ChatMessageBubbleNoneMergeStatus) case Left case Right + case Both } enum ChatMessageBubbleRelativePosition { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 9ddd088070..061e5d0a89 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -46,7 +46,7 @@ private final class ChatMessageBubbleClippingNode: ASDisplayNode { } } -private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], Bool) { +private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)], Bool, Bool) { var result: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)] = [] var skipText = false var messageWithCaptionToAdd: (Message, ChatMessageEntryAttributes)? @@ -56,10 +56,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ var previousItemIsFile = false var hasFiles = false + var needReactions = true + outer: for (message, itemAttributes) in item.content { for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil { result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false break outer } } @@ -86,6 +89,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ isFile = true hasFiles = true result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing))) + needReactions = false } } else if let action = media as? TelegramMediaAction { isAction = true @@ -94,26 +98,33 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else { result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) } + needReactions = false } else if let _ = media as? TelegramMediaMap { result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) } else if let _ = media as? TelegramMediaGame { skipText = true result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false break inner } else if let _ = media as? TelegramMediaInvoice { skipText = true result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false break inner } else if let _ = media as? TelegramMediaContact { result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false } else if let _ = media as? TelegramMediaExpiredContent { result.removeAll() result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) - return (result, false) + needReactions = false + return (result, false, false) } else if let _ = media as? TelegramMediaPoll { result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false } else if let _ = media as? TelegramMediaUnsupported { isUnsupportedMedia = true + needReactions = false } previousItemIsFile = isFile } @@ -130,6 +141,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ skipText = true } else { result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: isFile ? .condensed : .default))) + needReactions = false } } else { if case .group = item.content { @@ -142,6 +154,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if let webpage = media as? TelegramMediaWebpage { if case .Loaded = webpage.content { result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false } break inner } @@ -151,34 +164,39 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.removeAll() result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false } if isUnsupportedMedia { result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false } } if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd { result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false } if let additionalContent = item.additionalContent { switch additionalContent { case let .eventLogPreviousMessage(previousMessage): result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false case let .eventLogPreviousDescription(previousMessage): result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false case let .eventLogPreviousLink(previousMessage): result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false } } let firstMessage = item.content.firstMessage - if let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty { - if result.last?.1 == ChatMessageWebpageBubbleContentNode.self || result.last?.1 == ChatMessagePollBubbleContentNode.self || result.last?.1 == ChatMessageContactBubbleContentNode.self { - result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) - } + let reactionsAreInline = shouldDisplayInlineDateReactions(message: firstMessage) + if reactionsAreInline { + needReactions = false } if !isAction && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) { @@ -222,12 +240,25 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } } + if !reactionsAreInline, let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty { + if result.last?.1 == ChatMessageWebpageBubbleContentNode.self || + result.last?.1 == ChatMessagePollBubbleContentNode.self || + result.last?.1 == ChatMessageContactBubbleContentNode.self { + result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) + needReactions = false + } else if result.last?.1 == ChatMessageCommentFooterContentNode.self { + result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) + needReactions = false + } + } + var needSeparateContainers = false if case .group = item.content, hasFiles { needSeparateContainers = true + needReactions = false } - return (result, needSeparateContainers) + return (result, needSeparateContainers, needReactions) } private let chatMessagePeerIdColors: [UIColor] = [ @@ -428,6 +459,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode private(set) var contentNodes: [ChatMessageBubbleContentNode] = [] private var mosaicStatusNode: ChatMessageDateAndStatusNode? private var actionButtonsNode: ChatMessageActionButtonsNode? + private var reactionButtonsNode: ChatMessageReactionButtonsNode? private var shareButtonNode: ChatMessageShareButton? @@ -890,6 +922,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) + let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode) let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode) @@ -911,6 +944,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode forwardInfoLayout: forwardInfoLayout, replyInfoLayout: replyInfoLayout, actionButtonsLayout: actionButtonsLayout, + reactionButtonsLayout: reactionButtonsLayout, mosaicStatusLayout: mosaicStatusLayout, layoutConstants: layoutConstants, currentItem: currentItem, @@ -926,7 +960,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), - actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), + actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), + reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)), layoutConstants: ChatMessageItemLayoutConstants, currentItem: ChatMessageItem?, @@ -1160,7 +1195,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, BubbleItemAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))))] = [] var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]? - let (contentNodeMessagesAndClasses, needSeparateContainers) = contentNodeMessagesAndClassesForItem(item) + let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item) for contentNodeItemValue in contentNodeMessagesAndClasses { let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes) @@ -1239,9 +1274,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var contentPropertiesAndLayouts: [(CGSize?, ChatMessageBubbleContentProperties, ChatMessageBubblePreparePosition, BubbleItemAttributes, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void)), UInt32?, Bool?)] = [] - let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) - let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) - var backgroundHiding: ChatMessageBubbleContentBackgroundHiding? var hasSolidWallpaper = false switch item.presentationData.theme.wallpaper { @@ -1383,6 +1415,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode index += 1 } + let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) + var bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) + + let bubbleReactions: ReactionsMessageAttribute + if needReactions { + bubbleReactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: []) + } else { + bubbleReactions = ReactionsMessageAttribute(reactions: [], recentPeers: []) + } + if !bubbleReactions.reactions.isEmpty { + bottomNodeMergeStatus = .Both + } + var currentCredibilityIconImage: UIImage? var initialDisplayHeader = true @@ -1538,7 +1583,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone, + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactions, @@ -1714,13 +1759,27 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var maxContentWidth: CGFloat = headerSize.width - var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? + var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } + var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? + if !bubbleReactions.reactions.isEmpty { + let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( + context: item.context, + presentationData: item.presentationData, + availableReactions: item.associatedData.availableReactions, + reactions: bubbleReactions, + isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), + constrainedWidth: maximumNodeWidth + )) + maxContentWidth = max(maxContentWidth, minWidth) + reactionButtonsFinalize = buttonsLayout + } + for i in 0 ..< contentPropertiesAndLayouts.count { let (_, contentNodeProperties, preparePosition, _, contentNodeLayout, contentGroupId, itemSelection) = contentPropertiesAndLayouts[i] @@ -1744,7 +1803,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode case let .None(status): if position.contains(.top) && position.contains(.left) { switch status { - case .Left: + case .Left, .Both: topLeft = .mergedBubble case .Right: topLeft = .none(tail: false) @@ -1759,7 +1818,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode switch status { case .Left: topRight = .none(tail: false) - case .Right: + case .Right, .Both: topRight = .mergedBubble case .None: topRight = .none(tail: false) @@ -1795,7 +1854,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode case let .None(status): if position.contains(.bottom) && position.contains(.left) { switch status { - case .Left: + case .Left, .Both: bottomLeft = .mergedBubble case .Right: bottomLeft = .none(tail: false) @@ -1814,7 +1873,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode switch status { case .Left: bottomRight = .none(tail: false) - case .Right: + case .Right, .Both: bottomRight = .mergedBubble case let .None(tailStatus): if case .Outgoing = tailStatus { @@ -1970,11 +2029,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contentSize.height += totalContentNodesHeight - var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)? + var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)? if let actionButtonsFinalize = actionButtonsFinalize { actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) } + var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)? + if let reactionButtonsFinalize = reactionButtonsFinalize { + reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth) + } + let minimalContentSize: CGSize if hideBackground { minimalContentSize = CGSize(width: 1.0, height: 1.0) @@ -2007,6 +2071,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let bubbleContentWidth = maxContentWidth - layoutConstants.bubble.edgeInset * 2.0 - (layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left) var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height) + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height + } if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { layoutSize.height += actionButtonsSizeAndApply.0.height } @@ -2032,7 +2099,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if headerSize.height.isZero && contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { updatedMergedBottom = .none } - if actionButtonsSizeAndApply != nil { + if actionButtonsSizeAndApply != nil || reactionButtonsSizeAndApply != nil { updatedMergedTop = .fullyMerged } } @@ -2047,6 +2114,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode forwardAuthorSignature: forwardAuthorSignature, accessibilityData: accessibilityData, actionButtonsSizeAndApply: actionButtonsSizeAndApply, + reactionButtonsSizeAndApply: reactionButtonsSizeAndApply, updatedMergedTop: updatedMergedTop, updatedMergedBottom: updatedMergedBottom, hideBackground: hideBackground, @@ -2087,7 +2155,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode forwardSource: Peer?, forwardAuthorSignature: String?, accessibilityData: ChatMessageAccessibilityData, - actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?, + actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?, + reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?, updatedMergedTop: ChatMessageMerge, updatedMergedBottom: ChatMessageMerge, hideBackground: Bool, @@ -2141,7 +2210,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } var forceBackgroundSide = false - if actionButtonsSizeAndApply != nil { + if actionButtonsSizeAndApply != nil || reactionButtonsSizeAndApply != nil { forceBackgroundSide = true } else if case .semanticallyMerged = updatedMergedTop { forceBackgroundSide = true @@ -2632,14 +2701,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview { if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) { - /*strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame) - if let type = strongSelf.backgroundNode.type { - if case .none = type { - } else { - strongSelf.clippingNode.clipsToBounds = true - } - }*/ - animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil) animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil) strongSelf.clippingNode.clipsToBounds = true @@ -2651,6 +2712,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: animation) animation.animator.updateFrame(layer: strongSelf.backgroundWallpaperNode.layer, frame: backgroundFrame, completion: nil) strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: animation.transition) + strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: animation.transition) if let type = strongSelf.backgroundNode.type { var incomingOffset: CGFloat = 0.0 @@ -2669,11 +2731,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } strongSelf.messageAccessibilityArea.frame = backgroundFrame - - /*if let item = strongSelf.item, let shareButtonNode = strongSelf.shareButtonNode { - let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true) - animation.animator.updateFrame(layer: shareButtonNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize), completion: nil) - }*/ } if let shareButtonNode = strongSelf.shareButtonNode { let currentBackgroundFrame = strongSelf.backgroundNode.frame @@ -2681,10 +2738,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode animation.animator.updateFrame(layer: shareButtonNode.layer, frame: CGRect(origin: CGPoint(x: currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize), completion: nil) } } else { - if let _ = strongSelf.backgroundFrameTransition { + /*if let _ = strongSelf.backgroundFrameTransition { strongSelf.animateFrameTransition(1.0, backgroundFrame.size.height) strongSelf.backgroundFrameTransition = nil - } + }*/ strongSelf.messageAccessibilityArea.frame = backgroundFrame if let shareButtonNode = strongSelf.shareButtonNode { let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true) @@ -2717,17 +2774,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.selectionNode?.frame = selectionFrame strongSelf.selectionNode?.updateLayout(size: selectionFrame.size, leftInset: params.leftInset) + var reactionButtonsOffset: CGFloat = 0.0 + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { - var animated = false - if let _ = strongSelf.actionButtonsNode { - if case .System = animation { - animated = true - } - } - let actionButtonsNode = actionButtonsSizeAndApply.1(animated) - let previousFrame = actionButtonsNode.frame + let actionButtonsNode = actionButtonsSizeAndApply.1(animation) let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0) - actionButtonsNode.frame = actionButtonsFrame if actionButtonsNode !== strongSelf.actionButtonsNode { strongSelf.actionButtonsNode = actionButtonsNode actionButtonsNode.buttonPressed = { [weak strongSelf] button in @@ -2741,16 +2792,47 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea) + actionButtonsNode.frame = actionButtonsFrame } else { - if case let .System(duration, _) = animation { - actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: timingFunction) - } + animation.animator.updateFrame(layer: actionButtonsNode.layer, frame: actionButtonsFrame, completion: nil) } + + reactionButtonsOffset += actionButtonsSizeAndApply.0.height } else if let actionButtonsNode = strongSelf.actionButtonsNode { actionButtonsNode.removeFromSupernode() strongSelf.actionButtonsNode = nil } + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) + let reactionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY + reactionButtonsOffset + 4.0), size: reactionButtonsSizeAndApply.0) + if reactionButtonsNode !== strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = reactionButtonsNode + reactionButtonsNode.reactionSelected = { [weak strongSelf] value in + guard let strongSelf = strongSelf, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) + } + reactionButtonsNode.frame = reactionButtonsFrame + strongSelf.addSubnode(reactionButtonsNode) + if animation.isAnimated { + reactionButtonsNode.animateIn(animation: animation) + } + } else { + animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) + } + } else if let reactionButtonsNode = strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = nil + if animation.isAnimated { + reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in + reactionButtonsNode?.removeFromSupernode() + }) + } else { + reactionButtonsNode.removeFromSupernode() + } + } + let previousContextContentFrame = strongSelf.mainContextSourceNode.contentRect strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0) strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect @@ -2876,8 +2958,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode case let .optionalAction(f): f() case let .openContextMenu(tapMessage, selectAll, subFrame): - if canAddMessageReactions(message: tapMessage), let defaultReaction = item.associatedData.defaultReaction { - item.controllerInteraction.updateMessageReaction(tapMessage, defaultReaction) + if canAddMessageReactions(message: tapMessage) { + item.controllerInteraction.updateMessageReaction(tapMessage, .default) } else { item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) } @@ -3713,6 +3795,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } override func targetReactionView(value: String) -> UIView? { + if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { + return result + } for contentNode in self.contentNodes { if let result = contentNode.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift index 25d7ffcf18..2c833524bf 100644 --- a/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageCommentFooterContentNode.swift @@ -105,12 +105,15 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { let displaySeparator: Bool let topOffset: CGFloat + let topSeparatorOffset: CGFloat if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top { displaySeparator = false topOffset = 2.0 + topSeparatorOffset = 0.0 } else { displaySeparator = true - topOffset = 0.0 + topOffset = 2.0 + topSeparatorOffset = 2.0 } return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in @@ -374,7 +377,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode { strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator strongSelf.separatorNode.isHidden = !displaySeparator - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.strokeInsets.left, y: -3.0), size: CGSize(width: boundingWidth - layoutConstants.bubble.strokeInsets.left - layoutConstants.bubble.strokeInsets.right, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.strokeInsets.left, y: -3.0 + topSeparatorOffset), size: CGSize(width: boundingWidth - layoutConstants.bubble.strokeInsets.left - layoutConstants.bubble.strokeInsets.right, height: UIScreenPixel)) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: boundingSize.height)) diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 3dd03120d7..871d909ba8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -46,7 +46,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.updateMessageReaction(item.message, value) + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 65075fd2d4..6315227cd7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -9,6 +9,7 @@ import TelegramPresentationData import AccountContext import AppBundle import ReactionButtonListComponent +import WebPBinding private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { if let _ = layer.animation(forKey: "clockFrameAnimation") { @@ -45,59 +46,91 @@ private let reactionCountFont = Font.semibold(11.0) private let reactionFont = Font.regular(12.0) private final class StatusReactionNode: ASDisplayNode { - let selectedImageNode: ASImageNode + let iconView: UIImageView + + private let iconImageDisposable = MetaDisposable() private var theme: PresentationTheme? private var value: String? private var isSelected: Bool? override init() { - self.selectedImageNode = ASImageNode() - self.selectedImageNode.displaysAsynchronously = false + self.iconView = UIImageView() super.init() - self.addSubnode(self.selectedImageNode) + self.view.addSubview(self.iconView) } - func update(type: ChatMessageDateAndStatusType, value: String, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) { + deinit { + self.iconImageDisposable.dispose() + } + + func update(context: AccountContext, type: ChatMessageDateAndStatusType, value: String, file: TelegramMediaFile?, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) { if self.value != value { self.value = value - let selectedImage: UIImage? = generateImage(CGSize(width: 14.0, height: 14.0), rotatedContext: { size, context in - UIGraphicsPushContext(context) - - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.scaleBy(x: size.width / 20.0, y: size.width / 20.0) - - let string = NSAttributedString(string: value, font: reactionFont, textColor: .black) - string.draw(at: CGPoint(x: 1.0, y: 2.0)) - - UIGraphicsPopContext() - }) - - if let selectedImage = selectedImage { - self.selectedImageNode.image = selectedImage - self.selectedImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: selectedImage.size) + let defaultImageSize = CGSize(width: 19.0, height: 19.0) + let imageSize: CGSize + if let file = file { + self.iconImageDisposable.set((context.account.postbox.mediaBox.resourceData(file.resource) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + + if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = WebP.convert(fromWebP: dataValue) { + strongSelf.iconView.image = image + } + } + })) + imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize + } else { + imageSize = defaultImageSize } + + self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((defaultImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((defaultImageSize.height - imageSize.height) / 2.0)), size: imageSize) } } } - class ChatMessageDateAndStatusNode: ASDisplayNode { - struct ReactionSettings { + struct TrailingReactionSettings { + var displayInline: Bool var preferAdditionalInset: Bool - init(preferAdditionalInset: Bool) { + init(displayInline: Bool, preferAdditionalInset: Bool) { + self.displayInline = displayInline self.preferAdditionalInset = preferAdditionalInset } } + struct StandaloneReactionSettings { + init() { + } + } + enum LayoutInput { - case trailingContent(contentWidth: CGFloat, reactionSettings: ReactionSettings?) - case standalone + case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?) + case standalone(reactionSettings: StandaloneReactionSettings?) + + var displayInlineReactions: Bool { + switch self { + case let .trailingContent(_, reactionSettings): + if let reactionSettings = reactionSettings { + return reactionSettings.displayInline + } else { + return false + } + case let .standalone(reactionSettings): + if let _ = reactionSettings { + return true + } else { + return false + } + } + } } struct Arguments { @@ -154,7 +187,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private var clockMinNode: ASImageNode? private let dateNode: TextNode private var impressionIcon: ASImageNode? - private var reactionNodes: [StatusReactionNode] = [] + private var reactionNodes: [String: StatusReactionNode] = [:] private let reactionButtonsContainer = ReactionButtonsLayoutContainer() private var reactionCountNode: TextNode? private var reactionButtonNode: HighlightTrackingButtonNode? @@ -247,22 +280,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { switch arguments.type { case .BubbleIncoming, .ImageIncoming, .FreeIncoming: reactionColors = ReactionButtonComponent.Colors( - background: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, - foreground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, - stroke: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb + deselectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, + selectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb, + deselectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, + selectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb ) case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing: reactionColors = ReactionButtonComponent.Colors( - background: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, - foreground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, - stroke: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb + deselectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, + selectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb, + deselectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, + selectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb ) } switch arguments.type { case .BubbleIncoming: dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor - leftInset = 10.0 + leftInset = 5.0 loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize) loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockBubbleIncomingFrameImage @@ -278,7 +313,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { case let .BubbleOutgoing(status): dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor outgoingStatus = status - leftInset = 10.0 + leftInset = 5.0 loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize) loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize) clockFrameImage = graphics.clockBubbleOutgoingFrameImage @@ -524,13 +559,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? - let reactionSize: CGFloat = 14.0 + let reactionSize: CGFloat = 19.0 var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? - let reactionSpacing: CGFloat = -4.0 - let reactionTrailingSpacing: CGFloat = 4.0 + let reactionSpacing: CGFloat = 2.0 + let reactionTrailingSpacing: CGFloat = 6.0 var reactionInset: CGFloat = 0.0 - if !"".isEmpty && !arguments.reactions.isEmpty { + if arguments.layoutInput.displayInlineReactions, !arguments.reactions.isEmpty { reactionInset = -1.0 + CGFloat(arguments.reactions.count) * reactionSize + CGFloat(arguments.reactions.count - 1) * reactionSpacing + reactionTrailingSpacing var count = 0 @@ -547,9 +582,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { countString = "\(count)" } - let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) - reactionInset += max(10.0, layoutAndApply.0.size.width) + 2.0 - reactionCountLayoutAndApply = layoutAndApply + if count > arguments.reactions.count { + let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) + reactionInset += layoutAndApply.0.size.width + 4.0 + reactionCountLayoutAndApply = layoutAndApply + } } if arguments.replyCount > 0 { @@ -599,7 +636,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { transition: .immediate ) case let .trailingContent(contentWidth, reactionSettings): - if let _ = reactionSettings { + if let reactionSettings = reactionSettings, !reactionSettings.displayInline { reactionButtons = reactionButtonsContainer.update( context: arguments.context, action: { value in @@ -686,11 +723,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { resultingHeight = 0.0 } } else { + var additionalVerticalInset: CGFloat = 0.0 if let reactionSettings = reactionSettings { if reactionSettings.preferAdditionalInset { - verticalReactionsInset = 5.0 + verticalReactionsInset = 8.0 + additionalVerticalInset += 1.0 } else { - verticalReactionsInset = 2.0 + verticalReactionsInset = 3.0 } } else { verticalReactionsInset = 0.0 @@ -698,12 +737,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if currentRowWidth + layoutSize.width > arguments.constrainedSize.width { resultingWidth = max(layoutSize.width, reactionButtonsSize.width) - resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height - verticalInset = verticalReactionsInset + reactionButtonsSize.height + resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0 + layoutSize.height + verticalInset = verticalReactionsInset + reactionButtonsSize.height + 3.0 } else { resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width) - verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height - resultingHeight = verticalReactionsInset + reactionButtonsSize.height + verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height + additionalVerticalInset + resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0 } } } @@ -717,7 +756,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.type = arguments.type strongSelf.layoutSize = layoutSize - var reactionButtonPosition = CGPoint(x: 0.0, y: verticalReactionsInset) + var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset) for item in reactionButtons.items { if reactionButtonPosition.x + item.size.width > boundingWidth { reactionButtonPosition.x = 0.0 @@ -773,7 +812,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate) animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil) - blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: animation.transition) + blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, animator: animation.animator) } else { let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1) strongSelf.blurredBackgroundNode = blurredBackgroundNode @@ -789,6 +828,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let _ = dateApply() if let currentImpressionIcon = currentImpressionIcon { + let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize) currentImpressionIcon.displaysAsynchronously = false if currentImpressionIcon.image !== impressionImage { currentImpressionIcon.image = impressionImage @@ -796,8 +836,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if currentImpressionIcon.supernode == nil { strongSelf.impressionIcon = currentImpressionIcon strongSelf.addSubnode(currentImpressionIcon) + currentImpressionIcon.frame = impressionIconFrame + } else { + animation.animator.updateFrame(layer: currentImpressionIcon.layer, frame: impressionIconFrame, completion: nil) } - currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize) } else if let impressionIcon = strongSelf.impressionIcon { impressionIcon.removeFromSupernode() strongSelf.impressionIcon = nil @@ -908,38 +950,49 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left - if !"".isEmpty { - for i in 0 ..< arguments.reactions.count { + if arguments.layoutInput.displayInlineReactions { + var validReactions = Set() + for reaction in arguments.reactions.sorted(by: { lhs, rhs in + if lhs.isSelected != rhs.isSelected { + if lhs.isSelected { + return true + } else { + return false + } + } else { + return lhs.value < rhs.value + } + }) { let node: StatusReactionNode var animateNode = true - if strongSelf.reactionNodes.count > i { - node = strongSelf.reactionNodes[i] + if let current = strongSelf.reactionNodes[reaction.value] { + node = current } else { animateNode = false node = StatusReactionNode() - if strongSelf.reactionNodes.count > i { - let previousNode = strongSelf.reactionNodes[i] - if animation.isAnimated { - previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in - previousNode?.removeFromSupernode() - }) - } else { - previousNode.removeFromSupernode() + strongSelf.reactionNodes[reaction.value] = node + } + validReactions.insert(reaction.value) + + var iconFile: TelegramMediaFile? + + if let availableReactions = arguments.availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction.value { + iconFile = availableReaction.staticIcon + break } - strongSelf.reactionNodes[i] = node - } else { - strongSelf.reactionNodes.append(node) } } - node.update(type: arguments.type, value: arguments.reactions[i].value, isSelected: arguments.reactions[i].isSelected, count: Int(arguments.reactions[i].count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false) + node.update(context: arguments.context, type: arguments.type, value: reaction.value, file: iconFile, isSelected: reaction.isSelected, count: Int(reaction.count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false) if node.supernode == nil { strongSelf.addSubnode(node) if animation.isAnimated { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + verticalInset + 1.0), size: CGSize(width: reactionSize, height: reactionSize)) + let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + verticalInset - 2.0), size: CGSize(width: reactionSize, height: reactionSize)) if animateNode { animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil) } else { @@ -950,18 +1003,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if !arguments.reactions.isEmpty { reactionOffset += reactionTrailingSpacing } - - for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count { - let node = strongSelf.reactionNodes.removeLast() - if animation.isAnimated { - node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) - node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in - node?.removeFromSupernode() - }) - } else { - node.removeFromSupernode() + + var removeIds: [String] = [] + for (id, node) in strongSelf.reactionNodes { + if !validReactions.contains(id) { + removeIds.append(id) + if animation.isAnimated { + node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() + }) + } else { + node.removeFromSupernode() + } } } + for id in removeIds { + strongSelf.reactionNodes.removeValue(forKey: id) + } } if let (layout, apply) = reactionCountLayoutAndApply { @@ -974,7 +1033,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } - let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) + let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset - 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil) reactionOffset += 1.0 + layout.size.width + 4.0 } else if let reactionCountNode = strongSelf.reactionCountNode { @@ -1068,6 +1127,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } func reactionView(value: String) -> UIView? { + for (id, node) in self.reactionNodes { + if id == value { + return node.iconView + } + } for (_, button) in self.reactionButtonsContainer.buttons { if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View { return result.iconView @@ -1092,3 +1156,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return nil } } + +func shouldDisplayInlineDateReactions(message: Message) -> Bool { + if message.id.peerId.namespace == Namespaces.Peer.CloudUser || message.id.peerId.namespace == Namespaces.Peer.SecretChat { + return true + } + return false +} diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 9920350a64..935f6aa1b1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -61,7 +61,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.updateMessageReaction(item.message, value) + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 468bcec162..8b8447208f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -50,6 +50,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD private var replyBackgroundNode: NavigationBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? + private var reactionButtonsNode: ChatMessageReactionButtonsNode? private let messageAccessibilityArea: AccessibilityAreaNode @@ -255,6 +256,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let currentForwardBackgroundNode = self.forwardBackgroundNode let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) + let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode) let currentItem = self.appliedItem let currentForwardInfo = self.appliedForwardInfo @@ -527,22 +529,54 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } var maxContentWidth = normalDisplaySize.width - var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? + var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } - var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)? + var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)? if let actionButtonsFinalize = actionButtonsFinalize { actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) } + let reactions: ReactionsMessageAttribute + if shouldDisplayInlineDateReactions(message: item.message) { + reactions = ReactionsMessageAttribute(reactions: [], recentPeers: []) + } else { + reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: []) + } + + var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? + if !reactions.reactions.isEmpty { + let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right + + let maxReactionsWidth = params.width - totalInset + let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( + context: item.context, + presentationData: item.presentationData, + availableReactions: item.associatedData.availableReactions, + reactions: reactions, + isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), + constrainedWidth: maxReactionsWidth + )) + maxContentWidth = max(maxContentWidth, minWidth) + reactionButtonsFinalize = buttonsLayout + } + + var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)? + if let reactionButtonsFinalize = reactionButtonsFinalize { + reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth) + } + var layoutSize = CGSize(width: params.width, height: videoLayout.contentSize.height) if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { layoutSize.height += actionButtonsSizeAndApply.0.height } + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + layoutSize.height += 6.0 + reactionButtonsSizeAndApply.0.height + } return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in if let strongSelf = self { @@ -577,7 +611,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let animating = (currentItem != nil && currentPlaying != isPlaying) || strongSelf.animatingHeight if !animating { strongSelf.interactiveVideoNode.frame = videoFrame - videoApply(videoLayoutData, transition) + videoApply(videoLayoutData, animation) } if currentPlaying != isPlaying { @@ -750,14 +784,38 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { - var animated = false - if let _ = strongSelf.actionButtonsNode { - if case .System = animation { - animated = true + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) + let reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSizeAndApply.0) + if reactionButtonsNode !== strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = reactionButtonsNode + reactionButtonsNode.reactionSelected = { value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.frame = reactionButtonsFrame + strongSelf.addSubnode(reactionButtonsNode) + if animation.isAnimated { + reactionButtonsNode.animateIn(animation: animation) + } + } else { + animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) } - let actionButtonsNode = actionButtonsSizeAndApply.1(animated) + } else if let reactionButtonsNode = strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = nil + if animation.isAnimated { + reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in + reactionButtonsNode?.removeFromSupernode() + }) + } else { + reactionButtonsNode.removeFromSupernode() + } + } + + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + let actionButtonsNode = actionButtonsSizeAndApply.1(animation) let previousFrame = actionButtonsNode.frame let actionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY), size: actionButtonsSizeAndApply.0) actionButtonsNode.frame = actionButtonsFrame @@ -784,6 +842,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD strongSelf.actionButtonsNode = nil } } + + if let (_, f) = strongSelf.awaitingAppliedReaction { + strongSelf.awaitingAppliedReaction = nil + + f() + } } }) } @@ -1196,7 +1260,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } else { videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0) } - videoApply(videoLayoutData, .immediate) + videoApply(videoLayoutData, .None) if let shareButtonNode = self.shareButtonNode { let buttonSize = shareButtonNode.frame.size @@ -1247,6 +1311,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } override func targetReactionView(value: String) -> UIView? { + if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { + return result + } if !self.interactiveVideoNode.dateAndStatusNode.isHidden { return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 484d0b4306..3bde6244d4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -455,7 +455,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)), + layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: !shouldDisplayInlineDateReactions(message: message))), constrainedSize: constrainedSize, availableReactions: associatedData.availableReactions, reactions: dateReactions, @@ -1132,8 +1132,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) { - return result + if self.dateAndStatusNode.supernode != nil { + if let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) { + return result + } + if !self.dateAndStatusNode.frame.height.isZero { + if self.dateAndStatusNode.frame.contains(point) { + return nil + } + } } return super.hitTest(point, with: event) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 71a361faf4..bf2ada8be6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -132,7 +132,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.view.addGestureRecognizer(recognizer) } - func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) { + func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void) { let previousFile = self.media let currentItem = self.item @@ -296,7 +296,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone, + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactions, @@ -321,7 +321,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let result = ChatMessageInstantVideoItemLayoutResult(contentSize: contentSize, overflowLeft: 0.0, overflowRight: dateAndStatusOverflow ? 0.0 : (max(0.0, floorToScreenPixels(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width))) - return (result, { [weak self] layoutData, transition in + return (result, { [weak self] layoutData, animation in if let strongSelf = self { strongSelf.item = item strongSelf.videoFrame = displayVideoFrame @@ -362,18 +362,18 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - dateAndStatusApply(.None) + dateAndStatusApply(animation) switch layoutData { - case let .unconstrained(width): - let dateAndStatusOrigin: CGPoint - if dateAndStatusOverflow { - dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0) - } else { - dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height) - } - strongSelf.dateAndStatusNode.frame = CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize) - case let .constrained(_, right): - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + case let .unconstrained(width): + let dateAndStatusOrigin: CGPoint + if dateAndStatusOverflow { + dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0) + } else { + dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height) + } + animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil) + case let .constrained(_, right): + animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize), completion: nil) } var updatedPlayerStatusSignal: Signal? @@ -847,11 +847,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return nil } - static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode) { + static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode) { let makeLayout = node?.asyncLayout() return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload in var createdNode: ChatMessageInteractiveInstantVideoNode? - let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) + let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void) if let makeLayout = makeLayout { sizeAndApplyLayout = makeLayout(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload) } else { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index cd07dec56c..84318d1ec8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -91,7 +91,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } let dateAndStatusNode: ChatMessageDateAndStatusNode private var badgeNode: ChatMessageInteractiveMediaBadge? - private var tapRecognizer: UITapGestureRecognizer? + private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? private var context: AccountContext? private var message: Message? @@ -150,6 +150,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } var activatePinch: ((PinchSourceContainerNode) -> Void)? + var updateMessageReaction: ((Message, ChatControllerInteractionReaction) -> Void)? override init() { self.pinchContainerNode = PinchSourceContainerNode() @@ -267,9 +268,18 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio override func didLoad() { super.didLoad() - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:))) - self.imageNode.view.addGestureRecognizer(tapRecognizer) - self.tapRecognizer = tapRecognizer + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.imageTap(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + guard let strongSelf = self else { + return .fail + } + if !strongSelf.imageNode.bounds.contains(point) { + return .fail + } + return .waitForDoubleTap + } + self.imageNode.view.addGestureRecognizer(recognizer) + self.tapRecognizer = recognizer } private func progressPressed(canActivate: Bool) { @@ -320,26 +330,33 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - @objc func imageTap(_ recognizer: UITapGestureRecognizer) { + @objc func imageTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if case .ended = recognizer.state { - let point = recognizer.location(in: self.imageNode.view) - if let _ = self.attributes?.updatingMedia { - if let statusNode = self.statusNode, statusNode.frame.contains(point) { - self.progressPressed(canActivate: true) - } - } else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { - var videoContentMatch = true - if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId { - videoContentMatch = self.message?.stableId == stableId && self.media?.id == mediaId - } - self.activateLocalContent((self.automaticPlayback ?? false) && videoContentMatch ? .automaticPlayback : .default) - } else { - if let message = self.message, message.flags.isSending { - if let statusNode = self.statusNode, statusNode.frame.contains(point) { - self.progressPressed(canActivate: true) + if let (gesture, point) = recognizer.lastRecognizedGestureAndLocation, let message = self.message { + if case .doubleTap = gesture { + if canAddMessageReactions(message: message) { + self.updateMessageReaction?(message, .default) } } else { - self.progressPressed(canActivate: true) + if let _ = self.attributes?.updatingMedia { + if let statusNode = self.statusNode, statusNode.frame.contains(point) { + self.progressPressed(canActivate: true) + } + } else if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { + var videoContentMatch = true + if let content = self.videoContent, case let .message(stableId, mediaId) = content.nativeId { + videoContentMatch = self.message?.stableId == stableId && self.media?.id == mediaId + } + self.activateLocalContent((self.automaticPlayback ?? false) && videoContentMatch ? .automaticPlayback : .default) + } else { + if let message = self.message, message.flags.isSending { + if let statusNode = self.statusNode, statusNode.frame.contains(point) { + self.progressPressed(canActivate: true) + } + } else { + self.progressPressed(canActivate: true) + } + } } } } @@ -475,7 +492,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio impressionCount: dateAndStatus.viewCount, dateText: dateAndStatus.dateText, type: dateAndStatus.type, - layoutInput: .standalone, + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), availableReactions: associatedData.availableReactions, reactions: dateAndStatus.dateReactions, @@ -868,15 +885,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio imageApply() if let statusApply = statusApply { + let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) if strongSelf.dateAndStatusNode.supernode == nil { strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode) + statusApply(.None) + strongSelf.dateAndStatusNode.frame = dateAndStatusFrame + } else { + transition.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) + statusApply(transition) } - statusApply(transition) - - let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) - - strongSelf.dateAndStatusNode.frame = dateAndStatusFrame - strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() } diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 8fa79f911c..ee08023fe9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -252,7 +252,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone, + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactions, diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index 98cf345e1d..ec7aa4bac3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -51,6 +51,13 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } } + + self.interactiveImageNode.updateMessageReaction = { [weak self] message, value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(message, value) + } self.interactiveImageNode.activatePinch = { [weak self] sourceNode in guard let strongSelf = self, let _ = strongSelf.item else { diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index 03b9875778..6c9c6c6238 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1072,7 +1072,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: 100.0, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)), + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, reactions: dateReactions, diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 0a5c42f90a..95bc1fea51 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -19,6 +19,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { case freeform } + enum DisplayAlignment { + case left + case right + } + private let container: ReactionButtonsLayoutContainer var reactionSelected: ((String) -> Void)? @@ -33,22 +38,32 @@ final class MessageReactionButtonsNode: ASDisplayNode { presentationData: ChatPresentationData, availableReactions: AvailableReactions?, reactions: ReactionsMessageAttribute, + alignment: DisplayAlignment, constrainedWidth: CGFloat, type: DisplayType ) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) { let reactionColors: ReactionButtonComponent.Colors switch type { - case .incoming, .freeform: + case .incoming: reactionColors = ReactionButtonComponent.Colors( - background: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, - foreground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, - stroke: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb + deselectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, + selectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb, + deselectedForeground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, + selectedForeground: presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb ) case .outgoing: reactionColors = ReactionButtonComponent.Colors( - background: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, - foreground: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, - stroke: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb + deselectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, + selectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb, + deselectedForeground: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, + selectedForeground: presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb + ) + case .freeform: + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, + selectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, + deselectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb, + selectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb ) } @@ -115,29 +130,53 @@ final class MessageReactionButtonsNode: ASDisplayNode { let bottomInset: CGFloat = 2.0 return (proposedWidth: reactionButtonsSize.width, continueLayout: { [weak self] boundingWidth in - return (size: CGSize(width: boundingWidth, height: topInset + reactionButtonsSize.height + bottomInset), apply: { animation in + let size = CGSize(width: boundingWidth, height: topInset + reactionButtonsSize.height + bottomInset) + return (size: size, apply: { animation in guard let strongSelf = self else { return } - var reactionButtonPosition = CGPoint(x: 0.0, y: topInset) + var reactionButtonPosition: CGPoint + switch alignment { + case .left: + reactionButtonPosition = CGPoint(x: -1.0, y: topInset) + case .right: + reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset) + } for item in reactionButtons.items { - if reactionButtonPosition.x + item.size.width > boundingWidth { - reactionButtonPosition.x = 0.0 - reactionButtonPosition.y += item.size.height + 6.0 + switch alignment { + case .left: + if reactionButtonPosition.x + item.size.width > boundingWidth { + reactionButtonPosition.x = 0.0 + reactionButtonPosition.y += item.size.height + 6.0 + } + case .right: + if reactionButtonPosition.x - item.size.width < -1.0 { + reactionButtonPosition.x = size.width + 1.0 + reactionButtonPosition.y += item.size.height + 6.0 + } + } + + let itemFrame: CGRect + switch alignment { + case .left: + itemFrame = CGRect(origin: reactionButtonPosition, size: item.size) + reactionButtonPosition.x += item.size.width + 6.0 + case .right: + itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size) + reactionButtonPosition.x -= item.size.width + 6.0 } if item.view.superview == nil { strongSelf.view.addSubview(item.view) - item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size) if animation.isAnimated { item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + item.view.frame = itemFrame } else { - animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil) + animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil) } - reactionButtonPosition.x += item.size.width + 6.0 } for view in reactionButtons.removedViews { @@ -163,9 +202,15 @@ final class MessageReactionButtonsNode: ASDisplayNode { return nil } - func animateOut() { + func animateIn(animation: ListViewItemUpdateAnimation) { for (_, button) in self.container.buttons { - button.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + animation.animator.animateScale(layer: button.layer, from: 0.01, to: 1.0, completion: nil) + } + } + + func animateOut(animation: ListViewItemUpdateAnimation) { + for (_, button) in self.container.buttons { + animation.animator.updateScale(layer: button.layer, scale: 0.01, completion: nil) } } } @@ -184,7 +229,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.updateMessageReaction(item.message, value) + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } } @@ -202,7 +247,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode let topOffset: CGFloat if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top { //displaySeparator = false - topOffset = 2.0 + topOffset = 4.0 } else { //displaySeparator = true topOffset = 0.0 @@ -213,7 +258,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode let buttonsUpdate = buttonsNode.prepareUpdate( context: item.context, presentationData: item.presentationData, - availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, constrainedWidth: constrainedSize.width, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing) + availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, alignment: .left, constrainedWidth: constrainedSize.width, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing) return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in var boundingSize = CGSize() @@ -250,7 +295,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.buttonsNode.animateOut() + self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false))) } override func animateInsertionIntoBubble(_ duration: Double) { @@ -263,18 +308,18 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in completion() }) - self.buttonsNode.animateOut() + self.buttonsNode.animateOut(animation: ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .spring, interactive: false))) } override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - if self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil) != nil { + if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil), result !== self.buttonsNode.view { return .ignore } return .none } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event) { + if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view { return result } return nil @@ -284,3 +329,96 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode return self.buttonsNode.reactionTargetView(value: value) } } + +final class ChatMessageReactionButtonsNode: ASDisplayNode { + final class Arguments { + let context: AccountContext + let presentationData: ChatPresentationData + let availableReactions: AvailableReactions? + let reactions: ReactionsMessageAttribute + let isIncoming: Bool + let constrainedWidth: CGFloat + + init( + context: AccountContext, + presentationData: ChatPresentationData, + availableReactions: AvailableReactions?, + reactions: ReactionsMessageAttribute, + isIncoming: Bool, + constrainedWidth: CGFloat + ) { + self.context = context + self.presentationData = presentationData + self.availableReactions = availableReactions + self.reactions = reactions + self.isIncoming = isIncoming + self.constrainedWidth = constrainedWidth + } + } + + private let buttonsNode: MessageReactionButtonsNode + + var reactionSelected: ((String) -> Void)? + + override init() { + self.buttonsNode = MessageReactionButtonsNode() + + super.init() + + self.addSubnode(self.buttonsNode) + self.buttonsNode.reactionSelected = { [weak self] value in + self?.reactionSelected?(value) + } + } + + class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) { + return { arguments in + let node = maybeNode ?? ChatMessageReactionButtonsNode() + + let buttonsUpdate = node.buttonsNode.prepareUpdate( + context: arguments.context, + presentationData: arguments.presentationData, + availableReactions: arguments.availableReactions, + reactions: arguments.reactions, + alignment: arguments.isIncoming ? .left : .right, + constrainedWidth: arguments.constrainedWidth, + type: .freeform + ) + + return (buttonsUpdate.proposedWidth, { constrainedWidth in + let buttonsResult = buttonsUpdate.continueLayout(constrainedWidth) + + return (CGSize(width: constrainedWidth, height: buttonsResult.size.height), { animation in + node.buttonsNode.frame = CGRect(origin: CGPoint(), size: buttonsResult.size) + buttonsResult.apply(animation) + + return node + }) + }) + } + } + + func animateIn(animation: ListViewItemUpdateAnimation) { + self.buttonsNode.animateIn(animation: animation) + self.buttonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + func animateOut(animation: ListViewItemUpdateAnimation, completion: @escaping () -> Void) { + self.buttonsNode.animateOut(animation: animation) + animation.animator.updateAlpha(layer: self.buttonsNode.layer, alpha: 0.0, completion: { _ in + completion() + }) + animation.animator.updateFrame(layer: self.buttonsNode.layer, frame: self.buttonsNode.layer.frame.offsetBy(dx: 0.0, dy: -self.buttonsNode.layer.bounds.height / 2.0), completion: nil) + } + + func reactionTargetView(value: String) -> UIView? { + return self.buttonsNode.reactionTargetView(value: value) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: event), result !== self.buttonsNode.view { + return result + } + return nil + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 7975bb5cf0..db3defa635 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -120,7 +120,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)), + layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: false)), constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, reactions: dateReactions, diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 1a026b098f..0224ea8e4a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -45,6 +45,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var replyBackgroundNode: NavigationBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? + private var reactionButtonsNode: ChatMessageReactionButtonsNode? private let messageAccessibilityArea: AccessibilityAreaNode @@ -178,7 +179,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } } - return .waitForSingleTap + return .waitForDoubleTap } recognizer.longTap = { [weak self] point, recognizer in guard let strongSelf = self else { @@ -299,6 +300,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let imageLayout = self.imageNode.asyncLayout() let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) + let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode) let textLayout = TextNode.asyncLayout(self.textNode) let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) @@ -490,7 +492,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .standalone, + layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, reactions: dateReactions, @@ -619,22 +621,54 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } var maxContentWidth = imageSize.width - var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? + var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } - var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)? + var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)? if let actionButtonsFinalize = actionButtonsFinalize { actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) } + let reactions: ReactionsMessageAttribute + if shouldDisplayInlineDateReactions(message: item.message) { + reactions = ReactionsMessageAttribute(reactions: [], recentPeers: []) + } else { + reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(reactions: [], recentPeers: []) + } + + var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? + if !reactions.reactions.isEmpty { + let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right + + let maxReactionsWidth = params.width - totalInset + let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( + context: item.context, + presentationData: item.presentationData, + availableReactions: item.associatedData.availableReactions, + reactions: reactions, + isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), + constrainedWidth: maxReactionsWidth + )) + maxContentWidth = max(maxContentWidth, minWidth) + reactionButtonsFinalize = buttonsLayout + } + + var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)? + if let reactionButtonsFinalize = reactionButtonsFinalize { + reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth) + } + var layoutSize = CGSize(width: params.width, height: contentHeight) if isEmoji && !incoming { layoutSize.height += dateAndStatusSize.height } + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + layoutSize.height += reactionButtonsSizeAndApply.0.height + } if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { layoutSize.height += actionButtonsSizeAndApply.0.height } @@ -740,9 +774,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect - dateAndStatusApply(.None) - - transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: dateAndStatusFrame) + animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) + dateAndStatusApply(animation) if let updatedShareButtonNode = updatedShareButtonNode { if updatedShareButtonNode !== strongSelf.shareButtonNode { @@ -902,13 +935,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { - var animated = false - if let _ = strongSelf.actionButtonsNode { - if case .System = animation { - animated = true - } - } - let actionButtonsNode = actionButtonsSizeAndApply.1(animated) + let actionButtonsNode = actionButtonsSizeAndApply.1(animation) let previousFrame = actionButtonsNode.frame let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: actionButtonsSizeAndApply.0) actionButtonsNode.frame = actionButtonsFrame @@ -935,6 +962,36 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.actionButtonsNode = nil } + if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { + let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) + let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: reactionButtonsSizeAndApply.0) + if reactionButtonsNode !== strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = reactionButtonsNode + reactionButtonsNode.reactionSelected = { value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) + } + reactionButtonsNode.frame = reactionButtonsFrame + strongSelf.addSubnode(reactionButtonsNode) + if animation.isAnimated { + reactionButtonsNode.animateIn(animation: animation) + } + } else { + animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) + } + } else if let reactionButtonsNode = strongSelf.reactionButtonsNode { + strongSelf.reactionButtonsNode = nil + if animation.isAnimated { + reactionButtonsNode.animateOut(animation: animation, completion: { [weak reactionButtonsNode] in + reactionButtonsNode?.removeFromSupernode() + }) + } else { + reactionButtonsNode.removeFromSupernode() + } + } + if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { strongSelf.dateAndStatusNode.pressed = { guard let strongSelf = self else { @@ -963,7 +1020,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if case .doubleTap = gesture { self.containerNode.cancelGesture() } - if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: nil) { + if let item = self.item, let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: nil) { if case .doubleTap = gesture { self.containerNode.cancelGesture() } @@ -973,7 +1030,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView { case let .optionalAction(f): f() case let .openContextMenu(tapMessage, selectAll, subFrame): - self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) + if canAddMessageReactions(message: item.message) { + item.controllerInteraction.updateMessageReaction(tapMessage, .default) + } else { + item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) + } } } else if case .tap = gesture { self.item?.controllerInteraction.clickThroughMessage() @@ -1189,6 +1250,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return shareButtonNode.view } + if let reactionButtonsNode = self.reactionButtonsNode { + if let result = reactionButtonsNode.hitTest(self.view.convert(point, to: reactionButtonsNode.view), with: event) { + return result + } + } + return super.hitTest(point, with: event) } @@ -1496,6 +1563,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } override func targetReactionView(value: String) -> UIView? { + if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { + return result + } if !self.dateAndStatusNode.isHidden { return self.dateAndStatusNode.reactionView(value: value) } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index a320abae7e..25f9b9c690 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -74,7 +74,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.updateMessageReaction(item.message, value) + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } } @@ -292,6 +292,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { isReplyThread = true } + let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput + dateLayoutInput = .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message), preferAdditionalInset: false)) + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: item.context, presentationData: item.presentationData, @@ -299,7 +302,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)), + layoutInput: dateLayoutInput, constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, reactions: dateReactions, diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 8192563e04..365e263cf5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -544,9 +544,6 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } override func reactionTargetView(value: String) -> UIView? { - if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionView(value: value) - } - return nil + return self.contentNode.reactionTargetView(value: value) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 4166f86f60..6eee314d7d 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, transition: ControlledTransition(duration: 0.2, curve: .spring)), completion: { layout, apply in + messageItem.updateNode(async: { f in f() }, node: { return current }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring, interactive: false)), completion: { layout, apply in current.contentSize = layout.contentSize current.insets = layout.insets @@ -972,7 +972,7 @@ private final class ItemView: UIView, SparseItemGridView { func update(size: CGSize, insets: UIEdgeInsets) { if let messageItem = self.messageItem, let messageItemNode = self.messageItemNode { - messageItem.updateNode(async: { f in f() }, node: { return messageItemNode }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring)), completion: { layout, apply in + messageItem.updateNode(async: { f in f() }, node: { return messageItemNode }, params: ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2, transition: ControlledTransition(duration: 0.2, curve: .spring, interactive: false)), completion: { layout, apply in messageItemNode.contentSize = layout.contentSize messageItemNode.insets = layout.insets