mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reaction improvements
This commit is contained in:
parent
2397f3c5b1
commit
c58b8c33c4
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -39,18 +39,21 @@ public final class ReactionButtonComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct Colors: Equatable {
|
public struct Colors: Equatable {
|
||||||
public var background: UInt32
|
public var deselectedBackground: UInt32
|
||||||
public var foreground: UInt32
|
public var selectedBackground: UInt32
|
||||||
public var stroke: UInt32
|
public var deselectedForeground: UInt32
|
||||||
|
public var selectedForeground: UInt32
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
background: UInt32,
|
deselectedBackground: UInt32,
|
||||||
foreground: UInt32,
|
selectedBackground: UInt32,
|
||||||
stroke: UInt32
|
deselectedForeground: UInt32,
|
||||||
|
selectedForeground: UInt32
|
||||||
) {
|
) {
|
||||||
self.background = background
|
self.deselectedBackground = deselectedBackground
|
||||||
self.foreground = foreground
|
self.selectedBackground = selectedBackground
|
||||||
self.stroke = stroke
|
self.deselectedForeground = deselectedForeground
|
||||||
|
self.selectedForeground = selectedForeground
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +102,7 @@ public final class ReactionButtonComponent: Component {
|
|||||||
public final class View: UIButton, ComponentTaggedView {
|
public final class View: UIButton, ComponentTaggedView {
|
||||||
public let iconView: UIImageView
|
public let iconView: UIImageView
|
||||||
private let textView: ComponentHostView<Empty>
|
private let textView: ComponentHostView<Empty>
|
||||||
|
private let measureTextView: ComponentHostView<Empty>
|
||||||
|
|
||||||
private var currentComponent: ReactionButtonComponent?
|
private var currentComponent: ReactionButtonComponent?
|
||||||
|
|
||||||
@ -111,6 +115,9 @@ public final class ReactionButtonComponent: Component {
|
|||||||
self.textView = ComponentHostView<Empty>()
|
self.textView = ComponentHostView<Empty>()
|
||||||
self.textView.isUserInteractionEnabled = false
|
self.textView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.measureTextView = ComponentHostView<Empty>()
|
||||||
|
self.measureTextView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
super.init(frame: CGRect())
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
self.addSubview(self.iconView)
|
self.addSubview(self.iconView)
|
||||||
@ -148,11 +155,11 @@ public final class ReactionButtonComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func update(component: ReactionButtonComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: ReactionButtonComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
let sideInsets: CGFloat = 10.0
|
let sideInsets: CGFloat = 8.0
|
||||||
let height: CGFloat = 30.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
|
let imageSize: CGSize
|
||||||
if self.currentComponent?.reaction != component.reaction {
|
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)
|
self.iconView.frame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
|
||||||
|
|
||||||
let textSize: CGSize
|
let text = "\(component.count)"
|
||||||
if self.currentComponent?.count != component.count || self.currentComponent?.colors != component.colors {
|
var measureText = ""
|
||||||
textSize = self.textView.update(
|
for _ in 0 ..< text.count {
|
||||||
|
measureText.append("0")
|
||||||
|
}
|
||||||
|
|
||||||
|
let minTextWidth = self.measureTextView.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(Text(
|
component: AnyComponent(Text(
|
||||||
text: "\(component.count)",
|
text: measureText,
|
||||||
font: Font.regular(13.0),
|
font: Font.regular(11.0),
|
||||||
color: UIColor(argb: component.colors.foreground)
|
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: text,
|
||||||
|
font: Font.regular(11.0),
|
||||||
|
color: UIColor(argb: component.isSelected ? component.colors.selectedForeground : component.colors.deselectedForeground)
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
textSize = self.textView.bounds.size
|
actualTextSize = self.textView.bounds.size
|
||||||
}
|
|
||||||
|
|
||||||
if self.currentComponent?.colors != component.colors {
|
|
||||||
self.backgroundColor = UIColor(argb: component.colors.background)
|
|
||||||
}
|
}
|
||||||
|
let layoutTextSize = CGSize(width: max(actualTextSize.width, minTextWidth), height: actualTextSize.height)
|
||||||
|
|
||||||
if self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected {
|
if self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected {
|
||||||
if component.isSelected {
|
if component.isSelected {
|
||||||
self.layer.borderColor = UIColor(argb: component.colors.stroke).cgColor
|
self.backgroundColor = UIColor(argb: component.colors.selectedBackground)
|
||||||
self.layer.borderWidth = 1.5
|
|
||||||
} else {
|
} else {
|
||||||
self.layer.borderColor = nil
|
self.backgroundColor = UIColor(argb: component.colors.deselectedBackground)
|
||||||
self.layer.borderWidth = 0.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.layer.cornerRadius = height / 2.0
|
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
|
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<Empty>] = []
|
var removedViews: [ComponentHostView<Empty>] = []
|
||||||
|
|
||||||
var validIds = Set<String>()
|
var validIds = Set<String>()
|
||||||
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)
|
validIds.insert(reaction.reaction.value)
|
||||||
|
|
||||||
let view: ComponentHostView<Empty>
|
let view: ComponentHostView<Empty>
|
||||||
|
@ -360,6 +360,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
strongSelf.hapticFeedback.tap()
|
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
|
strongSelf.highlightedActionNode = nil
|
||||||
highlightedActionNode.performAction()
|
highlightedActionNode.performAction()
|
||||||
}
|
}
|
||||||
|
if let highlightedReaction = strongSelf.highlightedReaction {
|
||||||
|
strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
||||||
strongSelf.highlightedActionNode = nil
|
strongSelf.highlightedActionNode = nil
|
||||||
@ -417,6 +429,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
strongSelf.hapticFeedback.tap()
|
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
|
strongSelf.highlightedActionNode = nil
|
||||||
highlightedActionNode.performAction()
|
highlightedActionNode.performAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let highlightedReaction = strongSelf.highlightedReaction {
|
||||||
|
strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
||||||
strongSelf.highlightedActionNode = nil
|
strongSelf.highlightedActionNode = nil
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
|
import ObjCRuntimeUtils
|
||||||
|
|
||||||
public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
|
public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
|
||||||
case linear
|
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) {
|
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 {
|
switch self {
|
||||||
case .immediate:
|
case .immediate:
|
||||||
completion?(true)
|
completion?(true)
|
||||||
case let .animated(duration, curve):
|
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)
|
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) {
|
func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||||
let t = view.layer.transform
|
let t = view.layer.transform
|
||||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
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 finishAnimation()
|
||||||
|
|
||||||
func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?)
|
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 updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?)
|
||||||
func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?)
|
func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?)
|
||||||
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?)
|
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?)
|
||||||
|
func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol AnyValueProviding {
|
protocol AnyValueProviding {
|
||||||
@ -1609,87 +1639,46 @@ final class ControlledTransitionProperty {
|
|||||||
|
|
||||||
let layer: CALayer
|
let layer: CALayer
|
||||||
let path: String
|
let path: String
|
||||||
let keyPath: AnyKeyPath
|
|
||||||
var fromValue: AnyValue
|
var fromValue: AnyValue
|
||||||
let toValue: AnyValue
|
let toValue: AnyValue
|
||||||
private(set) var lastValue: AnyValue
|
|
||||||
private let completion: ((Bool) -> Void)?
|
private let completion: ((Bool) -> Void)?
|
||||||
|
|
||||||
private var animator: AnyObject?
|
init<T: Equatable>(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding {
|
||||||
|
|
||||||
init<T: Equatable>(layer: CALayer, path: String, keyPath: ReferenceWritableKeyPath<CALayer, T>, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding {
|
|
||||||
self.layer = layer
|
self.layer = layer
|
||||||
self.path = path
|
self.path = path
|
||||||
self.keyPath = keyPath
|
|
||||||
self.fromValue = fromValue.anyValue
|
self.fromValue = fromValue.anyValue
|
||||||
self.toValue = toValue.anyValue
|
self.toValue = toValue.anyValue
|
||||||
self.lastValue = self.fromValue
|
|
||||||
self.completion = completion
|
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)
|
self.update(at: 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
if #available(iOS 10.0, *) {
|
self.layer.removeAnimation(forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
|
||||||
if let animator = self.animator as? UIViewPropertyAnimator {
|
|
||||||
animator.stopAnimation(true)
|
|
||||||
self.animator = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(at fraction: CGFloat) {
|
func update(at fraction: CGFloat) {
|
||||||
let value = self.fromValue.interpolate(toValue, fraction)
|
let value = self.fromValue.interpolate(toValue, fraction)
|
||||||
self.lastValue = value
|
|
||||||
//self.write(self.layer, value)
|
|
||||||
|
|
||||||
if #available(iOS 10.0, *) {
|
let animation = CABasicAnimation(keyPath: self.path)
|
||||||
if let animator = self.animator as? UIViewPropertyAnimator {
|
|
||||||
animator.fractionComplete = fraction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*let animation = CABasicAnimation()
|
|
||||||
animation.speed = 0.0
|
animation.speed = 0.0
|
||||||
|
animation.beginTime = CACurrentMediaTime() + 1000.0
|
||||||
|
animation.timeOffset = 0.01
|
||||||
animation.duration = 1.0
|
animation.duration = 1.0
|
||||||
animation.fillMode = .both
|
animation.fillMode = .both
|
||||||
animation.fromValue = value.nsValue
|
animation.fromValue = value.nsValue
|
||||||
animation.toValue = self.toValue.nsValue
|
animation.toValue = value.nsValue
|
||||||
animation.keyPath = self.path
|
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||||
if let previousAnimation = self.layer.animation(forKey: self.animationKey) {
|
animation.isRemovedOnCompletion = false
|
||||||
self.layer.removeAnimation(forKey: self.animationKey)
|
self.layer.add(animation, forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
|
||||||
let _ = previousAnimation
|
|
||||||
}
|
|
||||||
self.layer.add(animation, forKey: self.animationKey)*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func complete(atEnd: Bool) {
|
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)
|
self.completion?(atEnd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ControlledTransition {
|
public final class ControlledTransition {
|
||||||
@available(iOS 10.0, *)
|
|
||||||
public final class NativeAnimator: ControlledTransitionAnimator {
|
public final class NativeAnimator: ControlledTransitionAnimator {
|
||||||
public let duration: Double
|
public let duration: Double
|
||||||
private let curve: ContainedViewLayoutTransitionCurve
|
private let curve: ContainedViewLayoutTransitionCurve
|
||||||
@ -1713,7 +1702,7 @@ public final class ControlledTransition {
|
|||||||
for j in 0 ..< other.animations.count {
|
for j in 0 ..< other.animations.count {
|
||||||
let otherAnimation = other.animations[j]
|
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 {
|
if animation.toValue == otherAnimation.toValue {
|
||||||
removeAnimationIndices.append(i)
|
removeAnimationIndices.append(i)
|
||||||
} else {
|
} else {
|
||||||
@ -1723,7 +1712,8 @@ public final class ControlledTransition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for j in removeOtherAnimationIndices.reversed() {
|
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) {
|
private func add(animation: ControlledTransitionProperty) {
|
||||||
for i in 0 ..< self.animations.count {
|
for i in 0 ..< self.animations.count {
|
||||||
let otherAnimation = self.animations[i]
|
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)
|
self.animations.remove(at: i)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -1771,25 +1763,56 @@ public final class ControlledTransition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) {
|
public func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?) {
|
||||||
|
if layer.opacity == Float(alpha) {
|
||||||
|
return
|
||||||
|
}
|
||||||
let fromValue = layer.presentation()?.opacity ?? layer.opacity
|
let fromValue = layer.presentation()?.opacity ?? layer.opacity
|
||||||
//layer.opacity = Float(alpha)
|
layer.opacity = Float(alpha)
|
||||||
self.add(animation: ControlledTransitionProperty(
|
self.add(animation: ControlledTransitionProperty(
|
||||||
layer: layer,
|
layer: layer,
|
||||||
path: "opacity",
|
path: "opacity",
|
||||||
keyPath: \.opacity,
|
|
||||||
fromValue: fromValue,
|
fromValue: fromValue,
|
||||||
toValue: Float(alpha),
|
toValue: Float(alpha),
|
||||||
completion: completion
|
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)?) {
|
public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) {
|
||||||
|
if layer.position == position {
|
||||||
|
return
|
||||||
|
}
|
||||||
let fromValue = layer.presentation()?.position ?? layer.position
|
let fromValue = layer.presentation()?.position ?? layer.position
|
||||||
//layer.position = position
|
layer.position = position
|
||||||
self.add(animation: ControlledTransitionProperty(
|
self.add(animation: ControlledTransitionProperty(
|
||||||
layer: layer,
|
layer: layer,
|
||||||
path: "position",
|
path: "position",
|
||||||
keyPath: \.position,
|
|
||||||
fromValue: fromValue,
|
fromValue: fromValue,
|
||||||
toValue: position,
|
toValue: position,
|
||||||
completion: completion
|
completion: completion
|
||||||
@ -1797,12 +1820,14 @@ public final class ControlledTransition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) {
|
public func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?) {
|
||||||
|
if layer.bounds == bounds {
|
||||||
|
return
|
||||||
|
}
|
||||||
let fromValue = layer.presentation()?.bounds ?? layer.bounds
|
let fromValue = layer.presentation()?.bounds ?? layer.bounds
|
||||||
//layer.bounds = bounds
|
layer.bounds = bounds
|
||||||
self.add(animation: ControlledTransitionProperty(
|
self.add(animation: ControlledTransitionProperty(
|
||||||
layer: layer,
|
layer: layer,
|
||||||
path: "bounds",
|
path: "bounds",
|
||||||
keyPath: \.bounds,
|
|
||||||
fromValue: fromValue,
|
fromValue: fromValue,
|
||||||
toValue: bounds,
|
toValue: bounds,
|
||||||
completion: completion
|
completion: completion
|
||||||
@ -1810,20 +1835,21 @@ public final class ControlledTransition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) {
|
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
|
return
|
||||||
}
|
}
|
||||||
let fromValue = layer.presentation()?.frame ?? layer.frame
|
let fromValue = layer.presentation()?.cornerRadius ?? layer.cornerRadius
|
||||||
if let presentation = layer.presentation(), presentation.frame != layer.frame {
|
layer.cornerRadius = cornerRadius
|
||||||
assert(true)
|
|
||||||
}
|
|
||||||
//layer.frame = frame
|
|
||||||
self.add(animation: ControlledTransitionProperty(
|
self.add(animation: ControlledTransitionProperty(
|
||||||
layer: layer,
|
layer: layer,
|
||||||
path: "frame",
|
path: "cornerRadius",
|
||||||
keyPath: \.frame,
|
|
||||||
fromValue: fromValue,
|
fromValue: fromValue,
|
||||||
toValue: frame,
|
toValue: cornerRadius,
|
||||||
completion: completion
|
completion: completion
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -1859,6 +1885,14 @@ public final class ControlledTransition {
|
|||||||
self.transition.updateAlpha(layer: layer, alpha: alpha, completion: completion)
|
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)?) {
|
public func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?) {
|
||||||
self.transition.updatePosition(layer: layer, position: position, completion: completion)
|
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)?) {
|
public func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?) {
|
||||||
self.transition.updateFrame(layer: layer, frame: frame, completion: completion)
|
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
|
public let animator: ControlledTransitionAnimator
|
||||||
@ -1877,13 +1915,14 @@ public final class ControlledTransition {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
duration: Double,
|
duration: Double,
|
||||||
curve: ContainedViewLayoutTransitionCurve
|
curve: ContainedViewLayoutTransitionCurve,
|
||||||
|
interactive: Bool
|
||||||
) {
|
) {
|
||||||
self.legacyAnimator = LegacyAnimator(
|
self.legacyAnimator = LegacyAnimator(
|
||||||
duration: duration,
|
duration: duration,
|
||||||
curve: curve
|
curve: curve
|
||||||
)
|
)
|
||||||
if #available(iOS 10.0, *) {
|
if interactive {
|
||||||
self.animator = NativeAnimator(
|
self.animator = NativeAnimator(
|
||||||
duration: duration,
|
duration: duration,
|
||||||
curve: curve
|
curve: curve
|
||||||
@ -1894,10 +1933,8 @@ public final class ControlledTransition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func merge(with other: 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 {
|
if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator {
|
||||||
animator.merge(with: otherAnimator)
|
animator.merge(with: otherAnimator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -1585,7 +1585,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
if updateAnimationIsCrossfade {
|
if updateAnimationIsCrossfade {
|
||||||
updateAnimation = .Crossfade
|
updateAnimation = .Crossfade
|
||||||
} else if updateAnimationIsAnimated {
|
} 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
|
controlledTransition = transition
|
||||||
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
|
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
|
||||||
} else {
|
} else {
|
||||||
@ -2048,7 +2048,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
var controlledTransition: ControlledTransition?
|
var controlledTransition: ControlledTransition?
|
||||||
let updateAnimation: ListViewItemUpdateAnimation
|
let updateAnimation: ListViewItemUpdateAnimation
|
||||||
if animated {
|
if animated {
|
||||||
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring)
|
let transition = ControlledTransition(duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: .spring, interactive: true)
|
||||||
controlledTransition = transition
|
controlledTransition = transition
|
||||||
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
|
updateAnimation = .System(duration: insertionAnimationDuration * UIView.animationDurationFactor(), transition: transition)
|
||||||
} else {
|
} else {
|
||||||
|
@ -251,6 +251,27 @@ public final class NavigationBackgroundNode: ASDisplayNode {
|
|||||||
effectView.clipsToBounds = !cornerRadius.isZero
|
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 {
|
open class NavigationBar: ASDisplayNode {
|
||||||
|
@ -294,7 +294,7 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer,
|
|||||||
self.state = .ended
|
self.state = .ended
|
||||||
case .waitForDoubleTap:
|
case .waitForDoubleTap:
|
||||||
self.state = .began
|
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
|
self.timer = timer
|
||||||
RunLoop.main.add(timer, forMode: .common)
|
RunLoop.main.add(timer, forMode: .common)
|
||||||
case let .waitForHold(_, acceptTap):
|
case let .waitForHold(_, acceptTap):
|
||||||
|
@ -13,7 +13,7 @@ public enum ReactionGestureItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class ReactionContextItem {
|
public final class ReactionContextItem {
|
||||||
public struct Reaction {
|
public struct Reaction: Equatable {
|
||||||
public var rawValue: String
|
public var rawValue: String
|
||||||
|
|
||||||
public init(rawValue: String) {
|
public init(rawValue: String) {
|
||||||
@ -446,43 +446,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [AnyObject] {
|
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||||
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
|
guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else {
|
||||||
|
completion()
|
||||||
let x1 = sourcePoint.x
|
return
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil)
|
||||||
|
|
||||||
@ -542,7 +510,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
if itemNode.item.reaction.rawValue != value {
|
if itemNode.item.reaction.rawValue != value {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if let targetSnapshotView = targetView.snapshotContentTree() {
|
|
||||||
if hideNode {
|
if hideNode {
|
||||||
targetView.isHidden = true
|
targetView.isHidden = true
|
||||||
}
|
}
|
||||||
@ -563,7 +530,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
itemNode.position = expandedFrame.center
|
itemNode.position = expandedFrame.center
|
||||||
transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size))
|
transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size))
|
||||||
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: transition)
|
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))
|
transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0))
|
||||||
|
|
||||||
let additionalAnimationNode = AnimatedStickerNode()
|
let additionalAnimationNode = AnimatedStickerNode()
|
||||||
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
|
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
|
||||||
@ -596,14 +563,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
})
|
})
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: {
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: {
|
||||||
self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: {
|
self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: {
|
||||||
mainAnimationCompleted = true
|
mainAnimationCompleted = true
|
||||||
intermediateCompletion()
|
intermediateCompletion()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,6 +604,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
return nil
|
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?) {
|
public func setHighlightedReaction(_ value: ReactionContextItem.Reaction?) {
|
||||||
self.highlightedReaction = value
|
self.highlightedReaction = value
|
||||||
if let (size, insets, anchorRect) = self.validLayout {
|
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) {
|
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()
|
completion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -733,16 +708,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: {
|
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
|
mainAnimationCompleted = true
|
||||||
intermediateCompletion()
|
intermediateCompletion()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||||
let itemFrame: CGRect = itemNode.frame
|
guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else {
|
||||||
let _ = itemFrame
|
completion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil)
|
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)
|
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
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import MurMurHash32
|
|||||||
|
|
||||||
func addSynchronizeEmojiKeywordsOperation(transaction: Transaction, inputLanguageCode: String, languageCode: String?, fromVersion: Int32?) {
|
func addSynchronizeEmojiKeywordsOperation(transaction: Transaction, inputLanguageCode: String, languageCode: String?, fromVersion: Int32?) {
|
||||||
let tag = OperationLogTags.SynchronizeEmojiKeywords
|
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
|
var hasExistingOperation = false
|
||||||
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag) { entry -> Bool in
|
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag) { entry -> Bool in
|
||||||
|
@ -1074,7 +1074,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
strongSelf.window?.presentInGlobalOverlay(controller)
|
strongSelf.window?.presentInGlobalOverlay(controller)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, updateMessageReaction: { [weak self] initialMessage, value in
|
}, updateMessageReaction: { [weak self] initialMessage, reaction in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1088,32 +1088,67 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedReaction: String? = value
|
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 {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? ReactionsMessageAttribute {
|
if let attribute = attribute as? ReactionsMessageAttribute {
|
||||||
for reaction in attribute.reactions {
|
for listReaction in attribute.reactions {
|
||||||
if reaction.value == updatedReaction {
|
switch reaction {
|
||||||
if reaction.isSelected {
|
case .default:
|
||||||
|
if listReaction.isSelected {
|
||||||
updatedReaction = nil
|
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 {
|
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||||
if let current = attribute.value, current == updatedReaction {
|
if attribute.value != nil {
|
||||||
|
switch reaction {
|
||||||
|
case .default:
|
||||||
updatedReaction = nil
|
updatedReaction = nil
|
||||||
|
removedReaction = attribute.value
|
||||||
|
case let .reaction(value):
|
||||||
|
if attribute.value == value {
|
||||||
|
updatedReaction = nil
|
||||||
|
removedReaction = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedReaction != nil {
|
if let updatedReaction = updatedReaction {
|
||||||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
if strongSelf.selectPollOptionFeedback == nil {
|
||||||
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||||||
if item.message.id == message.id {
|
}
|
||||||
|
strongSelf.selectPollOptionFeedback?.tap()
|
||||||
|
|
||||||
itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in
|
itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let updatedReaction = updatedReaction, let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
|
if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
|
||||||
for reaction in availableReactions.reactions {
|
for reaction in availableReactions.reactions {
|
||||||
if reaction.value == updatedReaction {
|
if reaction.value == updatedReaction {
|
||||||
let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem(
|
let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem(
|
||||||
@ -1135,23 +1170,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*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)
|
|
||||||
|
|
||||||
if let strongSelf = self {
|
|
||||||
if strongSelf.selectPollOptionFeedback == nil {
|
|
||||||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
|
||||||
}
|
|
||||||
strongSelf.selectPollOptionFeedback?.tap()
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} 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
|
}, activateMessagePinch: { [weak self] sourceNode in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
|
@ -46,12 +46,17 @@ public enum ChatControllerInteractionSwipeAction {
|
|||||||
case reply
|
case reply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ChatControllerInteractionReaction {
|
||||||
|
case `default`
|
||||||
|
case reaction(String)
|
||||||
|
}
|
||||||
|
|
||||||
public final class ChatControllerInteraction {
|
public final class ChatControllerInteraction {
|
||||||
let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
|
let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
|
||||||
let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void
|
let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void
|
||||||
let openPeerMention: (String) -> Void
|
let openPeerMention: (String) -> Void
|
||||||
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void
|
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void
|
||||||
let updateMessageReaction: (Message, String) -> Void
|
let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void
|
||||||
let activateMessagePinch: (PinchSourceContainerNode) -> Void
|
let activateMessagePinch: (PinchSourceContainerNode) -> Void
|
||||||
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||||
let navigateToMessage: (MessageId, MessageId) -> Void
|
let navigateToMessage: (MessageId, MessageId) -> Void
|
||||||
@ -148,7 +153,7 @@ public final class ChatControllerInteraction {
|
|||||||
openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void,
|
openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void,
|
||||||
openPeerMention: @escaping (String) -> Void,
|
openPeerMention: @escaping (String) -> Void,
|
||||||
openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void,
|
openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void,
|
||||||
updateMessageReaction: @escaping (Message, String) -> Void,
|
updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void,
|
||||||
activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void,
|
activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void,
|
||||||
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
||||||
navigateToMessage: @escaping (MessageId, MessageId) -> Void,
|
navigateToMessage: @escaping (MessageId, MessageId) -> Void,
|
||||||
|
@ -39,8 +39,6 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
|||||||
self.addSubnode(self.backgroundBlurNode)
|
self.addSubnode(self.backgroundBlurNode)
|
||||||
self.addSubnode(self.accessibilityArea)
|
self.addSubnode(self.accessibilityArea)
|
||||||
|
|
||||||
//self.backgroundBlurNode.view.mask = backgroundMaskNode.view
|
|
||||||
|
|
||||||
self.accessibilityArea.activate = { [weak self] in
|
self.accessibilityArea.activate = { [weak self] in
|
||||||
self?.buttonPressed()
|
self?.buttonPressed()
|
||||||
return true
|
return true
|
||||||
@ -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)
|
let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||||
|
|
||||||
return { context, theme, bubbleCorners, strings, message, button, constrainedWidth, position in
|
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 (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
|
let node: ChatMessageActionButtonNode
|
||||||
if let maybeNode = maybeNode {
|
if let maybeNode = maybeNode {
|
||||||
node = maybeNode
|
node = maybeNode
|
||||||
} else {
|
} else {
|
||||||
node = ChatMessageActionButtonNode()
|
node = ChatMessageActionButtonNode()
|
||||||
|
animation = .None
|
||||||
}
|
}
|
||||||
|
|
||||||
node.button = button
|
node.button = button
|
||||||
@ -160,10 +161,10 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node.backgroundMaskNode.image = backgroundMaskImage
|
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))
|
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, transition: .immediate)
|
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)
|
node.backgroundBlurNode.updateColor(color: selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper), transition: .immediate)
|
||||||
|
|
||||||
if iconImage != nil {
|
if iconImage != nil {
|
||||||
@ -185,11 +186,17 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
|||||||
node.addSubnode(titleNode)
|
node.addSubnode(titleNode)
|
||||||
titleNode.isUserInteractionEnabled = false
|
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))
|
if let buttonView = node.buttonView {
|
||||||
node.iconNode?.frame = CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0)
|
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.accessibilityLabel = title
|
||||||
node.accessibilityArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
|
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) } ?? []
|
let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? []
|
||||||
|
|
||||||
return { context, theme, chatBubbleCorners, strings, replyMarkup, message, constrainedWidth in
|
return { context, theme, chatBubbleCorners, strings, replyMarkup, message, constrainedWidth in
|
||||||
@ -234,14 +241,14 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
|||||||
|
|
||||||
var overallMinimumRowWidth: CGFloat = 0.0
|
var overallMinimumRowWidth: CGFloat = 0.0
|
||||||
|
|
||||||
var finalizeRowLayouts: [[((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))]] = []
|
var finalizeRowLayouts: [[((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))]] = []
|
||||||
|
|
||||||
var rowIndex = 0
|
var rowIndex = 0
|
||||||
var buttonIndex = 0
|
var buttonIndex = 0
|
||||||
for row in replyMarkup.rows {
|
for row in replyMarkup.rows {
|
||||||
var maximumRowButtonWidth: CGFloat = 0.0
|
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)))
|
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
|
var rowButtonIndex = 0
|
||||||
for button in row.buttons {
|
for button in row.buttons {
|
||||||
let buttonPosition: MessageBubbleActionButtonPosition
|
let buttonPosition: MessageBubbleActionButtonPosition
|
||||||
@ -259,7 +266,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
|||||||
buttonPosition = .middle
|
buttonPosition = .middle
|
||||||
}
|
}
|
||||||
|
|
||||||
let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode)))
|
let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)))
|
||||||
if buttonIndex < currentButtonLayouts.count {
|
if buttonIndex < currentButtonLayouts.count {
|
||||||
prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, message, button, maximumButtonWidth, buttonPosition)
|
prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, message, button, maximumButtonWidth, buttonPosition)
|
||||||
} else {
|
} else {
|
||||||
@ -280,7 +287,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (min(constrainedWidth, overallMinimumRowWidth), { constrainedWidth in
|
return (min(constrainedWidth, overallMinimumRowWidth), { constrainedWidth in
|
||||||
var buttonFramesAndApply: [(CGRect, () -> ChatMessageActionButtonNode)] = []
|
var buttonFramesAndApply: [(CGRect, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)] = []
|
||||||
|
|
||||||
var verticalRowOffset: CGFloat = 0.0
|
var verticalRowOffset: CGFloat = 0.0
|
||||||
verticalRowOffset += buttonSpacing
|
verticalRowOffset += buttonSpacing
|
||||||
@ -303,7 +310,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
|||||||
verticalRowOffset = max(0.0, verticalRowOffset - buttonSpacing)
|
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
|
let node: ChatMessageActionButtonsNode
|
||||||
if let maybeNode = maybeNode {
|
if let maybeNode = maybeNode {
|
||||||
node = maybeNode
|
node = maybeNode
|
||||||
@ -314,13 +321,15 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
|||||||
var updatedButtons: [ChatMessageActionButtonNode] = []
|
var updatedButtons: [ChatMessageActionButtonNode] = []
|
||||||
var index = 0
|
var index = 0
|
||||||
for (buttonFrame, buttonApply) in buttonFramesAndApply {
|
for (buttonFrame, buttonApply) in buttonFramesAndApply {
|
||||||
let buttonNode = buttonApply()
|
let buttonNode = buttonApply(animation)
|
||||||
buttonNode.frame = buttonFrame
|
|
||||||
updatedButtons.append(buttonNode)
|
updatedButtons.append(buttonNode)
|
||||||
if buttonNode.supernode == nil {
|
if buttonNode.supernode == nil {
|
||||||
node.addSubnode(buttonNode)
|
node.addSubnode(buttonNode)
|
||||||
buttonNode.pressed = node.buttonPressedWrapper
|
buttonNode.pressed = node.buttonPressedWrapper
|
||||||
buttonNode.longTapped = node.buttonLongTappedWrapper
|
buttonNode.longTapped = node.buttonLongTappedWrapper
|
||||||
|
buttonNode.frame = buttonFrame
|
||||||
|
} else {
|
||||||
|
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
|
||||||
}
|
}
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
@ -345,12 +354,6 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
node.buttonNodes = updatedButtons
|
node.buttonNodes = updatedButtons
|
||||||
|
|
||||||
if animated {
|
|
||||||
/*UIView.transition(with: node.view, duration: 0.2, options: [.transitionCrossDissolve], animations: {
|
|
||||||
|
|
||||||
}, completion: nil)*/
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
return node
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -199,6 +199,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
private var replyBackgroundNode: NavigationBackgroundNode?
|
private var replyBackgroundNode: NavigationBackgroundNode?
|
||||||
|
|
||||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||||
|
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||||
|
|
||||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||||
|
|
||||||
@ -341,7 +342,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return .waitForSingleTap
|
return .waitForDoubleTap
|
||||||
}
|
}
|
||||||
recognizer.longTap = { [weak self] point, recognizer in
|
recognizer.longTap = { [weak self] point, recognizer in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -701,6 +702,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
let imageLayout = self.imageNode.asyncLayout()
|
let imageLayout = self.imageNode.asyncLayout()
|
||||||
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
||||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||||
|
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||||
|
|
||||||
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
||||||
|
|
||||||
@ -896,7 +898,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone,
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
@ -1021,22 +1023,53 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var maxContentWidth = imageSize.width
|
var maxContentWidth = imageSize.width
|
||||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||||
if let replyMarkup = replyMarkup {
|
if let replyMarkup = replyMarkup {
|
||||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
||||||
maxContentWidth = max(maxContentWidth, minWidth)
|
maxContentWidth = max(maxContentWidth, minWidth)
|
||||||
actionButtonsFinalize = buttonsLayout
|
actionButtonsFinalize = buttonsLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
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)
|
var layoutSize = CGSize(width: params.width, height: contentHeight)
|
||||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
layoutSize.height += actionButtonsSizeAndApply.0.height
|
||||||
}
|
}
|
||||||
|
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||||
|
layoutSize.height += reactionButtonsSizeAndApply.0.height
|
||||||
|
}
|
||||||
|
|
||||||
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
|
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -1122,8 +1155,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
strongSelf.shareButtonNode = nil
|
strongSelf.shareButtonNode = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dateAndStatusApply(.None)
|
let dateAndStatusFrame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0), size: dateAndStatusSize)
|
||||||
strongSelf.dateAndStatusNode.frame = 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 needsReplyBackground {
|
||||||
if let replyBackgroundNode = strongSelf.replyBackgroundNode {
|
if let replyBackgroundNode = strongSelf.replyBackgroundNode {
|
||||||
@ -1272,13 +1306,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
var animated = false
|
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||||
if let _ = strongSelf.actionButtonsNode {
|
|
||||||
if case .System = animation {
|
|
||||||
animated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
|
||||||
let previousFrame = actionButtonsNode.frame
|
let previousFrame = actionButtonsNode.frame
|
||||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: actionButtonsSizeAndApply.0)
|
let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: actionButtonsSizeAndApply.0)
|
||||||
actionButtonsNode.frame = actionButtonsFrame
|
actionButtonsNode.frame = actionButtonsFrame
|
||||||
@ -1305,6 +1333,36 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
strongSelf.actionButtonsNode = nil
|
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) {
|
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||||
strongSelf.dateAndStatusNode.pressed = {
|
strongSelf.dateAndStatusNode.pressed = {
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1329,7 +1387,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .ended:
|
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 let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: recognizer) {
|
||||||
if case .doubleTap = gesture {
|
if case .doubleTap = gesture {
|
||||||
self.containerNode.cancelGesture()
|
self.containerNode.cancelGesture()
|
||||||
@ -1340,10 +1398,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
case let .optionalAction(f):
|
case let .optionalAction(f):
|
||||||
f()
|
f()
|
||||||
case let .openContextMenu(tapMessage, selectAll, subFrame):
|
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 {
|
} else if case .tap = gesture {
|
||||||
self.item?.controllerInteraction.clickThroughMessage()
|
item.controllerInteraction.clickThroughMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -1916,6 +1978,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
return shareButtonNode.view
|
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)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2225,6 +2293,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func targetReactionView(value: String) -> UIView? {
|
override func targetReactionView(value: String) -> UIView? {
|
||||||
|
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
if !self.dateAndStatusNode.isHidden {
|
if !self.dateAndStatusNode.isHidden {
|
||||||
return self.dateAndStatusNode.reactionView(value: value)
|
return self.dateAndStatusNode.reactionView(value: value)
|
||||||
}
|
}
|
||||||
|
@ -363,7 +363,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
|
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
|
||||||
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)))?
|
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode)))?
|
||||||
|
|
||||||
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)?
|
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)?
|
||||||
|
|
||||||
let string = NSMutableAttributedString()
|
let string = NSMutableAttributedString()
|
||||||
var notEmpty = false
|
var notEmpty = false
|
||||||
@ -634,7 +634,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: textStatusType,
|
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,
|
constrainedSize: textConstrainedSize,
|
||||||
availableReactions: associatedData.availableReactions,
|
availableReactions: associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
@ -797,11 +797,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
adjustedLineHeight += statusSizeAndApply.0.height
|
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)
|
adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width)
|
||||||
|
|
||||||
return (adjustedBoundingSize, { [weak self] animation, synchronousLoads in
|
return (adjustedBoundingSize, { [weak self] animation, synchronousLoads in
|
||||||
@ -811,17 +806,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
strongSelf.media = mediaAndFlags?.0
|
strongSelf.media = mediaAndFlags?.0
|
||||||
strongSelf.theme = presentationData.theme
|
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.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.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
|
strongSelf.lineNode.isHidden = !displayLine
|
||||||
@ -864,6 +848,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
strongSelf.openMedia?(mode)
|
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
|
contentImageNode.visibility = strongSelf.visibility != .none
|
||||||
}
|
}
|
||||||
let _ = contentImageApply(animation, synchronousLoads)
|
let _ = contentImageApply(animation, synchronousLoads)
|
||||||
@ -924,7 +914,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
|
|
||||||
if let (videoLayout, apply) = contentInstantVideoSizeAndApply {
|
if let (videoLayout, apply) = contentInstantVideoSizeAndApply {
|
||||||
contentMediaHeight = videoLayout.contentSize.height
|
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 {
|
if strongSelf.contentInstantVideoNode !== contentInstantVideoNode {
|
||||||
strongSelf.contentInstantVideoNode = contentInstantVideoNode
|
strongSelf.contentInstantVideoNode = contentInstantVideoNode
|
||||||
strongSelf.addSubnode(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?)? {
|
func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
|
||||||
return self.contentImageNode?.playMediaWithSound()
|
return self.contentImageNode?.playMediaWithSound()
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,9 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat
|
|||||||
case .Right:
|
case .Right:
|
||||||
topLeftCorner = .Corner(normalRadius)
|
topLeftCorner = .Corner(normalRadius)
|
||||||
topRightCorner = .Corner(mergedRadius)
|
topRightCorner = .Corner(mergedRadius)
|
||||||
|
case .Both:
|
||||||
|
topLeftCorner = .Corner(mergedRadius)
|
||||||
|
topRightCorner = .Corner(mergedRadius)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .mosaic(position, _):
|
case let .mosaic(position, _):
|
||||||
@ -65,6 +68,9 @@ func chatMessageBubbleImageContentCorners(relativeContentPosition position: Chat
|
|||||||
case .Left:
|
case .Left:
|
||||||
bottomLeftCorner = .Corner(mergedRadius)
|
bottomLeftCorner = .Corner(mergedRadius)
|
||||||
bottomRightCorner = .Corner(normalRadius)
|
bottomRightCorner = .Corner(normalRadius)
|
||||||
|
case .Both:
|
||||||
|
bottomLeftCorner = .Corner(mergedRadius)
|
||||||
|
bottomRightCorner = .Corner(mergedRadius)
|
||||||
case let .None(status):
|
case let .None(status):
|
||||||
let bubbleInsets: UIEdgeInsets
|
let bubbleInsets: UIEdgeInsets
|
||||||
if case .color = chatPresentationData.theme.wallpaper {
|
if case .color = chatPresentationData.theme.wallpaper {
|
||||||
|
@ -37,6 +37,7 @@ enum ChatMessageBubbleMergeStatus {
|
|||||||
case None(ChatMessageBubbleNoneMergeStatus)
|
case None(ChatMessageBubbleNoneMergeStatus)
|
||||||
case Left
|
case Left
|
||||||
case Right
|
case Right
|
||||||
|
case Both
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ChatMessageBubbleRelativePosition {
|
enum ChatMessageBubbleRelativePosition {
|
||||||
|
@ -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 result: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)] = []
|
||||||
var skipText = false
|
var skipText = false
|
||||||
var messageWithCaptionToAdd: (Message, ChatMessageEntryAttributes)?
|
var messageWithCaptionToAdd: (Message, ChatMessageEntryAttributes)?
|
||||||
@ -56,10 +56,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
var previousItemIsFile = false
|
var previousItemIsFile = false
|
||||||
var hasFiles = false
|
var hasFiles = false
|
||||||
|
|
||||||
|
var needReactions = true
|
||||||
|
|
||||||
outer: for (message, itemAttributes) in item.content {
|
outer: for (message, itemAttributes) in item.content {
|
||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
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)))
|
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
break outer
|
break outer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,6 +89,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
isFile = true
|
isFile = true
|
||||||
hasFiles = true
|
hasFiles = true
|
||||||
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing)))
|
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing)))
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
} else if let action = media as? TelegramMediaAction {
|
} else if let action = media as? TelegramMediaAction {
|
||||||
isAction = true
|
isAction = true
|
||||||
@ -94,26 +98,33 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
} else {
|
} else {
|
||||||
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
}
|
}
|
||||||
|
needReactions = false
|
||||||
} else if let _ = media as? TelegramMediaMap {
|
} else if let _ = media as? TelegramMediaMap {
|
||||||
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
} else if let _ = media as? TelegramMediaGame {
|
} else if let _ = media as? TelegramMediaGame {
|
||||||
skipText = true
|
skipText = true
|
||||||
result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
break inner
|
break inner
|
||||||
} else if let _ = media as? TelegramMediaInvoice {
|
} else if let _ = media as? TelegramMediaInvoice {
|
||||||
skipText = true
|
skipText = true
|
||||||
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
break inner
|
break inner
|
||||||
} else if let _ = media as? TelegramMediaContact {
|
} else if let _ = media as? TelegramMediaContact {
|
||||||
result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
} else if let _ = media as? TelegramMediaExpiredContent {
|
} else if let _ = media as? TelegramMediaExpiredContent {
|
||||||
result.removeAll()
|
result.removeAll()
|
||||||
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
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 {
|
} else if let _ = media as? TelegramMediaPoll {
|
||||||
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
} else if let _ = media as? TelegramMediaUnsupported {
|
} else if let _ = media as? TelegramMediaUnsupported {
|
||||||
isUnsupportedMedia = true
|
isUnsupportedMedia = true
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
previousItemIsFile = isFile
|
previousItemIsFile = isFile
|
||||||
}
|
}
|
||||||
@ -130,6 +141,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
skipText = true
|
skipText = true
|
||||||
} else {
|
} else {
|
||||||
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: isFile ? .condensed : .default)))
|
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: isFile ? .condensed : .default)))
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if case .group = item.content {
|
if case .group = item.content {
|
||||||
@ -142,6 +154,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
if let webpage = media as? TelegramMediaWebpage {
|
if let webpage = media as? TelegramMediaWebpage {
|
||||||
if case .Loaded = webpage.content {
|
if case .Loaded = webpage.content {
|
||||||
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
break inner
|
break inner
|
||||||
}
|
}
|
||||||
@ -151,34 +164,39 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
result.removeAll()
|
result.removeAll()
|
||||||
|
|
||||||
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if isUnsupportedMedia {
|
if isUnsupportedMedia {
|
||||||
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd {
|
if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd {
|
||||||
result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if let additionalContent = item.additionalContent {
|
if let additionalContent = item.additionalContent {
|
||||||
switch additionalContent {
|
switch additionalContent {
|
||||||
case let .eventLogPreviousMessage(previousMessage):
|
case let .eventLogPreviousMessage(previousMessage):
|
||||||
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
case let .eventLogPreviousDescription(previousMessage):
|
case let .eventLogPreviousDescription(previousMessage):
|
||||||
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
case let .eventLogPreviousLink(previousMessage):
|
case let .eventLogPreviousLink(previousMessage):
|
||||||
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstMessage = item.content.firstMessage
|
let firstMessage = item.content.firstMessage
|
||||||
|
|
||||||
if let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty {
|
let reactionsAreInline = shouldDisplayInlineDateReactions(message: firstMessage)
|
||||||
if result.last?.1 == ChatMessageWebpageBubbleContentNode.self || result.last?.1 == ChatMessagePollBubbleContentNode.self || result.last?.1 == ChatMessageContactBubbleContentNode.self {
|
if reactionsAreInline {
|
||||||
result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
|
needReactions = false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isAction && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) {
|
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
|
var needSeparateContainers = false
|
||||||
if case .group = item.content, hasFiles {
|
if case .group = item.content, hasFiles {
|
||||||
needSeparateContainers = true
|
needSeparateContainers = true
|
||||||
|
needReactions = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return (result, needSeparateContainers)
|
return (result, needSeparateContainers, needReactions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let chatMessagePeerIdColors: [UIColor] = [
|
private let chatMessagePeerIdColors: [UIColor] = [
|
||||||
@ -428,6 +459,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
private(set) var contentNodes: [ChatMessageBubbleContentNode] = []
|
private(set) var contentNodes: [ChatMessageBubbleContentNode] = []
|
||||||
private var mosaicStatusNode: ChatMessageDateAndStatusNode?
|
private var mosaicStatusNode: ChatMessageDateAndStatusNode?
|
||||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||||
|
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||||
|
|
||||||
private var shareButtonNode: ChatMessageShareButton?
|
private var shareButtonNode: ChatMessageShareButton?
|
||||||
|
|
||||||
@ -890,6 +922,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
||||||
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
|
let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
|
||||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||||
|
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||||
|
|
||||||
let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode)
|
let mosaicStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.mosaicStatusNode)
|
||||||
|
|
||||||
@ -911,6 +944,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
forwardInfoLayout: forwardInfoLayout,
|
forwardInfoLayout: forwardInfoLayout,
|
||||||
replyInfoLayout: replyInfoLayout,
|
replyInfoLayout: replyInfoLayout,
|
||||||
actionButtonsLayout: actionButtonsLayout,
|
actionButtonsLayout: actionButtonsLayout,
|
||||||
|
reactionButtonsLayout: reactionButtonsLayout,
|
||||||
mosaicStatusLayout: mosaicStatusLayout,
|
mosaicStatusLayout: mosaicStatusLayout,
|
||||||
layoutConstants: layoutConstants,
|
layoutConstants: layoutConstants,
|
||||||
currentItem: currentItem,
|
currentItem: currentItem,
|
||||||
@ -926,7 +960,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
|
||||||
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
|
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
|
||||||
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
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)),
|
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
|
||||||
layoutConstants: ChatMessageItemLayoutConstants,
|
layoutConstants: ChatMessageItemLayoutConstants,
|
||||||
currentItem: ChatMessageItem?,
|
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 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)]?
|
var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?
|
||||||
|
|
||||||
let (contentNodeMessagesAndClasses, needSeparateContainers) = contentNodeMessagesAndClassesForItem(item)
|
let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item)
|
||||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
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?)] = []
|
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 backgroundHiding: ChatMessageBubbleContentBackgroundHiding?
|
||||||
var hasSolidWallpaper = false
|
var hasSolidWallpaper = false
|
||||||
switch item.presentationData.theme.wallpaper {
|
switch item.presentationData.theme.wallpaper {
|
||||||
@ -1383,6 +1415,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
index += 1
|
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 currentCredibilityIconImage: UIImage?
|
||||||
|
|
||||||
var initialDisplayHeader = true
|
var initialDisplayHeader = true
|
||||||
@ -1538,7 +1583,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone,
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
@ -1714,13 +1759,27 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
|
|
||||||
var maxContentWidth: CGFloat = headerSize.width
|
var maxContentWidth: CGFloat = headerSize.width
|
||||||
|
|
||||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||||
if let replyMarkup = replyMarkup {
|
if let replyMarkup = replyMarkup {
|
||||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth)
|
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maximumNodeWidth)
|
||||||
maxContentWidth = max(maxContentWidth, minWidth)
|
maxContentWidth = max(maxContentWidth, minWidth)
|
||||||
actionButtonsFinalize = buttonsLayout
|
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 {
|
for i in 0 ..< contentPropertiesAndLayouts.count {
|
||||||
let (_, contentNodeProperties, preparePosition, _, contentNodeLayout, contentGroupId, itemSelection) = contentPropertiesAndLayouts[i]
|
let (_, contentNodeProperties, preparePosition, _, contentNodeLayout, contentGroupId, itemSelection) = contentPropertiesAndLayouts[i]
|
||||||
|
|
||||||
@ -1744,7 +1803,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
case let .None(status):
|
case let .None(status):
|
||||||
if position.contains(.top) && position.contains(.left) {
|
if position.contains(.top) && position.contains(.left) {
|
||||||
switch status {
|
switch status {
|
||||||
case .Left:
|
case .Left, .Both:
|
||||||
topLeft = .mergedBubble
|
topLeft = .mergedBubble
|
||||||
case .Right:
|
case .Right:
|
||||||
topLeft = .none(tail: false)
|
topLeft = .none(tail: false)
|
||||||
@ -1759,7 +1818,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
switch status {
|
switch status {
|
||||||
case .Left:
|
case .Left:
|
||||||
topRight = .none(tail: false)
|
topRight = .none(tail: false)
|
||||||
case .Right:
|
case .Right, .Both:
|
||||||
topRight = .mergedBubble
|
topRight = .mergedBubble
|
||||||
case .None:
|
case .None:
|
||||||
topRight = .none(tail: false)
|
topRight = .none(tail: false)
|
||||||
@ -1795,7 +1854,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
case let .None(status):
|
case let .None(status):
|
||||||
if position.contains(.bottom) && position.contains(.left) {
|
if position.contains(.bottom) && position.contains(.left) {
|
||||||
switch status {
|
switch status {
|
||||||
case .Left:
|
case .Left, .Both:
|
||||||
bottomLeft = .mergedBubble
|
bottomLeft = .mergedBubble
|
||||||
case .Right:
|
case .Right:
|
||||||
bottomLeft = .none(tail: false)
|
bottomLeft = .none(tail: false)
|
||||||
@ -1814,7 +1873,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
switch status {
|
switch status {
|
||||||
case .Left:
|
case .Left:
|
||||||
bottomRight = .none(tail: false)
|
bottomRight = .none(tail: false)
|
||||||
case .Right:
|
case .Right, .Both:
|
||||||
bottomRight = .mergedBubble
|
bottomRight = .mergedBubble
|
||||||
case let .None(tailStatus):
|
case let .None(tailStatus):
|
||||||
if case .Outgoing = tailStatus {
|
if case .Outgoing = tailStatus {
|
||||||
@ -1970,11 +2029,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
|
|
||||||
contentSize.height += totalContentNodesHeight
|
contentSize.height += totalContentNodesHeight
|
||||||
|
|
||||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?
|
||||||
|
if let reactionButtonsFinalize = reactionButtonsFinalize {
|
||||||
|
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
|
||||||
|
}
|
||||||
|
|
||||||
let minimalContentSize: CGSize
|
let minimalContentSize: CGSize
|
||||||
if hideBackground {
|
if hideBackground {
|
||||||
minimalContentSize = CGSize(width: 1.0, height: 1.0)
|
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)
|
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)
|
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height)
|
||||||
|
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||||
|
layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height
|
||||||
|
}
|
||||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
layoutSize.height += actionButtonsSizeAndApply.0.height
|
||||||
}
|
}
|
||||||
@ -2032,7 +2099,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
if headerSize.height.isZero && contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false {
|
if headerSize.height.isZero && contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false {
|
||||||
updatedMergedBottom = .none
|
updatedMergedBottom = .none
|
||||||
}
|
}
|
||||||
if actionButtonsSizeAndApply != nil {
|
if actionButtonsSizeAndApply != nil || reactionButtonsSizeAndApply != nil {
|
||||||
updatedMergedTop = .fullyMerged
|
updatedMergedTop = .fullyMerged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2047,6 +2114,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
forwardAuthorSignature: forwardAuthorSignature,
|
forwardAuthorSignature: forwardAuthorSignature,
|
||||||
accessibilityData: accessibilityData,
|
accessibilityData: accessibilityData,
|
||||||
actionButtonsSizeAndApply: actionButtonsSizeAndApply,
|
actionButtonsSizeAndApply: actionButtonsSizeAndApply,
|
||||||
|
reactionButtonsSizeAndApply: reactionButtonsSizeAndApply,
|
||||||
updatedMergedTop: updatedMergedTop,
|
updatedMergedTop: updatedMergedTop,
|
||||||
updatedMergedBottom: updatedMergedBottom,
|
updatedMergedBottom: updatedMergedBottom,
|
||||||
hideBackground: hideBackground,
|
hideBackground: hideBackground,
|
||||||
@ -2087,7 +2155,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
forwardSource: Peer?,
|
forwardSource: Peer?,
|
||||||
forwardAuthorSignature: String?,
|
forwardAuthorSignature: String?,
|
||||||
accessibilityData: ChatMessageAccessibilityData,
|
accessibilityData: ChatMessageAccessibilityData,
|
||||||
actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?,
|
actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?,
|
||||||
|
reactionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)?,
|
||||||
updatedMergedTop: ChatMessageMerge,
|
updatedMergedTop: ChatMessageMerge,
|
||||||
updatedMergedBottom: ChatMessageMerge,
|
updatedMergedBottom: ChatMessageMerge,
|
||||||
hideBackground: Bool,
|
hideBackground: Bool,
|
||||||
@ -2141,7 +2210,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
var forceBackgroundSide = false
|
var forceBackgroundSide = false
|
||||||
if actionButtonsSizeAndApply != nil {
|
if actionButtonsSizeAndApply != nil || reactionButtonsSizeAndApply != nil {
|
||||||
forceBackgroundSide = true
|
forceBackgroundSide = true
|
||||||
} else if case .semanticallyMerged = updatedMergedTop {
|
} else if case .semanticallyMerged = updatedMergedTop {
|
||||||
forceBackgroundSide = true
|
forceBackgroundSide = true
|
||||||
@ -2632,14 +2701,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
|
|
||||||
if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
||||||
if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) {
|
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.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
|
||||||
animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil)
|
animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil)
|
||||||
strongSelf.clippingNode.clipsToBounds = true
|
strongSelf.clippingNode.clipsToBounds = true
|
||||||
@ -2651,6 +2712,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: animation)
|
strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: animation)
|
||||||
animation.animator.updateFrame(layer: strongSelf.backgroundWallpaperNode.layer, frame: backgroundFrame, completion: nil)
|
animation.animator.updateFrame(layer: strongSelf.backgroundWallpaperNode.layer, frame: backgroundFrame, completion: nil)
|
||||||
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: animation.transition)
|
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: animation.transition)
|
||||||
|
strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: animation.transition)
|
||||||
|
|
||||||
if let type = strongSelf.backgroundNode.type {
|
if let type = strongSelf.backgroundNode.type {
|
||||||
var incomingOffset: CGFloat = 0.0
|
var incomingOffset: CGFloat = 0.0
|
||||||
@ -2669,11 +2731,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
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 {
|
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||||
let currentBackgroundFrame = strongSelf.backgroundNode.frame
|
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)
|
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 {
|
} else {
|
||||||
if let _ = strongSelf.backgroundFrameTransition {
|
/*if let _ = strongSelf.backgroundFrameTransition {
|
||||||
strongSelf.animateFrameTransition(1.0, backgroundFrame.size.height)
|
strongSelf.animateFrameTransition(1.0, backgroundFrame.size.height)
|
||||||
strongSelf.backgroundFrameTransition = nil
|
strongSelf.backgroundFrameTransition = nil
|
||||||
}
|
}*/
|
||||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
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)
|
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?.frame = selectionFrame
|
||||||
strongSelf.selectionNode?.updateLayout(size: selectionFrame.size, leftInset: params.leftInset)
|
strongSelf.selectionNode?.updateLayout(size: selectionFrame.size, leftInset: params.leftInset)
|
||||||
|
|
||||||
|
var reactionButtonsOffset: CGFloat = 0.0
|
||||||
|
|
||||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
var animated = false
|
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||||
if let _ = strongSelf.actionButtonsNode {
|
|
||||||
if case .System = animation {
|
|
||||||
animated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
|
||||||
let previousFrame = actionButtonsNode.frame
|
|
||||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
|
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 {
|
if actionButtonsNode !== strongSelf.actionButtonsNode {
|
||||||
strongSelf.actionButtonsNode = actionButtonsNode
|
strongSelf.actionButtonsNode = actionButtonsNode
|
||||||
actionButtonsNode.buttonPressed = { [weak strongSelf] button in
|
actionButtonsNode.buttonPressed = { [weak strongSelf] button in
|
||||||
@ -2741,16 +2792,47 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||||
|
actionButtonsNode.frame = actionButtonsFrame
|
||||||
} else {
|
} else {
|
||||||
if case let .System(duration, _) = animation {
|
animation.animator.updateFrame(layer: actionButtonsNode.layer, frame: actionButtonsFrame, completion: nil)
|
||||||
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: timingFunction)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reactionButtonsOffset += actionButtonsSizeAndApply.0.height
|
||||||
} else if let actionButtonsNode = strongSelf.actionButtonsNode {
|
} else if let actionButtonsNode = strongSelf.actionButtonsNode {
|
||||||
actionButtonsNode.removeFromSupernode()
|
actionButtonsNode.removeFromSupernode()
|
||||||
strongSelf.actionButtonsNode = nil
|
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
|
let previousContextContentFrame = strongSelf.mainContextSourceNode.contentRect
|
||||||
strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
|
strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
|
||||||
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
|
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
|
||||||
@ -2876,8 +2958,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
case let .optionalAction(f):
|
case let .optionalAction(f):
|
||||||
f()
|
f()
|
||||||
case let .openContextMenu(tapMessage, selectAll, subFrame):
|
case let .openContextMenu(tapMessage, selectAll, subFrame):
|
||||||
if canAddMessageReactions(message: tapMessage), let defaultReaction = item.associatedData.defaultReaction {
|
if canAddMessageReactions(message: tapMessage) {
|
||||||
item.controllerInteraction.updateMessageReaction(tapMessage, defaultReaction)
|
item.controllerInteraction.updateMessageReaction(tapMessage, .default)
|
||||||
} else {
|
} else {
|
||||||
item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
|
item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil)
|
||||||
}
|
}
|
||||||
@ -3713,6 +3795,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func targetReactionView(value: String) -> UIView? {
|
override func targetReactionView(value: String) -> UIView? {
|
||||||
|
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
for contentNode in self.contentNodes {
|
for contentNode in self.contentNodes {
|
||||||
if let result = contentNode.reactionTargetView(value: value) {
|
if let result = contentNode.reactionTargetView(value: value) {
|
||||||
return result
|
return result
|
||||||
|
@ -105,12 +105,15 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
let displaySeparator: Bool
|
let displaySeparator: Bool
|
||||||
let topOffset: CGFloat
|
let topOffset: CGFloat
|
||||||
|
let topSeparatorOffset: CGFloat
|
||||||
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
|
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
|
||||||
displaySeparator = false
|
displaySeparator = false
|
||||||
topOffset = 2.0
|
topOffset = 2.0
|
||||||
|
topSeparatorOffset = 0.0
|
||||||
} else {
|
} else {
|
||||||
displaySeparator = true
|
displaySeparator = true
|
||||||
topOffset = 0.0
|
topOffset = 2.0
|
||||||
|
topSeparatorOffset = 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
||||||
@ -374,7 +377,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
|
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
|
||||||
strongSelf.separatorNode.isHidden = !displaySeparator
|
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))
|
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: boundingSize.height))
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
guard let strongSelf = self, let item = strongSelf.item else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import TelegramPresentationData
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import AppBundle
|
import AppBundle
|
||||||
import ReactionButtonListComponent
|
import ReactionButtonListComponent
|
||||||
|
import WebPBinding
|
||||||
|
|
||||||
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
||||||
if let _ = layer.animation(forKey: "clockFrameAnimation") {
|
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 let reactionFont = Font.regular(12.0)
|
||||||
|
|
||||||
private final class StatusReactionNode: ASDisplayNode {
|
private final class StatusReactionNode: ASDisplayNode {
|
||||||
let selectedImageNode: ASImageNode
|
let iconView: UIImageView
|
||||||
|
|
||||||
|
private let iconImageDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
private var value: String?
|
private var value: String?
|
||||||
private var isSelected: Bool?
|
private var isSelected: Bool?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.selectedImageNode = ASImageNode()
|
self.iconView = UIImageView()
|
||||||
self.selectedImageNode.displaysAsynchronously = false
|
|
||||||
|
|
||||||
super.init()
|
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 {
|
if self.value != value {
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
let selectedImage: UIImage? = generateImage(CGSize(width: 14.0, height: 14.0), rotatedContext: { size, context in
|
let defaultImageSize = CGSize(width: 19.0, height: 19.0)
|
||||||
UIGraphicsPushContext(context)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
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
|
||||||
|
}
|
||||||
|
|
||||||
context.scaleBy(x: size.width / 20.0, y: size.width / 20.0)
|
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((defaultImageSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((defaultImageSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageDateAndStatusNode: ASDisplayNode {
|
class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||||
struct ReactionSettings {
|
struct TrailingReactionSettings {
|
||||||
|
var displayInline: Bool
|
||||||
var preferAdditionalInset: Bool
|
var preferAdditionalInset: Bool
|
||||||
|
|
||||||
init(preferAdditionalInset: Bool) {
|
init(displayInline: Bool, preferAdditionalInset: Bool) {
|
||||||
|
self.displayInline = displayInline
|
||||||
self.preferAdditionalInset = preferAdditionalInset
|
self.preferAdditionalInset = preferAdditionalInset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct StandaloneReactionSettings {
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum LayoutInput {
|
enum LayoutInput {
|
||||||
case trailingContent(contentWidth: CGFloat, reactionSettings: ReactionSettings?)
|
case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?)
|
||||||
case standalone
|
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 {
|
struct Arguments {
|
||||||
@ -154,7 +187,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
private var clockMinNode: ASImageNode?
|
private var clockMinNode: ASImageNode?
|
||||||
private let dateNode: TextNode
|
private let dateNode: TextNode
|
||||||
private var impressionIcon: ASImageNode?
|
private var impressionIcon: ASImageNode?
|
||||||
private var reactionNodes: [StatusReactionNode] = []
|
private var reactionNodes: [String: StatusReactionNode] = [:]
|
||||||
private let reactionButtonsContainer = ReactionButtonsLayoutContainer()
|
private let reactionButtonsContainer = ReactionButtonsLayoutContainer()
|
||||||
private var reactionCountNode: TextNode?
|
private var reactionCountNode: TextNode?
|
||||||
private var reactionButtonNode: HighlightTrackingButtonNode?
|
private var reactionButtonNode: HighlightTrackingButtonNode?
|
||||||
@ -247,22 +280,24 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
switch arguments.type {
|
switch arguments.type {
|
||||||
case .BubbleIncoming, .ImageIncoming, .FreeIncoming:
|
case .BubbleIncoming, .ImageIncoming, .FreeIncoming:
|
||||||
reactionColors = ReactionButtonComponent.Colors(
|
reactionColors = ReactionButtonComponent.Colors(
|
||||||
background: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
deselectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||||
foreground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
selectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||||
stroke: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.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:
|
case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing:
|
||||||
reactionColors = ReactionButtonComponent.Colors(
|
reactionColors = ReactionButtonComponent.Colors(
|
||||||
background: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
deselectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||||
foreground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
|
selectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||||
stroke: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.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 {
|
switch arguments.type {
|
||||||
case .BubbleIncoming:
|
case .BubbleIncoming:
|
||||||
dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor
|
dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor
|
||||||
leftInset = 10.0
|
leftInset = 5.0
|
||||||
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||||
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||||
clockFrameImage = graphics.clockBubbleIncomingFrameImage
|
clockFrameImage = graphics.clockBubbleIncomingFrameImage
|
||||||
@ -278,7 +313,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
case let .BubbleOutgoing(status):
|
case let .BubbleOutgoing(status):
|
||||||
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||||
outgoingStatus = status
|
outgoingStatus = status
|
||||||
leftInset = 10.0
|
leftInset = 5.0
|
||||||
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||||
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
||||||
clockFrameImage = graphics.clockBubbleOutgoingFrameImage
|
clockFrameImage = graphics.clockBubbleOutgoingFrameImage
|
||||||
@ -524,13 +559,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
|
|
||||||
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||||
|
|
||||||
let reactionSize: CGFloat = 14.0
|
let reactionSize: CGFloat = 19.0
|
||||||
var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||||
let reactionSpacing: CGFloat = -4.0
|
let reactionSpacing: CGFloat = 2.0
|
||||||
let reactionTrailingSpacing: CGFloat = 4.0
|
let reactionTrailingSpacing: CGFloat = 6.0
|
||||||
|
|
||||||
var reactionInset: CGFloat = 0.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
|
reactionInset = -1.0 + CGFloat(arguments.reactions.count) * reactionSize + CGFloat(arguments.reactions.count - 1) * reactionSpacing + reactionTrailingSpacing
|
||||||
|
|
||||||
var count = 0
|
var count = 0
|
||||||
@ -547,10 +582,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
countString = "\(count)"
|
countString = "\(count)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)))
|
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
|
reactionInset += layoutAndApply.0.size.width + 4.0
|
||||||
reactionCountLayoutAndApply = layoutAndApply
|
reactionCountLayoutAndApply = layoutAndApply
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if arguments.replyCount > 0 {
|
if arguments.replyCount > 0 {
|
||||||
let countString: String
|
let countString: String
|
||||||
@ -599,7 +636,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
case let .trailingContent(contentWidth, reactionSettings):
|
case let .trailingContent(contentWidth, reactionSettings):
|
||||||
if let _ = reactionSettings {
|
if let reactionSettings = reactionSettings, !reactionSettings.displayInline {
|
||||||
reactionButtons = reactionButtonsContainer.update(
|
reactionButtons = reactionButtonsContainer.update(
|
||||||
context: arguments.context,
|
context: arguments.context,
|
||||||
action: { value in
|
action: { value in
|
||||||
@ -686,11 +723,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
resultingHeight = 0.0
|
resultingHeight = 0.0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
var additionalVerticalInset: CGFloat = 0.0
|
||||||
if let reactionSettings = reactionSettings {
|
if let reactionSettings = reactionSettings {
|
||||||
if reactionSettings.preferAdditionalInset {
|
if reactionSettings.preferAdditionalInset {
|
||||||
verticalReactionsInset = 5.0
|
verticalReactionsInset = 8.0
|
||||||
|
additionalVerticalInset += 1.0
|
||||||
} else {
|
} else {
|
||||||
verticalReactionsInset = 2.0
|
verticalReactionsInset = 3.0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
verticalReactionsInset = 0.0
|
verticalReactionsInset = 0.0
|
||||||
@ -698,12 +737,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
|
|
||||||
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
|
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
|
||||||
resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
|
resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
|
||||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height
|
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0 + layoutSize.height
|
||||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height
|
verticalInset = verticalReactionsInset + reactionButtonsSize.height + 3.0
|
||||||
} else {
|
} else {
|
||||||
resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
|
resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
|
||||||
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height
|
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height + additionalVerticalInset
|
||||||
resultingHeight = verticalReactionsInset + reactionButtonsSize.height
|
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -717,7 +756,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
strongSelf.type = arguments.type
|
strongSelf.type = arguments.type
|
||||||
strongSelf.layoutSize = layoutSize
|
strongSelf.layoutSize = layoutSize
|
||||||
|
|
||||||
var reactionButtonPosition = CGPoint(x: 0.0, y: verticalReactionsInset)
|
var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset)
|
||||||
for item in reactionButtons.items {
|
for item in reactionButtons.items {
|
||||||
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
||||||
reactionButtonPosition.x = 0.0
|
reactionButtonPosition.x = 0.0
|
||||||
@ -773,7 +812,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
|
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
|
||||||
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
|
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
|
||||||
animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
|
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 {
|
} else {
|
||||||
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
|
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
|
||||||
strongSelf.blurredBackgroundNode = blurredBackgroundNode
|
strongSelf.blurredBackgroundNode = blurredBackgroundNode
|
||||||
@ -789,6 +828,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
let _ = dateApply()
|
let _ = dateApply()
|
||||||
|
|
||||||
if let currentImpressionIcon = currentImpressionIcon {
|
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
|
currentImpressionIcon.displaysAsynchronously = false
|
||||||
if currentImpressionIcon.image !== impressionImage {
|
if currentImpressionIcon.image !== impressionImage {
|
||||||
currentImpressionIcon.image = impressionImage
|
currentImpressionIcon.image = impressionImage
|
||||||
@ -796,8 +836,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
if currentImpressionIcon.supernode == nil {
|
if currentImpressionIcon.supernode == nil {
|
||||||
strongSelf.impressionIcon = currentImpressionIcon
|
strongSelf.impressionIcon = currentImpressionIcon
|
||||||
strongSelf.addSubnode(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 {
|
} else if let impressionIcon = strongSelf.impressionIcon {
|
||||||
impressionIcon.removeFromSupernode()
|
impressionIcon.removeFromSupernode()
|
||||||
strongSelf.impressionIcon = nil
|
strongSelf.impressionIcon = nil
|
||||||
@ -908,38 +950,49 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left
|
var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left
|
||||||
if !"".isEmpty {
|
if arguments.layoutInput.displayInlineReactions {
|
||||||
for i in 0 ..< arguments.reactions.count {
|
var validReactions = Set<String>()
|
||||||
|
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
|
let node: StatusReactionNode
|
||||||
var animateNode = true
|
var animateNode = true
|
||||||
if strongSelf.reactionNodes.count > i {
|
if let current = strongSelf.reactionNodes[reaction.value] {
|
||||||
node = strongSelf.reactionNodes[i]
|
node = current
|
||||||
} else {
|
} else {
|
||||||
animateNode = false
|
animateNode = false
|
||||||
node = StatusReactionNode()
|
node = StatusReactionNode()
|
||||||
if strongSelf.reactionNodes.count > i {
|
strongSelf.reactionNodes[reaction.value] = node
|
||||||
let previousNode = strongSelf.reactionNodes[i]
|
}
|
||||||
if animation.isAnimated {
|
validReactions.insert(reaction.value)
|
||||||
previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in
|
|
||||||
previousNode?.removeFromSupernode()
|
var iconFile: TelegramMediaFile?
|
||||||
})
|
|
||||||
} else {
|
if let availableReactions = arguments.availableReactions {
|
||||||
previousNode.removeFromSupernode()
|
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 {
|
if node.supernode == nil {
|
||||||
strongSelf.addSubnode(node)
|
strongSelf.addSubnode(node)
|
||||||
if animation.isAnimated {
|
if animation.isAnimated {
|
||||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
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 {
|
if animateNode {
|
||||||
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
|
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
|
||||||
} else {
|
} else {
|
||||||
@ -951,8 +1004,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
reactionOffset += reactionTrailingSpacing
|
reactionOffset += reactionTrailingSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count {
|
var removeIds: [String] = []
|
||||||
let node = strongSelf.reactionNodes.removeLast()
|
for (id, node) in strongSelf.reactionNodes {
|
||||||
|
if !validReactions.contains(id) {
|
||||||
|
removeIds.append(id)
|
||||||
if animation.isAnimated {
|
if animation.isAnimated {
|
||||||
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
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.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||||
@ -963,6 +1018,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for id in removeIds {
|
||||||
|
strongSelf.reactionNodes.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let (layout, apply) = reactionCountLayoutAndApply {
|
if let (layout, apply) = reactionCountLayoutAndApply {
|
||||||
let node = apply()
|
let node = apply()
|
||||||
@ -974,7 +1033,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
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)
|
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
|
||||||
reactionOffset += 1.0 + layout.size.width + 4.0
|
reactionOffset += 1.0 + layout.size.width + 4.0
|
||||||
} else if let reactionCountNode = strongSelf.reactionCountNode {
|
} else if let reactionCountNode = strongSelf.reactionCountNode {
|
||||||
@ -1068,6 +1127,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reactionView(value: String) -> UIView? {
|
func reactionView(value: String) -> UIView? {
|
||||||
|
for (id, node) in self.reactionNodes {
|
||||||
|
if id == value {
|
||||||
|
return node.iconView
|
||||||
|
}
|
||||||
|
}
|
||||||
for (_, button) in self.reactionButtonsContainer.buttons {
|
for (_, button) in self.reactionButtonsContainer.buttons {
|
||||||
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View {
|
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View {
|
||||||
return result.iconView
|
return result.iconView
|
||||||
@ -1092,3 +1156,10 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -61,7 +61,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
guard let strongSelf = self, let item = strongSelf.item else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
private var replyBackgroundNode: NavigationBackgroundNode?
|
private var replyBackgroundNode: NavigationBackgroundNode?
|
||||||
|
|
||||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||||
|
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||||
|
|
||||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||||
|
|
||||||
@ -255,6 +256,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
let currentForwardBackgroundNode = self.forwardBackgroundNode
|
let currentForwardBackgroundNode = self.forwardBackgroundNode
|
||||||
|
|
||||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||||
|
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||||
|
|
||||||
let currentItem = self.appliedItem
|
let currentItem = self.appliedItem
|
||||||
let currentForwardInfo = self.appliedForwardInfo
|
let currentForwardInfo = self.appliedForwardInfo
|
||||||
@ -527,22 +529,54 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
}
|
}
|
||||||
|
|
||||||
var maxContentWidth = normalDisplaySize.width
|
var maxContentWidth = normalDisplaySize.width
|
||||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||||
if let replyMarkup = replyMarkup {
|
if let replyMarkup = replyMarkup {
|
||||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
||||||
maxContentWidth = max(maxContentWidth, minWidth)
|
maxContentWidth = max(maxContentWidth, minWidth)
|
||||||
actionButtonsFinalize = buttonsLayout
|
actionButtonsFinalize = buttonsLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
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)
|
var layoutSize = CGSize(width: params.width, height: videoLayout.contentSize.height)
|
||||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
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
|
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -577,7 +611,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
let animating = (currentItem != nil && currentPlaying != isPlaying) || strongSelf.animatingHeight
|
let animating = (currentItem != nil && currentPlaying != isPlaying) || strongSelf.animatingHeight
|
||||||
if !animating {
|
if !animating {
|
||||||
strongSelf.interactiveVideoNode.frame = videoFrame
|
strongSelf.interactiveVideoNode.frame = videoFrame
|
||||||
videoApply(videoLayoutData, transition)
|
videoApply(videoLayoutData, animation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentPlaying != isPlaying {
|
if currentPlaying != isPlaying {
|
||||||
@ -750,14 +784,38 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
} 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 {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
var animated = false
|
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||||
if let _ = strongSelf.actionButtonsNode {
|
|
||||||
if case .System = animation {
|
|
||||||
animated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
|
||||||
let previousFrame = actionButtonsNode.frame
|
let previousFrame = actionButtonsNode.frame
|
||||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY), size: actionButtonsSizeAndApply.0)
|
let actionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY), size: actionButtonsSizeAndApply.0)
|
||||||
actionButtonsNode.frame = actionButtonsFrame
|
actionButtonsNode.frame = actionButtonsFrame
|
||||||
@ -784,6 +842,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
strongSelf.actionButtonsNode = nil
|
strongSelf.actionButtonsNode = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let (_, f) = strongSelf.awaitingAppliedReaction {
|
||||||
|
strongSelf.awaitingAppliedReaction = nil
|
||||||
|
|
||||||
|
f()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1196,7 +1260,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
} else {
|
} else {
|
||||||
videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0)
|
videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0)
|
||||||
}
|
}
|
||||||
videoApply(videoLayoutData, .immediate)
|
videoApply(videoLayoutData, .None)
|
||||||
|
|
||||||
if let shareButtonNode = self.shareButtonNode {
|
if let shareButtonNode = self.shareButtonNode {
|
||||||
let buttonSize = shareButtonNode.frame.size
|
let buttonSize = shareButtonNode.frame.size
|
||||||
@ -1247,6 +1311,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func targetReactionView(value: String) -> UIView? {
|
override func targetReactionView(value: String) -> UIView? {
|
||||||
|
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
if !self.interactiveVideoNode.dateAndStatusNode.isHidden {
|
if !self.interactiveVideoNode.dateAndStatusNode.isHidden {
|
||||||
return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value)
|
return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value)
|
||||||
}
|
}
|
||||||
|
@ -455,7 +455,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
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,
|
constrainedSize: constrainedSize,
|
||||||
availableReactions: associatedData.availableReactions,
|
availableReactions: associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
@ -1132,9 +1132,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
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) {
|
if self.dateAndStatusNode.supernode != nil {
|
||||||
|
if let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
if !self.dateAndStatusNode.frame.height.isZero {
|
||||||
|
if self.dateAndStatusNode.frame.contains(point) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return super.hitTest(point, with: event)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
self.view.addGestureRecognizer(recognizer)
|
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 previousFile = self.media
|
||||||
|
|
||||||
let currentItem = self.item
|
let currentItem = self.item
|
||||||
@ -296,7 +296,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone,
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
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)))
|
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 {
|
if let strongSelf = self {
|
||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
strongSelf.videoFrame = displayVideoFrame
|
strongSelf.videoFrame = displayVideoFrame
|
||||||
@ -362,7 +362,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dateAndStatusApply(.None)
|
dateAndStatusApply(animation)
|
||||||
switch layoutData {
|
switch layoutData {
|
||||||
case let .unconstrained(width):
|
case let .unconstrained(width):
|
||||||
let dateAndStatusOrigin: CGPoint
|
let dateAndStatusOrigin: CGPoint
|
||||||
@ -371,9 +371,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
} else {
|
} else {
|
||||||
dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height)
|
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)
|
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil)
|
||||||
case let .constrained(_, right):
|
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)
|
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<MediaPlayerStatus?, NoError>?
|
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
|
||||||
@ -847,11 +847,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
return nil
|
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()
|
let makeLayout = node?.asyncLayout()
|
||||||
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload in
|
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload in
|
||||||
var createdNode: ChatMessageInteractiveInstantVideoNode?
|
var createdNode: ChatMessageInteractiveInstantVideoNode?
|
||||||
let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void)
|
let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void)
|
||||||
if let makeLayout = makeLayout {
|
if let makeLayout = makeLayout {
|
||||||
sizeAndApplyLayout = makeLayout(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload)
|
sizeAndApplyLayout = makeLayout(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload)
|
||||||
} else {
|
} else {
|
||||||
|
@ -91,7 +91,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
}
|
}
|
||||||
let dateAndStatusNode: ChatMessageDateAndStatusNode
|
let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||||
private var badgeNode: ChatMessageInteractiveMediaBadge?
|
private var badgeNode: ChatMessageInteractiveMediaBadge?
|
||||||
private var tapRecognizer: UITapGestureRecognizer?
|
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||||
|
|
||||||
private var context: AccountContext?
|
private var context: AccountContext?
|
||||||
private var message: Message?
|
private var message: Message?
|
||||||
@ -150,6 +150,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
|
|
||||||
var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in }
|
var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in }
|
||||||
var activatePinch: ((PinchSourceContainerNode) -> Void)?
|
var activatePinch: ((PinchSourceContainerNode) -> Void)?
|
||||||
|
var updateMessageReaction: ((Message, ChatControllerInteractionReaction) -> Void)?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.pinchContainerNode = PinchSourceContainerNode()
|
self.pinchContainerNode = PinchSourceContainerNode()
|
||||||
@ -267,9 +268,18 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))
|
||||||
self.imageNode.view.addGestureRecognizer(tapRecognizer)
|
recognizer.tapActionAtPoint = { [weak self] point in
|
||||||
self.tapRecognizer = tapRecognizer
|
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) {
|
private func progressPressed(canActivate: Bool) {
|
||||||
@ -320,9 +330,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func imageTap(_ recognizer: UITapGestureRecognizer) {
|
@objc func imageTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state {
|
if case .ended = recognizer.state {
|
||||||
let point = recognizer.location(in: self.imageNode.view)
|
if let (gesture, point) = recognizer.lastRecognizedGestureAndLocation, let message = self.message {
|
||||||
|
if case .doubleTap = gesture {
|
||||||
|
if canAddMessageReactions(message: message) {
|
||||||
|
self.updateMessageReaction?(message, .default)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if let _ = self.attributes?.updatingMedia {
|
if let _ = self.attributes?.updatingMedia {
|
||||||
if let statusNode = self.statusNode, statusNode.frame.contains(point) {
|
if let statusNode = self.statusNode, statusNode.frame.contains(point) {
|
||||||
self.progressPressed(canActivate: true)
|
self.progressPressed(canActivate: true)
|
||||||
@ -344,6 +359,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||||
let currentMessage = self.message
|
let currentMessage = self.message
|
||||||
@ -475,7 +492,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
impressionCount: dateAndStatus.viewCount,
|
impressionCount: dateAndStatus.viewCount,
|
||||||
dateText: dateAndStatus.dateText,
|
dateText: dateAndStatus.dateText,
|
||||||
type: dateAndStatus.type,
|
type: dateAndStatus.type,
|
||||||
layoutInput: .standalone,
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: associatedData.availableReactions,
|
availableReactions: associatedData.availableReactions,
|
||||||
reactions: dateAndStatus.dateReactions,
|
reactions: dateAndStatus.dateReactions,
|
||||||
@ -868,15 +885,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
imageApply()
|
imageApply()
|
||||||
|
|
||||||
if let statusApply = statusApply {
|
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 {
|
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||||
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
|
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
|
||||||
}
|
statusApply(.None)
|
||||||
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.frame = dateAndStatusFrame
|
||||||
strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size)
|
} else {
|
||||||
|
transition.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||||
|
statusApply(transition)
|
||||||
|
}
|
||||||
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
} else if strongSelf.dateAndStatusNode.supernode != nil {
|
||||||
strongSelf.dateAndStatusNode.removeFromSupernode()
|
strongSelf.dateAndStatusNode.removeFromSupernode()
|
||||||
}
|
}
|
||||||
|
@ -252,7 +252,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone,
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
|
@ -52,6 +52,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
|
self.interactiveImageNode.activatePinch = { [weak self] sourceNode in
|
||||||
guard let strongSelf = self, let _ = strongSelf.item else {
|
guard let strongSelf = self, let _ = strongSelf.item else {
|
||||||
return
|
return
|
||||||
|
@ -1072,7 +1072,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .trailingContent(contentWidth: 100.0, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: true)),
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: textConstrainedSize,
|
constrainedSize: textConstrainedSize,
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
|
@ -19,6 +19,11 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
|||||||
case freeform
|
case freeform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DisplayAlignment {
|
||||||
|
case left
|
||||||
|
case right
|
||||||
|
}
|
||||||
|
|
||||||
private let container: ReactionButtonsLayoutContainer
|
private let container: ReactionButtonsLayoutContainer
|
||||||
var reactionSelected: ((String) -> Void)?
|
var reactionSelected: ((String) -> Void)?
|
||||||
|
|
||||||
@ -33,22 +38,32 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
|||||||
presentationData: ChatPresentationData,
|
presentationData: ChatPresentationData,
|
||||||
availableReactions: AvailableReactions?,
|
availableReactions: AvailableReactions?,
|
||||||
reactions: ReactionsMessageAttribute,
|
reactions: ReactionsMessageAttribute,
|
||||||
|
alignment: DisplayAlignment,
|
||||||
constrainedWidth: CGFloat,
|
constrainedWidth: CGFloat,
|
||||||
type: DisplayType
|
type: DisplayType
|
||||||
) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
|
) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) {
|
||||||
let reactionColors: ReactionButtonComponent.Colors
|
let reactionColors: ReactionButtonComponent.Colors
|
||||||
switch type {
|
switch type {
|
||||||
case .incoming, .freeform:
|
case .incoming:
|
||||||
reactionColors = ReactionButtonComponent.Colors(
|
reactionColors = ReactionButtonComponent.Colors(
|
||||||
background: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
deselectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||||
foreground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
selectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||||
stroke: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb
|
deselectedForeground: presentationData.theme.theme.chat.message.incoming.accentTextColor.argb,
|
||||||
|
selectedForeground: presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb
|
||||||
)
|
)
|
||||||
case .outgoing:
|
case .outgoing:
|
||||||
reactionColors = ReactionButtonComponent.Colors(
|
reactionColors = ReactionButtonComponent.Colors(
|
||||||
background: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
deselectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb,
|
||||||
foreground: presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb,
|
selectedBackground: presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb,
|
||||||
stroke: presentationData.theme.theme.chat.message.outgoing.accentTextColor.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
|
let bottomInset: CGFloat = 2.0
|
||||||
|
|
||||||
return (proposedWidth: reactionButtonsSize.width, continueLayout: { [weak self] boundingWidth in
|
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 {
|
guard let strongSelf = self else {
|
||||||
return
|
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 {
|
for item in reactionButtons.items {
|
||||||
|
switch alignment {
|
||||||
|
case .left:
|
||||||
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
||||||
reactionButtonPosition.x = 0.0
|
reactionButtonPosition.x = 0.0
|
||||||
reactionButtonPosition.y += item.size.height + 6.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 {
|
if item.view.superview == nil {
|
||||||
strongSelf.view.addSubview(item.view)
|
strongSelf.view.addSubview(item.view)
|
||||||
item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size)
|
|
||||||
if animation.isAnimated {
|
if animation.isAnimated {
|
||||||
item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
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.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
}
|
}
|
||||||
|
item.view.frame = itemFrame
|
||||||
} else {
|
} 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 {
|
for view in reactionButtons.removedViews {
|
||||||
@ -163,9 +202,15 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateOut() {
|
func animateIn(animation: ListViewItemUpdateAnimation) {
|
||||||
for (_, button) in self.container.buttons {
|
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 {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return
|
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
|
let topOffset: CGFloat
|
||||||
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
|
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
|
||||||
//displaySeparator = false
|
//displaySeparator = false
|
||||||
topOffset = 2.0
|
topOffset = 4.0
|
||||||
} else {
|
} else {
|
||||||
//displaySeparator = true
|
//displaySeparator = true
|
||||||
topOffset = 0.0
|
topOffset = 0.0
|
||||||
@ -213,7 +258,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
|||||||
let buttonsUpdate = buttonsNode.prepareUpdate(
|
let buttonsUpdate = buttonsNode.prepareUpdate(
|
||||||
context: item.context,
|
context: item.context,
|
||||||
presentationData: item.presentationData,
|
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
|
return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in
|
||||||
var boundingSize = CGSize()
|
var boundingSize = CGSize()
|
||||||
@ -250,7 +295,7 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
|||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
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) {
|
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
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||||
completion()
|
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 {
|
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 .ignore
|
||||||
}
|
}
|
||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
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 result
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -284,3 +329,96 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
|
|||||||
return self.buttonsNode.reactionTargetView(value: value)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -120,7 +120,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
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,
|
constrainedSize: textConstrainedSize,
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
|
@ -45,6 +45,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
private var replyBackgroundNode: NavigationBackgroundNode?
|
private var replyBackgroundNode: NavigationBackgroundNode?
|
||||||
|
|
||||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||||
|
private var reactionButtonsNode: ChatMessageReactionButtonsNode?
|
||||||
|
|
||||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||||
|
|
||||||
@ -178,7 +179,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return .waitForSingleTap
|
return .waitForDoubleTap
|
||||||
}
|
}
|
||||||
recognizer.longTap = { [weak self] point, recognizer in
|
recognizer.longTap = { [weak self] point, recognizer in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -299,6 +300,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
let imageLayout = self.imageNode.asyncLayout()
|
let imageLayout = self.imageNode.asyncLayout()
|
||||||
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
||||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||||
|
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||||
let textLayout = TextNode.asyncLayout(self.textNode)
|
let textLayout = TextNode.asyncLayout(self.textNode)
|
||||||
|
|
||||||
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
||||||
@ -490,7 +492,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone,
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
@ -619,22 +621,54 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var maxContentWidth = imageSize.width
|
var maxContentWidth = imageSize.width
|
||||||
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
|
||||||
if let replyMarkup = replyMarkup {
|
if let replyMarkup = replyMarkup {
|
||||||
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
||||||
maxContentWidth = max(maxContentWidth, minWidth)
|
maxContentWidth = max(maxContentWidth, minWidth)
|
||||||
actionButtonsFinalize = buttonsLayout
|
actionButtonsFinalize = buttonsLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
var actionButtonsSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)?
|
||||||
if let actionButtonsFinalize = actionButtonsFinalize {
|
if let actionButtonsFinalize = actionButtonsFinalize {
|
||||||
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
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)
|
var layoutSize = CGSize(width: params.width, height: contentHeight)
|
||||||
if isEmoji && !incoming {
|
if isEmoji && !incoming {
|
||||||
layoutSize.height += dateAndStatusSize.height
|
layoutSize.height += dateAndStatusSize.height
|
||||||
}
|
}
|
||||||
|
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||||
|
layoutSize.height += reactionButtonsSizeAndApply.0.height
|
||||||
|
}
|
||||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
layoutSize.height += actionButtonsSizeAndApply.0.height
|
layoutSize.height += actionButtonsSizeAndApply.0.height
|
||||||
}
|
}
|
||||||
@ -740,9 +774,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
||||||
|
|
||||||
dateAndStatusApply(.None)
|
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||||
|
dateAndStatusApply(animation)
|
||||||
transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: dateAndStatusFrame)
|
|
||||||
|
|
||||||
if let updatedShareButtonNode = updatedShareButtonNode {
|
if let updatedShareButtonNode = updatedShareButtonNode {
|
||||||
if updatedShareButtonNode !== strongSelf.shareButtonNode {
|
if updatedShareButtonNode !== strongSelf.shareButtonNode {
|
||||||
@ -902,13 +935,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
||||||
var animated = false
|
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
|
||||||
if let _ = strongSelf.actionButtonsNode {
|
|
||||||
if case .System = animation {
|
|
||||||
animated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
|
||||||
let previousFrame = actionButtonsNode.frame
|
let previousFrame = actionButtonsNode.frame
|
||||||
let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: actionButtonsSizeAndApply.0)
|
let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: actionButtonsSizeAndApply.0)
|
||||||
actionButtonsNode.frame = actionButtonsFrame
|
actionButtonsNode.frame = actionButtonsFrame
|
||||||
@ -935,6 +962,36 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
strongSelf.actionButtonsNode = nil
|
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) {
|
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||||
strongSelf.dateAndStatusNode.pressed = {
|
strongSelf.dateAndStatusNode.pressed = {
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -963,7 +1020,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
if case .doubleTap = gesture {
|
if case .doubleTap = gesture {
|
||||||
self.containerNode.cancelGesture()
|
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 {
|
if case .doubleTap = gesture {
|
||||||
self.containerNode.cancelGesture()
|
self.containerNode.cancelGesture()
|
||||||
}
|
}
|
||||||
@ -973,7 +1030,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
case let .optionalAction(f):
|
case let .optionalAction(f):
|
||||||
f()
|
f()
|
||||||
case let .openContextMenu(tapMessage, selectAll, subFrame):
|
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 {
|
} else if case .tap = gesture {
|
||||||
self.item?.controllerInteraction.clickThroughMessage()
|
self.item?.controllerInteraction.clickThroughMessage()
|
||||||
@ -1189,6 +1250,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
return shareButtonNode.view
|
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)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1496,6 +1563,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func targetReactionView(value: String) -> UIView? {
|
override func targetReactionView(value: String) -> UIView? {
|
||||||
|
if let result = self.reactionButtonsNode?.reactionTargetView(value: value) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
if !self.dateAndStatusNode.isHidden {
|
if !self.dateAndStatusNode.isHidden {
|
||||||
return self.dateAndStatusNode.reactionView(value: value)
|
return self.dateAndStatusNode.reactionView(value: value)
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
guard let strongSelf = self, let item = strongSelf.item else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.controllerInteraction.updateMessageReaction(item.message, value)
|
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +292,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
isReplyThread = true
|
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(
|
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||||
context: item.context,
|
context: item.context,
|
||||||
presentationData: item.presentationData,
|
presentationData: item.presentationData,
|
||||||
@ -299,7 +302,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.ReactionSettings(preferAdditionalInset: false)),
|
layoutInput: dateLayoutInput,
|
||||||
constrainedSize: textConstrainedSize,
|
constrainedSize: textConstrainedSize,
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactions,
|
reactions: dateReactions,
|
||||||
|
@ -544,9 +544,6 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func reactionTargetView(value: String) -> UIView? {
|
override func reactionTargetView(value: String) -> UIView? {
|
||||||
if !self.contentNode.statusNode.isHidden {
|
return self.contentNode.reactionTargetView(value: value)
|
||||||
return self.contentNode.statusNode.reactionView(value: value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -941,7 +941,7 @@ private final class ItemView: UIView, SparseItemGridView {
|
|||||||
let messageItemNode: ListViewItemNode
|
let messageItemNode: ListViewItemNode
|
||||||
if let current = self.messageItemNode {
|
if let current = self.messageItemNode {
|
||||||
messageItemNode = current
|
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.contentSize = layout.contentSize
|
||||||
current.insets = layout.insets
|
current.insets = layout.insets
|
||||||
|
|
||||||
@ -972,7 +972,7 @@ private final class ItemView: UIView, SparseItemGridView {
|
|||||||
|
|
||||||
func update(size: CGSize, insets: UIEdgeInsets) {
|
func update(size: CGSize, insets: UIEdgeInsets) {
|
||||||
if let messageItem = self.messageItem, let messageItemNode = self.messageItemNode {
|
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.contentSize = layout.contentSize
|
||||||
messageItemNode.insets = layout.insets
|
messageItemNode.insets = layout.insets
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user