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