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
9f64981aca
commit
7c8315218a
@ -46,7 +46,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
var defaultReaction: String?
|
||||
if let availableReactions = availableReactions {
|
||||
for reaction in availableReactions.reactions {
|
||||
if reaction.title.lowercased().contains("heart") {
|
||||
if reaction.title.lowercased().contains("thumbs up") {
|
||||
defaultReaction = reaction.value
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ swift_library(
|
||||
"//submodules/WebPBinding:WebPBinding",
|
||||
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/AvatarNode:AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -11,36 +11,793 @@ import UIKit
|
||||
import WebPBinding
|
||||
import AnimatedAvatarSetNode
|
||||
import ContextUI
|
||||
import AvatarNode
|
||||
|
||||
public final class ReactionListContextMenuContent: ContextControllerItemsContent {
|
||||
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode {
|
||||
private let contentNode: ASDisplayNode
|
||||
private final class ReactionImageNode: ASImageNode {
|
||||
private var disposable: Disposable?
|
||||
let size: CGSize
|
||||
|
||||
override init() {
|
||||
self.contentNode = ASDisplayNode()
|
||||
init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) {
|
||||
var file: TelegramMediaFile?
|
||||
if let availableReactions = availableReactions {
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if availableReaction.value == reaction {
|
||||
file = availableReaction.staticIcon
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if let file = file {
|
||||
self.size = file.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
//self.contentNode.backgroundColor = .blue
|
||||
self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) {
|
||||
let size = CGSize(width: min(260.0, constrainedWidth), height: maxHeight)
|
||||
|
||||
let contentSize = CGSize(width: size.width, height: size.height + bottomInset + 14.0)
|
||||
//contentSize.height = 120.0
|
||||
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
|
||||
return (size, contentSize)
|
||||
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
if let image = WebP.convert(fromWebP: dataValue) {
|
||||
strongSelf.image = image
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.size = CGSize(width: 18.0, height: 18.0)
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
public func node() -> ContextControllerItemsNode {
|
||||
return ItemsNode()
|
||||
private let avatarFont = avatarPlaceholderFont(size: 16.0)
|
||||
|
||||
public final class ReactionListContextMenuContent: ContextControllerItemsContent {
|
||||
private final class BackButtonNode: HighlightTrackingButtonNode {
|
||||
let highlightBackgroundNode: ASDisplayNode
|
||||
let titleLabelNode: ImmediateTextNode
|
||||
let separatorNode: ASDisplayNode
|
||||
let iconNode: ASImageNode
|
||||
|
||||
var action: (() -> Void)?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
init() {
|
||||
self.highlightBackgroundNode = ASDisplayNode()
|
||||
self.highlightBackgroundNode.alpha = 0.0
|
||||
|
||||
self.titleLabelNode = ImmediateTextNode()
|
||||
self.titleLabelNode.maximumNumberOfLines = 1
|
||||
self.titleLabelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.highlightBackgroundNode)
|
||||
self.addSubnode(self.titleLabelNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
strongSelf.highlightBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
|
||||
strongSelf.highlightBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.action?()
|
||||
}
|
||||
|
||||
func update(size: CGSize, presentationData: PresentationData, isLast: Bool) {
|
||||
let standardIconWidth: CGFloat = 32.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
let iconSideInset: CGFloat = 12.0
|
||||
|
||||
if self.theme !== presentationData.theme {
|
||||
self.theme = presentationData.theme
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: presentationData.theme.contextMenu.primaryColor)
|
||||
}
|
||||
|
||||
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
|
||||
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.titleLabelNode.attributedText = NSAttributedString(string: presentationData.strings.Common_Back, font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
|
||||
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: size.width - sideInset - standardIconWidth, height: 100.0))
|
||||
self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
if let iconImage = self.iconNode.image {
|
||||
let iconWidth = max(standardIconWidth, iconImage.size.width)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size)
|
||||
self.iconNode.frame = iconFrame
|
||||
}
|
||||
|
||||
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))
|
||||
self.separatorNode.isHidden = isLast
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReactionTabListNode: ASDisplayNode {
|
||||
private final class ItemNode: ASDisplayNode {
|
||||
let context: AccountContext
|
||||
let reaction: String?
|
||||
let count: Int
|
||||
|
||||
let titleLabelNode: ImmediateTextNode
|
||||
let iconNode: ASImageNode?
|
||||
let reactionIconNode: ReactionImageNode?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
var action: ((String?) -> Void)?
|
||||
|
||||
init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String?, count: Int) {
|
||||
self.context = context
|
||||
self.reaction = reaction
|
||||
self.count = count
|
||||
|
||||
self.titleLabelNode = ImmediateTextNode()
|
||||
self.titleLabelNode.isUserInteractionEnabled = false
|
||||
|
||||
if let reaction = reaction {
|
||||
self.reactionIconNode = ReactionImageNode(context: context, availableReactions: availableReactions, reaction: reaction)
|
||||
self.reactionIconNode?.isUserInteractionEnabled = false
|
||||
self.iconNode = nil
|
||||
} else {
|
||||
self.reactionIconNode = nil
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode?.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleLabelNode)
|
||||
if let iconNode = self.iconNode {
|
||||
self.addSubnode(iconNode)
|
||||
}
|
||||
if let reactionIconNode = self.reactionIconNode {
|
||||
self.addSubnode(reactionIconNode)
|
||||
}
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.action?(self.reaction)
|
||||
}
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, constrainedSize: CGSize, isSelected: Bool) -> CGSize {
|
||||
if presentationData.theme !== self.theme {
|
||||
self.theme = presentationData.theme
|
||||
|
||||
if let iconNode = self.iconNode {
|
||||
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.contextMenu.primaryColor)
|
||||
}
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 12.0
|
||||
let iconSpacing: CGFloat = 4.0
|
||||
|
||||
var iconSize = CGSize(width: 22.0, height: 22.0)
|
||||
if let reactionIconNode = self.reactionIconNode {
|
||||
iconSize = reactionIconNode.size.aspectFitted(iconSize)
|
||||
} else if let iconNode = self.iconNode, let image = iconNode.image {
|
||||
iconSize = image.size.aspectFitted(iconSize)
|
||||
}
|
||||
|
||||
self.titleLabelNode.attributedText = NSAttributedString(string: "\(count)", font: Font.medium(11.0), textColor: presentationData.theme.contextMenu.primaryColor)
|
||||
let titleSize = self.titleLabelNode.updateLayout(constrainedSize)
|
||||
|
||||
let contentSize = CGSize(width: sideInset * 2.0 + titleSize.width + iconSize.width + iconSpacing, height: titleSize.height)
|
||||
|
||||
self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floor((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
if let reactionIconNode = self.reactionIconNode {
|
||||
reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
} else if let iconNode = self.iconNode {
|
||||
iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
}
|
||||
|
||||
return CGSize(width: contentSize.width, height: constrainedSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private let selectionHighlightNode: ASDisplayNode
|
||||
private let itemNodes: [ItemNode]
|
||||
|
||||
var action: ((String?) -> Void)?
|
||||
|
||||
init(context: AccountContext, availableReactions: AvailableReactions?, reactions: [(String?, Int)], message: EngineMessage) {
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.itemNodes = reactions.map { reaction, count in
|
||||
return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, count: count)
|
||||
}
|
||||
|
||||
self.selectionHighlightNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
self.scrollNode.addSubnode(self.selectionHighlightNode)
|
||||
|
||||
for itemNode in self.itemNodes {
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
itemNode.action = { [weak self] reaction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.action?(reaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, presentationData: PresentationData, selectedReaction: String?, transition: ContainedViewLayoutTransition) {
|
||||
let sideInset: CGFloat = 16.0
|
||||
let spacing: CGFloat = 0.0
|
||||
let verticalInset: CGFloat = 6.0
|
||||
|
||||
self.selectionHighlightNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
||||
let highlightHeight: CGFloat = size.height - verticalInset * 2.0
|
||||
self.selectionHighlightNode.cornerRadius = highlightHeight / 2.0
|
||||
|
||||
var contentWidth: CGFloat = sideInset
|
||||
for i in 0 ..< self.itemNodes.count {
|
||||
if i != 0 {
|
||||
contentWidth += spacing
|
||||
}
|
||||
|
||||
let itemNode = self.itemNodes[i]
|
||||
let itemSize = itemNode.update(presentationData: presentationData, constrainedSize: CGSize(width: size.width, height: size.height), isSelected: itemNode.reaction == selectedReaction)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
|
||||
itemNode.frame = itemFrame
|
||||
|
||||
if itemNode.reaction == selectedReaction {
|
||||
transition.updateFrame(node: self.selectionHighlightNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: verticalInset), size: CGSize(width: itemFrame.width, height: highlightHeight)))
|
||||
}
|
||||
|
||||
contentWidth += itemSize.width
|
||||
}
|
||||
contentWidth += sideInset
|
||||
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let contentSize = CGSize(width: contentWidth, height: size.height)
|
||||
if self.scrollNode.view.contentSize != contentSize {
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReactionsTabNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private final class ItemNode: HighlightTrackingButtonNode {
|
||||
let context: AccountContext
|
||||
let availableReactions: AvailableReactions?
|
||||
let highlightBackgroundNode: ASDisplayNode
|
||||
let avatarNode: AvatarNode
|
||||
let titleLabelNode: ImmediateTextNode
|
||||
let separatorNode: ASDisplayNode
|
||||
var reactionIconNode: ReactionImageNode?
|
||||
let action: () -> Void
|
||||
|
||||
init(context: AccountContext, availableReactions: AvailableReactions?, action: @escaping () -> Void) {
|
||||
self.action = action
|
||||
self.context = context
|
||||
self.availableReactions = availableReactions
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
|
||||
self.highlightBackgroundNode = ASDisplayNode()
|
||||
self.highlightBackgroundNode.alpha = 0.0
|
||||
|
||||
self.titleLabelNode = ImmediateTextNode()
|
||||
self.titleLabelNode.maximumNumberOfLines = 1
|
||||
self.titleLabelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.highlightBackgroundNode)
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubnode(self.titleLabelNode)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
strongSelf.highlightBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
|
||||
strongSelf.highlightBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.action()
|
||||
}
|
||||
|
||||
func update(size: CGSize, presentationData: PresentationData, item: EngineMessageReactionListContext.Item, isLast: Bool, syncronousLoad: Bool) {
|
||||
let avatarInset: CGFloat = 10.0
|
||||
let avatarSpacing: CGFloat = 8.0
|
||||
let avatarSize: CGFloat = 28.0
|
||||
|
||||
let reaction: String? = item.reaction
|
||||
if let reaction = reaction {
|
||||
if self.reactionIconNode == nil {
|
||||
let reactionIconNode = ReactionImageNode(context: self.context, availableReactions: self.availableReactions, reaction: reaction)
|
||||
self.reactionIconNode = reactionIconNode
|
||||
self.addSubnode(reactionIconNode)
|
||||
}
|
||||
} else if let reactionIconNode = self.reactionIconNode {
|
||||
reactionIconNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
|
||||
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: item.peer, synchronousLoad: true)
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
self.titleLabelNode.attributedText = NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
|
||||
var maxTextWidth: CGFloat = size.width - avatarInset - avatarSize - avatarSpacing - sideInset
|
||||
if reactionIconNode != nil {
|
||||
maxTextWidth -= 32.0
|
||||
}
|
||||
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0))
|
||||
self.titleLabelNode.frame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
if let reactionIconNode = self.reactionIconNode {
|
||||
let reactionSize = reactionIconNode.size.aspectFitted(CGSize(width: 22.0, height: 22.0))
|
||||
reactionIconNode.frame = CGRect(origin: CGPoint(x: size.width - 32.0 - floor((32.0 - reactionSize.width) / 2.0), y: floor((size.height - reactionSize.height) / 2.0)), size: reactionSize)
|
||||
}
|
||||
|
||||
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel))
|
||||
self.separatorNode.isHidden = isLast
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let availableReactions: AvailableReactions?
|
||||
let reaction: String?
|
||||
private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
|
||||
private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void
|
||||
private let openPeer: (PeerId) -> Void
|
||||
|
||||
private var hasMore: Bool = false
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var presentationData: PresentationData?
|
||||
private var currentSize: CGSize?
|
||||
private var apparentHeight: CGFloat = 0.0
|
||||
|
||||
private let listContext: EngineMessageReactionListContext
|
||||
private var state: EngineMessageReactionListContext.State
|
||||
private var stateDisposable: Disposable?
|
||||
|
||||
private var itemNodes: [Int: ItemNode] = [:]
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
availableReactions: AvailableReactions?,
|
||||
message: EngineMessage,
|
||||
reaction: String?,
|
||||
requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void,
|
||||
openPeer: @escaping (PeerId) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.availableReactions = availableReactions
|
||||
self.reaction = reaction
|
||||
self.requestUpdate = requestUpdate
|
||||
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
||||
self.openPeer = openPeer
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
self.listContext = context.engine.messages.messageReactionList(message: message, reaction: reaction)
|
||||
self.state = EngineMessageReactionListContext.State(message: message, reaction: reaction)
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollNode.clipsToBounds = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
self.scrollNode.view.delegate = self
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.stateDisposable = (self.listContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var animateIn = false
|
||||
if strongSelf.state.items.isEmpty && !state.items.isEmpty {
|
||||
animateIn = true
|
||||
}
|
||||
strongSelf.state = state
|
||||
strongSelf.requestUpdate(strongSelf, .immediate)
|
||||
if animateIn {
|
||||
strongSelf.scrollNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if self.ignoreScrolling {
|
||||
return
|
||||
}
|
||||
self.updateVisibleItems(syncronousLoad: false)
|
||||
|
||||
if let size = self.currentSize {
|
||||
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
|
||||
apparentHeight = max(apparentHeight, 44.0)
|
||||
apparentHeight = min(apparentHeight, size.height + 100.0)
|
||||
if self.apparentHeight != apparentHeight {
|
||||
self.apparentHeight = apparentHeight
|
||||
|
||||
self.requestUpdateApparentHeight(self, .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibleItems(syncronousLoad: Bool) {
|
||||
guard let size = self.currentSize else {
|
||||
return
|
||||
}
|
||||
guard let presentationData = self.presentationData else {
|
||||
return
|
||||
}
|
||||
let itemHeight: CGFloat = 44.0
|
||||
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0)
|
||||
|
||||
var validIds = Set<Int>()
|
||||
|
||||
let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight)))
|
||||
let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight))
|
||||
|
||||
if minVisibleIndex <= maxVisibleIndex {
|
||||
for index in minVisibleIndex ... maxVisibleIndex {
|
||||
if index >= self.state.items.count {
|
||||
break
|
||||
}
|
||||
|
||||
validIds.insert(index)
|
||||
|
||||
let itemNode: ItemNode
|
||||
if let current = self.itemNodes[index] {
|
||||
itemNode = current
|
||||
} else {
|
||||
let openPeer = self.openPeer
|
||||
let peerId = self.state.items[index].peer.id
|
||||
itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: {
|
||||
openPeer(peerId)
|
||||
})
|
||||
self.itemNodes[index] = itemNode
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight))
|
||||
itemNode.update(size: itemFrame.size, presentationData: presentationData, item: self.state.items[index], isLast: index == self.state.items.count - 1, syncronousLoad: syncronousLoad)
|
||||
itemNode.frame = itemFrame
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [Int] = []
|
||||
for (id, itemNode) in self.itemNodes {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
itemNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
for id in removeIds {
|
||||
self.itemNodes.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if self.state.canLoadMore && maxVisibleIndex >= self.state.items.count - 16 {
|
||||
self.listContext.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
func update(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) {
|
||||
let itemHeight: CGFloat = 44.0
|
||||
let size = CGSize(width: constrainedSize.width, height: CGFloat(self.state.totalCount) * itemHeight)
|
||||
|
||||
let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height))
|
||||
self.currentSize = containerSize
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) {
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
}
|
||||
if self.scrollNode.view.contentSize != size {
|
||||
self.scrollNode.view.contentSize = size
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateVisibleItems(syncronousLoad: !transition.isAnimated)
|
||||
|
||||
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
|
||||
apparentHeight = max(apparentHeight, 44.0)
|
||||
apparentHeight = min(apparentHeight, containerSize.height + 100.0)
|
||||
self.apparentHeight = apparentHeight
|
||||
|
||||
return (containerSize, apparentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode {
|
||||
private let context: AccountContext
|
||||
private let availableReactions: AvailableReactions?
|
||||
private let reactions: [(String?, Int)]
|
||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private var backButtonNode: BackButtonNode?
|
||||
private var separatorNode: ASDisplayNode?
|
||||
private var tabListNode: ReactionTabListNode?
|
||||
private var currentTabNode: ReactionsTabNode
|
||||
|
||||
private var dismissedTabNode: ReactionsTabNode?
|
||||
|
||||
private let openPeer: (PeerId) -> Void
|
||||
|
||||
private(set) var apparentHeight: CGFloat = 0.0
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
availableReactions: AvailableReactions?,
|
||||
message: EngineMessage,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
back: @escaping () -> Void,
|
||||
openPeer: @escaping (PeerId) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.availableReactions = availableReactions
|
||||
self.openPeer = openPeer
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
self.requestUpdate = requestUpdate
|
||||
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
||||
|
||||
var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
|
||||
var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
self.backButtonNode = BackButtonNode()
|
||||
self.backButtonNode?.action = {
|
||||
back()
|
||||
}
|
||||
|
||||
var reactions: [(String?, Int)] = []
|
||||
var totalCount: Int = 0
|
||||
if let reactionsAttribute = message._asMessage().reactionsAttribute {
|
||||
for reaction in reactionsAttribute.reactions {
|
||||
totalCount += Int(reaction.count)
|
||||
reactions.append((reaction.value, Int(reaction.count)))
|
||||
}
|
||||
}
|
||||
reactions.insert((nil, totalCount), at: 0)
|
||||
|
||||
if reactions.count > 2 {
|
||||
self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message)
|
||||
}
|
||||
|
||||
self.reactions = reactions
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
|
||||
self.currentTabNode = ReactionsTabNode(
|
||||
context: context,
|
||||
availableReactions: availableReactions,
|
||||
message: message,
|
||||
reaction: nil,
|
||||
requestUpdate: { tab, transition in
|
||||
requestUpdateTab?(tab, transition)
|
||||
},
|
||||
requestUpdateApparentHeight: { tab, transition in
|
||||
requestUpdateTabApparentHeight?(tab, transition)
|
||||
},
|
||||
openPeer: { id in
|
||||
openPeer(id)
|
||||
}
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
if let backButtonNode = self.backButtonNode {
|
||||
self.addSubnode(backButtonNode)
|
||||
}
|
||||
if let tabListNode = self.tabListNode {
|
||||
self.addSubnode(tabListNode)
|
||||
}
|
||||
if let separatorNode = self.separatorNode {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
self.addSubnode(self.currentTabNode)
|
||||
|
||||
self.tabListNode?.action = { [weak self] reaction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.currentTabNode.reaction != reaction {
|
||||
strongSelf.dismissedTabNode = strongSelf.currentTabNode
|
||||
let currentTabNode = ReactionsTabNode(
|
||||
context: context,
|
||||
availableReactions: availableReactions,
|
||||
message: message,
|
||||
reaction: reaction,
|
||||
requestUpdate: { tab, transition in
|
||||
requestUpdateTab?(tab, transition)
|
||||
},
|
||||
requestUpdateApparentHeight: { tab, transition in
|
||||
requestUpdateTabApparentHeight?(tab, transition)
|
||||
},
|
||||
openPeer: { id in
|
||||
openPeer(id)
|
||||
}
|
||||
)
|
||||
strongSelf.currentTabNode = currentTabNode
|
||||
strongSelf.addSubnode(currentTabNode)
|
||||
strongSelf.requestUpdate(.animated(duration: 0.45, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
requestUpdateTab = { [weak self] tab, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.currentTabNode == tab {
|
||||
strongSelf.requestUpdate(transition)
|
||||
}
|
||||
}
|
||||
|
||||
requestUpdateTabApparentHeight = { [weak self] tab, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.currentTabNode == tab {
|
||||
strongSelf.requestUpdateApparentHeight(transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) {
|
||||
let constrainedSize = CGSize(width: min(260.0, constrainedWidth), height: maxHeight)
|
||||
|
||||
var topContentHeight: CGFloat = 0.0
|
||||
if let backButtonNode = self.backButtonNode {
|
||||
let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
|
||||
backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: self.tabListNode == nil)
|
||||
transition.updateFrame(node: backButtonNode, frame: backButtonFrame)
|
||||
topContentHeight += backButtonFrame.height
|
||||
}
|
||||
if let tabListNode = self.tabListNode {
|
||||
let tabListFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
|
||||
tabListNode.update(size: tabListFrame.size, presentationData: self.presentationData, selectedReaction: self.currentTabNode.reaction, transition: transition)
|
||||
transition.updateFrame(node: tabListNode, frame: tabListFrame)
|
||||
topContentHeight += tabListFrame.height
|
||||
}
|
||||
if let separatorNode = self.separatorNode {
|
||||
let separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 7.0))
|
||||
separatorNode.backgroundColor = self.presentationData.theme.contextMenu.sectionSeparatorColor
|
||||
transition.updateFrame(node: separatorNode, frame: separatorFrame)
|
||||
topContentHeight += separatorFrame.height
|
||||
}
|
||||
|
||||
var currentTabTransition = transition
|
||||
if self.currentTabNode.bounds.isEmpty {
|
||||
currentTabTransition = .immediate
|
||||
}
|
||||
let currentTabLayout = self.currentTabNode.update(constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition)
|
||||
currentTabTransition.updateFrame(node: self.currentTabNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: currentTabLayout.size.width, height: currentTabLayout.size.height + 100.0)))
|
||||
|
||||
if let dismissedTabNode = self.dismissedTabNode {
|
||||
self.dismissedTabNode = nil
|
||||
if let previousIndex = self.reactions.firstIndex(where: { $0.0 == dismissedTabNode.reaction }), let currentIndex = self.reactions.firstIndex(where: { $0.0 == self.currentTabNode.reaction }) {
|
||||
let offset = previousIndex < currentIndex ? currentTabLayout.size.width : -currentTabLayout.size.width
|
||||
transition.updateFrame(node: dismissedTabNode, frame: dismissedTabNode.frame.offsetBy(dx: -offset, dy: 0.0), completion: { [weak dismissedTabNode] _ in
|
||||
dismissedTabNode?.removeFromSupernode()
|
||||
})
|
||||
transition.animatePositionAdditive(node: self.currentTabNode, offset: CGPoint(x: offset, y: 0.0))
|
||||
} else {
|
||||
dismissedTabNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dismissedTabNode] _ in
|
||||
dismissedTabNode?.removeFromSupernode()
|
||||
})
|
||||
self.currentTabNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: currentTabLayout.size.width, height: topContentHeight + currentTabLayout.size.height)
|
||||
|
||||
let apparentHeight = topContentHeight + currentTabLayout.apparentHeight
|
||||
|
||||
return (contentSize, apparentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let availableReactions: AvailableReactions?
|
||||
let message: EngineMessage
|
||||
let back: () -> Void
|
||||
let openPeer: (PeerId) -> Void
|
||||
|
||||
public init(context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, back: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) {
|
||||
self.context = context
|
||||
self.availableReactions = availableReactions
|
||||
self.message = message
|
||||
self.back = back
|
||||
self.openPeer = openPeer
|
||||
}
|
||||
|
||||
public func node(
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) -> ContextControllerItemsNode {
|
||||
return ItemsNode(
|
||||
context: self.context,
|
||||
availableReactions: self.availableReactions,
|
||||
message: self.message,
|
||||
requestUpdate: requestUpdate,
|
||||
requestUpdateApparentHeight: requestUpdateApparentHeight,
|
||||
back: self.back,
|
||||
openPeer: self.openPeer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -41,43 +41,7 @@ private enum ContextItemNode {
|
||||
case separator(ASDisplayNode)
|
||||
}
|
||||
|
||||
private protocol ContextInnerActionsContainerNode: ASDisplayNode {
|
||||
var panSelectionGestureEnabled: Bool { get set }
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize)
|
||||
func updateTheme(presentationData: PresentationData)
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol?
|
||||
}
|
||||
|
||||
private final class InnerCustomActionsContainerNode: ASDisplayNode, ContextInnerActionsContainerNode {
|
||||
private let node: ContextControllerItemsNode
|
||||
|
||||
var panSelectionGestureEnabled: Bool = false
|
||||
|
||||
init(content: ContextControllerItemsContent) {
|
||||
self.node = content.node()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.node)
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) {
|
||||
let nodeLayout = self.node.update(constrainedWidth: constrainedWidth, maxHeight: constrainedHeight, bottomInset: bottomInset, transition: transition)
|
||||
transition.updateFrame(node: self.node, frame: CGRect(origin: CGPoint(), size: nodeLayout.cleanSize))
|
||||
return (nodeLayout.cleanSize, nodeLayout.visibleSize)
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerActionsContainerNode {
|
||||
private final class InnerActionsContainerNode: ASDisplayNode {
|
||||
private let blurBackground: Bool
|
||||
private let presentationData: PresentationData
|
||||
private let containerNode: ASDisplayNode
|
||||
@ -225,7 +189,7 @@ private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerAction
|
||||
gesture.isEnabled = self.panSelectionGestureEnabled
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) {
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var minActionsWidth: CGFloat = 250.0
|
||||
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
|
||||
minActionsWidth = minimalWidth
|
||||
@ -334,7 +298,7 @@ private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerAction
|
||||
if let effectView = self.effectView {
|
||||
transition.updateFrame(view: effectView, frame: bounds)
|
||||
}
|
||||
return (size, size)
|
||||
return size
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
@ -517,12 +481,9 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
private let shadowNode: ASImageNode
|
||||
private let additionalShadowNode: ASImageNode?
|
||||
private let additionalActionsNode: InnerActionsContainerNode?
|
||||
|
||||
private let contentContainerNode: ASDisplayNode
|
||||
|
||||
private let actionsNode: ContextInnerActionsContainerNode
|
||||
private let actionsNode: InnerActionsContainerNode
|
||||
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
||||
//private let scrollNode: ASScrollNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
var panSelectionGestureEnabled: Bool = true {
|
||||
didSet {
|
||||
@ -545,11 +506,6 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
self.shadowNode.contentMode = .scaleToFill
|
||||
self.shadowNode.isHidden = true
|
||||
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode.clipsToBounds = true
|
||||
self.contentContainerNode.cornerRadius = 14.0
|
||||
self.contentContainerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
|
||||
|
||||
var items = items
|
||||
if case var .list(itemList) = items.content, let firstItem = itemList.first, case let .custom(_, additional) = firstItem, additional {
|
||||
let additionalShadowNode = ASImageNode()
|
||||
@ -568,8 +524,11 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
self.additionalActionsNode = nil
|
||||
}
|
||||
|
||||
switch items.content {
|
||||
case let .list(itemList):
|
||||
var itemList: [ContextMenuItem] = []
|
||||
if case let .list(list) = items.content {
|
||||
itemList = list
|
||||
}
|
||||
|
||||
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: itemList, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||
if let tip = items.tip {
|
||||
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip)
|
||||
@ -578,64 +537,58 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
} else {
|
||||
self.textSelectionTipNode = nil
|
||||
}
|
||||
case let .custom(customContent):
|
||||
self.actionsNode = InnerCustomActionsContainerNode(content: customContent)
|
||||
self.textSelectionTipNode = nil
|
||||
}
|
||||
|
||||
/*self.scrollNode = ASScrollNode()
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.clipsToBounds = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}*/
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.shadowNode)
|
||||
self.additionalShadowNode.flatMap(self.addSubnode)
|
||||
self.additionalActionsNode.flatMap(self.contentContainerNode.addSubnode)
|
||||
self.contentContainerNode.addSubnode(self.actionsNode)
|
||||
self.textSelectionTipNode.flatMap(self.addSubnode)
|
||||
self.addSubnode(self.contentContainerNode)
|
||||
self.additionalActionsNode.flatMap(self.scrollNode.addSubnode)
|
||||
self.scrollNode.addSubnode(self.actionsNode)
|
||||
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var widthClass = widthClass
|
||||
if !self.blurBackground {
|
||||
widthClass = .regular
|
||||
}
|
||||
|
||||
var contentSize = CGSize()
|
||||
let actionsLayout = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, bottomInset: bottomInset, minimalWidth: nil, transition: transition)
|
||||
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, minimalWidth: nil, transition: transition)
|
||||
|
||||
if let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode {
|
||||
let additionalActionsLayout = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsLayout.cleanSize.width, constrainedHeight: constrainedHeight, bottomInset: 0.0, minimalWidth: actionsLayout.cleanSize.width, transition: transition)
|
||||
contentSize = additionalActionsLayout.cleanSize
|
||||
let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, constrainedHeight: constrainedHeight, minimalWidth: actionsSize.width, transition: transition)
|
||||
contentSize = additionalActionsSize
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: additionalActionsLayout.cleanSize)
|
||||
let bounds = CGRect(origin: CGPoint(), size: additionalActionsSize)
|
||||
transition.updateFrame(node: additionalShadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
|
||||
additionalShadowNode.isHidden = widthClass == .compact
|
||||
|
||||
transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsLayout.cleanSize))
|
||||
transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsSize))
|
||||
contentSize.height += 8.0
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsLayout.visibleSize)
|
||||
let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsSize)
|
||||
transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
|
||||
self.shadowNode.isHidden = widthClass == .compact
|
||||
|
||||
contentSize.width = max(contentSize.width, actionsLayout.cleanSize.width)
|
||||
contentSize.height += actionsLayout.cleanSize.height
|
||||
contentSize.width = max(contentSize.width, actionsSize.width)
|
||||
contentSize.height += actionsSize.height
|
||||
|
||||
transition.updateFrame(node: self.actionsNode, frame: bounds)
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
|
||||
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
contentSize.height += 8.0
|
||||
let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsLayout.cleanSize.width, transition: transition)
|
||||
let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsSize.width, transition: transition)
|
||||
transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize))
|
||||
contentSize.height += textSelectionTipSize.height
|
||||
}
|
||||
@ -644,8 +597,8 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func updateSize(containerSize: CGSize, contentSize: CGSize) {
|
||||
//self.scrollNode.view.contentSize = contentSize
|
||||
//self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {
|
||||
|
@ -11,13 +11,15 @@ import AccountContext
|
||||
|
||||
private let animationDurationFactor: Double = 1.0
|
||||
|
||||
public protocol ContextControllerProtocol {
|
||||
public protocol ContextControllerProtocol: AnyObject {
|
||||
var useComplexItemsTransitionAnimation: Bool { get set }
|
||||
var immediateItemsTransitionAnimation: Bool { get set }
|
||||
|
||||
func getActionsMinHeight() -> ContextController.ActionsHeight?
|
||||
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?)
|
||||
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition)
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>)
|
||||
func popItems()
|
||||
func dismiss(completion: (() -> Void)?)
|
||||
}
|
||||
|
||||
@ -158,7 +160,7 @@ public enum ContextMenuItem {
|
||||
case separator
|
||||
}
|
||||
|
||||
private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
||||
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
||||
let sourceWindowFrame = fromView.convert(frame, to: nil)
|
||||
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
|
||||
|
||||
@ -196,6 +198,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
private let dismissNode: ASDisplayNode
|
||||
private let dismissAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
private var presentationNode: ContextControllerPresentationNode?
|
||||
private var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
@ -466,11 +471,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
}
|
||||
|
||||
self.itemsDisposable.set((items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale)
|
||||
}))
|
||||
|
||||
switch source {
|
||||
case .reference, .extracted:
|
||||
self.contentReady.set(.single(true))
|
||||
@ -480,6 +480,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
self.initializeContent()
|
||||
|
||||
self.itemsDisposable.set((items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale)
|
||||
}))
|
||||
|
||||
self.dismissAccessibilityArea.activate = { [weak self] in
|
||||
self?.dimNodeTapped()
|
||||
return true
|
||||
@ -523,7 +528,40 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
|
||||
}
|
||||
case let .extracted(source):
|
||||
let takenViewInfo = source.takeView()
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
getController: { [weak self] in
|
||||
return self?.getController()
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let validLayout = strongSelf.validLayout {
|
||||
strongSelf.updateLayout(
|
||||
layout: validLayout,
|
||||
transition: transition,
|
||||
previousActionsContainerNode: nil
|
||||
)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.dismissedForCancel?()
|
||||
strongSelf.beginDismiss(result)
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: source
|
||||
)
|
||||
self.presentationNode = presentationNode
|
||||
self.addSubnode(presentationNode)
|
||||
/*let takenViewInfo = source.takeView()
|
||||
|
||||
if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode {
|
||||
self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace)
|
||||
@ -553,7 +591,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
takenViewInfo.contentContainingNode.isExtractedToContextPreviewUpdated?(true)
|
||||
|
||||
self.originalProjectedContentViewFrame = (convertFrame(takenViewInfo.contentContainingNode.frame, from: parentSupernode.view, to: self.view), convertFrame(takenViewInfo.contentContainingNode.contentRect, from: takenViewInfo.contentContainingNode.view, to: self.view))
|
||||
}
|
||||
}*/
|
||||
case let .controller(source):
|
||||
let transitionInfo = source.transitionInfo()
|
||||
if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() {
|
||||
@ -574,9 +612,21 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
func animateIn() {
|
||||
self.gesture?.endPressedAppearance()
|
||||
|
||||
self.hapticFeedback.impact()
|
||||
|
||||
if let _ = self.presentationNode {
|
||||
self.didCompleteAnimationIn = true
|
||||
self.currentPresentationStateTransition = .animateIn
|
||||
if let validLayout = self.validLayout {
|
||||
self.updateLayout(
|
||||
layout: validLayout,
|
||||
transition: .animated(duration: 0.5, curve: .spring),
|
||||
previousActionsContainerNode: nil
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self.source {
|
||||
case .reference:
|
||||
break
|
||||
@ -759,6 +809,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
self.beganAnimatingOut()
|
||||
|
||||
if let _ = self.presentationNode {
|
||||
self.currentPresentationStateTransition = .animateOut(result: initialResult, completion: completion)
|
||||
if let validLayout = self.validLayout {
|
||||
self.updateLayout(
|
||||
layout: validLayout,
|
||||
transition: .animated(duration: 0.25, curve: .easeInOut),
|
||||
previousActionsContainerNode: nil
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var transitionDuration: Double = 0.2
|
||||
var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
|
||||
|
||||
@ -1152,6 +1214,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
if let presentationNode = self.presentationNode {
|
||||
presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
guard let reactionContextNode = self.reactionContextNode else {
|
||||
self.animateOut(result: .default, completion: completion)
|
||||
return
|
||||
@ -1207,6 +1274,16 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
|
||||
private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
|
||||
if let presentationNode = self.presentationNode {
|
||||
presentationNode.replaceItems(items: items, animated: self.didCompleteAnimationIn)
|
||||
|
||||
if !self.didSetItemsReady {
|
||||
self.didSetItemsReady = true
|
||||
self.itemsReady.set(.single(true))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let _ = self.currentItems, !self.didCompleteAnimationIn && self.getController()?.immediateItemsTransitionAnimation == true {
|
||||
return
|
||||
}
|
||||
@ -1248,7 +1325,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode)
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition)
|
||||
self.updateLayout(layout: layout, transition: self.didSetItemsReady ? .animated(duration: 0.3, curve: .spring) : .immediate, previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition)
|
||||
} else {
|
||||
previousActionsContainerNode.removeFromSupernode()
|
||||
}
|
||||
@ -1259,6 +1336,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
}
|
||||
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
self.itemsDisposable.set((items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let strongSelf = self, let presentationNode = strongSelf.presentationNode else {
|
||||
return
|
||||
}
|
||||
presentationNode.pushItems(items: items)
|
||||
}))
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
if let presentationNode = self.presentationNode {
|
||||
presentationNode.popItems()
|
||||
}
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
@ -1283,6 +1376,20 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
let presentationStateTransition = self.currentPresentationStateTransition
|
||||
self.currentPresentationStateTransition = .none
|
||||
|
||||
if let presentationNode = self.presentationNode {
|
||||
transition.updateFrame(node: presentationNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
presentationNode.update(
|
||||
presentationData: self.presentationData,
|
||||
layout: layout,
|
||||
transition: transition,
|
||||
stateTransition: presentationStateTransition
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var actionsContainerTransition = transition
|
||||
if previousActionsContainerNode != nil {
|
||||
actionsContainerTransition = .immediate
|
||||
@ -1340,7 +1447,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
|
||||
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
|
||||
|
||||
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, bottomInset: 0.0, transition: actionsContainerTransition)
|
||||
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition)
|
||||
let adjustedActionsSize = realActionsSize
|
||||
|
||||
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize)
|
||||
@ -1437,7 +1544,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
constrainedActionsBottomInset = 0.0
|
||||
}
|
||||
|
||||
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: constrainedActionsHeight, bottomInset: constrainedActionsBottomInset, transition: actionsContainerTransition)
|
||||
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: constrainedActionsHeight, transition: actionsContainerTransition)
|
||||
let adjustedActionsSize = realActionsSize
|
||||
|
||||
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize)
|
||||
@ -1602,7 +1709,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
constrainedWidth = floor(layout.size.width / 2.0)
|
||||
}
|
||||
|
||||
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, bottomInset: 0.0, transition: actionsContainerTransition)
|
||||
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition)
|
||||
let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth
|
||||
var contentUnscaledSize: CGSize
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
@ -1800,6 +1907,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
return nil
|
||||
}
|
||||
|
||||
if let presentationNode = self.presentationNode {
|
||||
return presentationNode.hitTest(self.view.convert(point, to: presentationNode.view), with: event)
|
||||
}
|
||||
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) {
|
||||
return result
|
||||
@ -1965,11 +2076,16 @@ public enum ContextContentSource {
|
||||
}
|
||||
|
||||
public protocol ContextControllerItemsNode: ASDisplayNode {
|
||||
func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize)
|
||||
func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat)
|
||||
|
||||
var apparentHeight: CGFloat { get }
|
||||
}
|
||||
|
||||
public protocol ContextControllerItemsContent: AnyObject {
|
||||
func node() -> ContextControllerItemsNode
|
||||
func node(
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) -> ContextControllerItemsNode
|
||||
}
|
||||
|
||||
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol {
|
||||
@ -2182,6 +2298,20 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
}
|
||||
}
|
||||
|
||||
public func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
if !self.isNodeLoaded {
|
||||
return
|
||||
}
|
||||
self.controllerNode.pushItems(items: items)
|
||||
}
|
||||
|
||||
public func popItems() {
|
||||
if !self.isNodeLoaded {
|
||||
return
|
||||
}
|
||||
self.controllerNode.popItems()
|
||||
}
|
||||
|
||||
public func updateTheme(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
if self.isNodeLoaded {
|
||||
|
@ -0,0 +1,758 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import ReactionSelectionNode
|
||||
|
||||
public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
constrainedSize: CGSize,
|
||||
standardWidth: CGFloat,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) -> (size: CGSize, apparentHeight: CGFloat)
|
||||
}
|
||||
|
||||
public protocol ContextControllerActionsStackItem: AnyObject {
|
||||
func node(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) -> ContextControllerActionsStackItemNode
|
||||
|
||||
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? { get }
|
||||
}
|
||||
|
||||
protocol ContextControllerActionsListItemNode: ASDisplayNode {
|
||||
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void)
|
||||
}
|
||||
|
||||
private final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode {
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let requestDismiss: (ContextMenuActionResult) -> Void
|
||||
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
|
||||
private let item: ContextMenuActionItem
|
||||
|
||||
private let highlightBackgroundNode: ASDisplayNode
|
||||
private let titleLabelNode: ImmediateTextNode
|
||||
private let subtitleNode: ImmediateTextNode
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
init(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void,
|
||||
item: ContextMenuActionItem
|
||||
) {
|
||||
self.getController = getController
|
||||
self.requestDismiss = requestDismiss
|
||||
self.requestUpdateAction = requestUpdateAction
|
||||
self.item = item
|
||||
|
||||
self.highlightBackgroundNode = ASDisplayNode()
|
||||
self.highlightBackgroundNode.isUserInteractionEnabled = false
|
||||
self.highlightBackgroundNode.alpha = 0.0
|
||||
|
||||
self.titleLabelNode = ImmediateTextNode()
|
||||
self.titleLabelNode.displaysAsynchronously = false
|
||||
self.titleLabelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.subtitleNode = ImmediateTextNode()
|
||||
self.subtitleNode.displaysAsynchronously = false
|
||||
self.subtitleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.highlightBackgroundNode)
|
||||
self.addSubnode(self.titleLabelNode)
|
||||
self.addSubnode(self.subtitleNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
strongSelf.highlightBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
|
||||
strongSelf.highlightBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let controller = self.getController() else {
|
||||
return
|
||||
}
|
||||
|
||||
self.item.action?(ContextMenuActionItem.Action(
|
||||
controller: controller,
|
||||
dismissWithResult: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestDismiss(result)
|
||||
},
|
||||
updateAction: { [weak self] id, updatedAction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestUpdateAction(id, updatedAction)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) {
|
||||
let sideInset: CGFloat = 16.0
|
||||
let verticalInset: CGFloat = 11.0
|
||||
let titleSubtitleSpacing: CGFloat = 1.0
|
||||
let iconSideInset: CGFloat = 12.0
|
||||
let standardIconWidth: CGFloat = 32.0
|
||||
|
||||
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
|
||||
var subtitle: String?
|
||||
switch self.item.textLayout {
|
||||
case .singleLine:
|
||||
self.titleLabelNode.maximumNumberOfLines = 1
|
||||
case .twoLinesMax:
|
||||
self.titleLabelNode.maximumNumberOfLines = 2
|
||||
case let .secondLineWithValue(subtitleValue):
|
||||
self.titleLabelNode.maximumNumberOfLines = 1
|
||||
subtitle = subtitleValue
|
||||
}
|
||||
|
||||
let titleFont: UIFont
|
||||
switch self.item.textFont {
|
||||
case let .custom(font):
|
||||
titleFont = font
|
||||
case .regular:
|
||||
titleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||
}
|
||||
|
||||
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
let subtitleColor = presentationData.theme.contextMenu.secondaryColor
|
||||
|
||||
let titleColor: UIColor
|
||||
switch self.item.textColor {
|
||||
case .primary:
|
||||
titleColor = presentationData.theme.contextMenu.primaryColor
|
||||
case .destructive:
|
||||
titleColor = presentationData.theme.contextMenu.destructiveColor
|
||||
case .disabled:
|
||||
titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||
}
|
||||
|
||||
self.titleLabelNode.attributedText = NSAttributedString(
|
||||
string: self.item.text,
|
||||
font: titleFont,
|
||||
textColor: titleColor
|
||||
)
|
||||
|
||||
self.subtitleNode.attributedText = subtitle.flatMap { subtitle in
|
||||
return NSAttributedString(
|
||||
string: self.item.text,
|
||||
font: subtitleFont,
|
||||
textColor: subtitleColor
|
||||
)
|
||||
}
|
||||
|
||||
let iconImage = self.iconNode.image ?? self.item.icon(presentationData.theme)
|
||||
|
||||
var maxTextWidth: CGFloat = constrainedSize.width
|
||||
maxTextWidth -= sideInset
|
||||
if let iconImage = iconImage {
|
||||
maxTextWidth -= max(standardIconWidth, iconImage.size.width)
|
||||
} else {
|
||||
maxTextWidth -= sideInset
|
||||
}
|
||||
maxTextWidth = max(1.0, maxTextWidth)
|
||||
|
||||
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
|
||||
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
|
||||
|
||||
var minSize = CGSize()
|
||||
minSize.width += sideInset
|
||||
minSize.width += max(titleSize.width, subtitleSize.width)
|
||||
if let iconImage = iconImage {
|
||||
minSize.width += max(standardIconWidth, iconImage.size.width)
|
||||
minSize.width += iconSideInset
|
||||
} else {
|
||||
minSize.width += sideInset
|
||||
}
|
||||
minSize.height += verticalInset * 2.0
|
||||
minSize.height += titleSize.height
|
||||
if subtitle != nil {
|
||||
minSize.height += titleSubtitleSpacing
|
||||
minSize.height += subtitleSize.height
|
||||
}
|
||||
|
||||
return (minSize: minSize, apply: { size, transition in
|
||||
let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleSize)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: sideInset, y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize)
|
||||
|
||||
transition.updateFrame(node: self.highlightBackgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame)
|
||||
transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame)
|
||||
|
||||
if let iconImage = iconImage {
|
||||
if self.iconNode.image !== iconImage {
|
||||
self.iconNode.image = iconImage
|
||||
}
|
||||
let iconWidth = max(standardIconWidth, iconImage.size.width)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size)
|
||||
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextControllerActionsListSeparatorItemNode: ASDisplayNode, ContextControllerActionsListItemNode {
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) {
|
||||
return (minSize: CGSize(width: 0.0, height: 7.0), apply: { _, _ in
|
||||
self.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, ContextControllerActionsListItemNode {
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let item: ContextMenuCustomItem
|
||||
|
||||
private var presentationData: PresentationData?
|
||||
private var itemNode: ContextMenuCustomNode?
|
||||
|
||||
init(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
item: ContextMenuCustomItem
|
||||
) {
|
||||
self.getController = getController
|
||||
self.item = item
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) {
|
||||
if self.presentationData?.theme !== presentationData.theme {
|
||||
if let itemNode = self.itemNode {
|
||||
itemNode.updateTheme(presentationData: presentationData)
|
||||
}
|
||||
}
|
||||
self.presentationData = presentationData
|
||||
|
||||
let itemNode: ContextMenuCustomNode
|
||||
if let current = self.itemNode {
|
||||
itemNode = current
|
||||
} else {
|
||||
itemNode = self.item.node(
|
||||
presentationData: presentationData,
|
||||
getController: self.getController,
|
||||
actionSelected: { result in
|
||||
let _ = result
|
||||
}
|
||||
)
|
||||
self.itemNode = itemNode
|
||||
self.addSubnode(itemNode)
|
||||
}
|
||||
|
||||
let itemLayoutAndApply = itemNode.updateLayout(constrainedWidth: constrainedSize.width, constrainedHeight: constrainedSize.height)
|
||||
|
||||
return (minSize: itemLayoutAndApply.0, apply: { size, transition in
|
||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
itemLayoutAndApply.1(size, transition)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
final class ContextControllerActionsListStackItem: ContextControllerActionsStackItem {
|
||||
private final class Node: ASDisplayNode, ContextControllerActionsStackItemNode {
|
||||
private final class Item {
|
||||
let node: ContextControllerActionsListItemNode
|
||||
let separatorNode: ASDisplayNode?
|
||||
|
||||
init(node: ContextControllerActionsListItemNode, separatorNode: ASDisplayNode?) {
|
||||
self.node = node
|
||||
self.separatorNode = separatorNode
|
||||
}
|
||||
}
|
||||
|
||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
private var items: [ContextMenuItem]
|
||||
private var itemNodes: [Item]
|
||||
|
||||
init(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
items: [ContextMenuItem]
|
||||
) {
|
||||
self.requestUpdate = requestUpdate
|
||||
self.items = items
|
||||
|
||||
var requestUpdateAction: ((AnyHashable, ContextMenuActionItem) -> Void)?
|
||||
self.itemNodes = items.map { item -> Item in
|
||||
switch item {
|
||||
case let .action(actionItem):
|
||||
return Item(
|
||||
node: ContextControllerActionsListActionItemNode(
|
||||
getController: getController,
|
||||
requestDismiss: requestDismiss,
|
||||
requestUpdateAction: { id, action in
|
||||
requestUpdateAction?(id, action)
|
||||
},
|
||||
item: actionItem
|
||||
),
|
||||
separatorNode: ASDisplayNode()
|
||||
)
|
||||
case .separator:
|
||||
return Item(
|
||||
node: ContextControllerActionsListSeparatorItemNode(),
|
||||
separatorNode: nil
|
||||
)
|
||||
case let .custom(customItem, _):
|
||||
return Item(
|
||||
node: ContextControllerActionsListCustomItemNode(
|
||||
getController: getController,
|
||||
item: customItem
|
||||
),
|
||||
separatorNode: ASDisplayNode()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
for item in self.itemNodes {
|
||||
if let separatorNode = item.separatorNode {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
}
|
||||
for item in self.itemNodes {
|
||||
self.addSubnode(item.node)
|
||||
}
|
||||
|
||||
requestUpdateAction = { [weak self] id, action in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
loop: for i in 0 ..< strongSelf.items.count {
|
||||
switch strongSelf.items[i] {
|
||||
case let .action(currentAction):
|
||||
if currentAction.id == id {
|
||||
let previousNode = strongSelf.itemNodes[i]
|
||||
previousNode.node.removeFromSupernode()
|
||||
previousNode.separatorNode?.removeFromSupernode()
|
||||
|
||||
let addedNode = Item(
|
||||
node: ContextControllerActionsListActionItemNode(
|
||||
getController: getController,
|
||||
requestDismiss: requestDismiss,
|
||||
requestUpdateAction: { id, action in
|
||||
requestUpdateAction?(id, action)
|
||||
},
|
||||
item: action
|
||||
),
|
||||
separatorNode: ASDisplayNode()
|
||||
)
|
||||
strongSelf.itemNodes[i] = addedNode
|
||||
if let separatorNode = addedNode.separatorNode {
|
||||
strongSelf.insertSubnode(separatorNode, at: 0)
|
||||
}
|
||||
strongSelf.addSubnode(addedNode.node)
|
||||
|
||||
strongSelf.requestUpdate(.immediate)
|
||||
|
||||
break loop
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
constrainedSize: CGSize,
|
||||
standardWidth: CGFloat,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) -> (size: CGSize, apparentHeight: CGFloat) {
|
||||
var itemNodeLayouts: [(minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void)] = []
|
||||
var combinedSize = CGSize()
|
||||
for item in self.itemNodes {
|
||||
item.separatorNode?.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
|
||||
let itemNodeLayout = item.node.update(
|
||||
presentationData: presentationData,
|
||||
constrainedSize: constrainedSize
|
||||
)
|
||||
itemNodeLayouts.append(itemNodeLayout)
|
||||
combinedSize.width = max(combinedSize.width, itemNodeLayout.minSize.width)
|
||||
combinedSize.height += itemNodeLayout.minSize.height
|
||||
}
|
||||
combinedSize.width = max(combinedSize.width, standardWidth)
|
||||
|
||||
var nextItemOrigin = CGPoint()
|
||||
for i in 0 ..< self.itemNodes.count {
|
||||
let item = self.itemNodes[i]
|
||||
let itemNodeLayout = itemNodeLayouts[i]
|
||||
|
||||
var itemTransition = transition
|
||||
if item.node.frame.isEmpty {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
let itemSize = CGSize(width: combinedSize.width, height: itemNodeLayout.minSize.height)
|
||||
let itemFrame = CGRect(origin: nextItemOrigin, size: itemSize)
|
||||
itemTransition.updateFrame(node: item.node, frame: itemFrame)
|
||||
|
||||
if let separatorNode = item.separatorNode {
|
||||
itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)))
|
||||
if i != self.itemNodes.count - 1 {
|
||||
switch self.items[i + 1] {
|
||||
case .separator:
|
||||
separatorNode.isHidden = true
|
||||
case .action:
|
||||
separatorNode.isHidden = false
|
||||
case .custom:
|
||||
separatorNode.isHidden = false
|
||||
}
|
||||
} else {
|
||||
separatorNode.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
itemNodeLayout.apply(itemSize, itemTransition)
|
||||
nextItemOrigin.y += itemSize.height
|
||||
}
|
||||
|
||||
return (combinedSize, combinedSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
private let items: [ContextMenuItem]
|
||||
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
||||
|
||||
init(
|
||||
items: [ContextMenuItem],
|
||||
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
||||
) {
|
||||
self.items = items
|
||||
self.reactionItems = reactionItems
|
||||
}
|
||||
|
||||
func node(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) -> ContextControllerActionsStackItemNode {
|
||||
return Node(
|
||||
getController: getController,
|
||||
requestDismiss: requestDismiss,
|
||||
requestUpdate: requestUpdate,
|
||||
items: self.items
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class ContextControllerActionsCustomStackItem: ContextControllerActionsStackItem {
|
||||
private final class Node: ASDisplayNode, ContextControllerActionsStackItemNode {
|
||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
private let contentNode: ContextControllerItemsNode
|
||||
|
||||
init(
|
||||
content: ContextControllerItemsContent,
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) {
|
||||
self.requestUpdate = requestUpdate
|
||||
self.contentNode = content.node(requestUpdate: { transition in
|
||||
requestUpdate(transition)
|
||||
}, requestUpdateApparentHeight: { transition in
|
||||
requestUpdateApparentHeight(transition)
|
||||
})
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
constrainedSize: CGSize,
|
||||
standardWidth: CGFloat,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) -> (size: CGSize, apparentHeight: CGFloat) {
|
||||
let contentLayout = self.contentNode.update(
|
||||
constrainedWidth: constrainedSize.width,
|
||||
maxHeight: constrainedSize.height,
|
||||
bottomInset: 0.0,
|
||||
transition: transition
|
||||
)
|
||||
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentLayout.cleanSize))
|
||||
|
||||
return (contentLayout.cleanSize, contentLayout.apparentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private let content: ContextControllerItemsContent
|
||||
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
||||
|
||||
init(
|
||||
content: ContextControllerItemsContent,
|
||||
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
||||
) {
|
||||
self.content = content
|
||||
self.reactionItems = reactionItems
|
||||
}
|
||||
|
||||
func node(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) -> ContextControllerActionsStackItemNode {
|
||||
return Node(
|
||||
content: self.content,
|
||||
getController: getController,
|
||||
requestUpdate: requestUpdate,
|
||||
requestUpdateApparentHeight: requestUpdateApparentHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func makeContextControllerActionsStackItem(items: ContextController.Items) -> ContextControllerActionsStackItem {
|
||||
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
||||
if let context = items.context, !items.reactionItems.isEmpty {
|
||||
reactionItems = (context, items.reactionItems)
|
||||
}
|
||||
switch items.content {
|
||||
case let .list(listItems):
|
||||
return ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems)
|
||||
case let .custom(customContent):
|
||||
return ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems)
|
||||
}
|
||||
}
|
||||
|
||||
final class ContextControllerActionsStackNode: ASDisplayNode {
|
||||
final class NavigationContainer: ASDisplayNode {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.cornerRadius = 14.0
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemContainer: ASDisplayNode {
|
||||
let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
let node: ContextControllerActionsStackItemNode
|
||||
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?
|
||||
let positionLock: CGFloat?
|
||||
|
||||
init(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
item: ContextControllerActionsStackItem,
|
||||
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])?,
|
||||
positionLock: CGFloat?
|
||||
) {
|
||||
self.requestUpdate = requestUpdate
|
||||
self.node = item.node(
|
||||
getController: getController,
|
||||
requestDismiss: requestDismiss,
|
||||
requestUpdate: requestUpdate,
|
||||
requestUpdateApparentHeight: requestUpdateApparentHeight
|
||||
)
|
||||
|
||||
self.reactionItems = reactionItems
|
||||
self.positionLock = positionLock
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.node)
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
constrainedSize: CGSize,
|
||||
standardWidth: CGFloat,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) -> (size: CGSize, apparentHeight: CGFloat) {
|
||||
let (size, apparentHeight) = self.node.update(
|
||||
presentationData: presentationData,
|
||||
constrainedSize: constrainedSize,
|
||||
standardWidth: standardWidth,
|
||||
transition: transition
|
||||
)
|
||||
transition.updateFrame(node: self.node, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
return (size, apparentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let requestDismiss: (ContextMenuActionResult) -> Void
|
||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
|
||||
private let navigationContainer: NavigationContainer
|
||||
private var itemContainers: [ItemContainer] = []
|
||||
private var dismissingItemContainers: [(container: ItemContainer, isPopped: Bool)] = []
|
||||
|
||||
var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? {
|
||||
return self.itemContainers.last?.reactionItems
|
||||
}
|
||||
|
||||
var topPositionLock: CGFloat? {
|
||||
return self.itemContainers.last?.positionLock
|
||||
}
|
||||
|
||||
init(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) {
|
||||
self.getController = getController
|
||||
self.requestDismiss = requestDismiss
|
||||
self.requestUpdate = requestUpdate
|
||||
|
||||
self.navigationContainer = NavigationContainer()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.navigationContainer)
|
||||
}
|
||||
|
||||
func replace(item: ContextControllerActionsStackItem, animated: Bool) {
|
||||
for itemContainer in self.itemContainers {
|
||||
if animated {
|
||||
self.dismissingItemContainers.append((itemContainer, false))
|
||||
} else {
|
||||
itemContainer.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
self.itemContainers.removeAll()
|
||||
|
||||
self.push(item: item, positionLock: nil, animated: animated)
|
||||
}
|
||||
|
||||
func push(item: ContextControllerActionsStackItem, positionLock: CGFloat?, animated: Bool) {
|
||||
let itemContainer = ItemContainer(
|
||||
getController: self.getController,
|
||||
requestDismiss: self.requestDismiss,
|
||||
requestUpdate: self.requestUpdate,
|
||||
requestUpdateApparentHeight: { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestUpdate(transition)
|
||||
},
|
||||
item: item,
|
||||
reactionItems: item.reactionItems,
|
||||
positionLock: positionLock
|
||||
)
|
||||
self.itemContainers.append(itemContainer)
|
||||
self.navigationContainer.addSubnode(itemContainer)
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.45, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
self.requestUpdate(transition)
|
||||
}
|
||||
|
||||
func pop() {
|
||||
if self.itemContainers.count == 1 {
|
||||
//dismiss
|
||||
} else {
|
||||
let itemContainer = self.itemContainers[self.itemContainers.count - 1]
|
||||
self.itemContainers.remove(at: self.itemContainers.count - 1)
|
||||
self.dismissingItemContainers.append((itemContainer, true))
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
|
||||
self.requestUpdate(transition)
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
constrainedSize: CGSize,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) -> CGSize {
|
||||
self.navigationContainer.backgroundColor = presentationData.theme.contextMenu.backgroundColor
|
||||
|
||||
let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty
|
||||
|
||||
var topItemSize = CGSize()
|
||||
var topItemApparentHeight: CGFloat = 0.0
|
||||
for i in 0 ..< self.itemContainers.count {
|
||||
let itemContainer = self.itemContainers[i]
|
||||
|
||||
var animateAppearingContainer = false
|
||||
var itemContainerTransition = transition
|
||||
if itemContainer.bounds.isEmpty {
|
||||
itemContainerTransition = .immediate
|
||||
animateAppearingContainer = i == self.itemContainers.count - 1 && animateAppearingContainers || self.itemContainers.count > 1
|
||||
}
|
||||
|
||||
let itemConstrainedHeight: CGFloat = constrainedSize.height
|
||||
|
||||
let itemSize = itemContainer.update(
|
||||
presentationData: presentationData,
|
||||
constrainedSize: CGSize(width: constrainedSize.width, height: itemConstrainedHeight),
|
||||
standardWidth: 260.0,
|
||||
transition: itemContainerTransition
|
||||
)
|
||||
if i == self.itemContainers.count - 1 {
|
||||
topItemSize = itemSize.size
|
||||
topItemApparentHeight = itemSize.apparentHeight
|
||||
}
|
||||
|
||||
let itemFrame: CGRect
|
||||
if i == self.itemContainers.count - 1 {
|
||||
itemFrame = CGRect(origin: CGPoint(), size: itemSize.size)
|
||||
} else {
|
||||
itemFrame = CGRect(origin: CGPoint(x: -itemSize.size.width, y: 0.0), size: itemSize.size)
|
||||
}
|
||||
|
||||
itemContainerTransition.updateFrame(node: itemContainer, frame: itemFrame)
|
||||
if animateAppearingContainer {
|
||||
transition.animatePositionAdditive(node: itemContainer, offset: CGPoint(x: itemContainer.bounds.width, y: 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.navigationContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: topItemSize.width, height: max(44.0, topItemApparentHeight))))
|
||||
|
||||
for (itemContainer, isPopped) in self.dismissingItemContainers {
|
||||
transition.updatePosition(node: itemContainer, position: CGPoint(x: isPopped ? itemContainer.bounds.width * 3.0 / 2.0 : -itemContainer.bounds.width / 2.0, y: itemContainer.position.y), completion: { [weak itemContainer] _ in
|
||||
itemContainer?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
self.dismissingItemContainers.removeAll()
|
||||
|
||||
return CGSize(width: topItemSize.width, height: topItemSize.height)
|
||||
}
|
||||
}
|
@ -6,7 +6,568 @@ import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import ReactionSelectionNode
|
||||
|
||||
final class ContextControllerExtractedPresentationNode: ASDisplayNode {
|
||||
final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode {
|
||||
private final class ContentNode: ASDisplayNode {
|
||||
let offsetContainerNode: ASDisplayNode
|
||||
let containingNode: ContextExtractedContentContainingNode
|
||||
|
||||
var animateClippingFromContentAreaInScreenSpace: CGRect?
|
||||
var storedGlobalFrame: CGRect?
|
||||
|
||||
init(containingNode: ContextExtractedContentContainingNode) {
|
||||
self.offsetContainerNode = ASDisplayNode()
|
||||
self.containingNode = containingNode
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.offsetContainerNode)
|
||||
self.offsetContainerNode.addSubnode(self.containingNode.contentNode)
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
if !self.containingNode.contentRect.contains(point) {
|
||||
return nil
|
||||
}
|
||||
return self.view
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatingOutState {
|
||||
var currentContentScreenFrame: CGRect
|
||||
|
||||
init(
|
||||
currentContentScreenFrame: CGRect
|
||||
) {
|
||||
self.currentContentScreenFrame = currentContentScreenFrame
|
||||
}
|
||||
}
|
||||
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
private let requestDismiss: (ContextMenuActionResult) -> Void
|
||||
private let requestAnimateOut: (ContextMenuActionResult, @escaping () -> Void) -> Void
|
||||
private let source: ContextExtractedContentSource
|
||||
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let dismissTapNode: ASDisplayNode
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var reactionContextNode: ReactionContextNode?
|
||||
private var reactionContextNodeIsAnimatingOut: Bool = false
|
||||
|
||||
private var contentNode: ContentNode?
|
||||
private let contentRectDebugNode: ASDisplayNode
|
||||
private let actionsStackNode: ContextControllerActionsStackNode
|
||||
|
||||
private var animatingOutState: AnimatingOutState?
|
||||
|
||||
init(
|
||||
getController: @escaping () -> ContextControllerProtocol?,
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||
requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void,
|
||||
source: ContextExtractedContentSource
|
||||
) {
|
||||
self.getController = getController
|
||||
self.requestUpdate = requestUpdate
|
||||
self.requestDismiss = requestDismiss
|
||||
self.requestAnimateOut = requestAnimateOut
|
||||
self.source = source
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
|
||||
|
||||
self.dismissTapNode = ASDisplayNode()
|
||||
self.clippingNode = ASDisplayNode()
|
||||
self.clippingNode.clipsToBounds = true
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.contentRectDebugNode = ASDisplayNode()
|
||||
self.contentRectDebugNode.isUserInteractionEnabled = false
|
||||
self.contentRectDebugNode.backgroundColor = UIColor.red.withAlphaComponent(0.2)
|
||||
|
||||
self.actionsStackNode = ContextControllerActionsStackNode(
|
||||
getController: getController,
|
||||
requestDismiss: { result in
|
||||
requestDismiss(result)
|
||||
},
|
||||
requestUpdate: requestUpdate
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.clippingNode)
|
||||
self.clippingNode.addSubnode(self.scrollNode)
|
||||
self.scrollNode.addSubnode(self.dismissTapNode)
|
||||
self.scrollNode.addSubnode(self.actionsStackNode)
|
||||
|
||||
/*#if DEBUG
|
||||
self.scrollNode.addSubnode(self.contentRectDebugNode)
|
||||
#endif*/
|
||||
|
||||
self.dismissTapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapGesture(_:))))
|
||||
}
|
||||
|
||||
@objc func dismissTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.requestDismiss(.default)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.bounds.contains(point) {
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func replaceItems(items: ContextController.Items, animated: Bool) {
|
||||
self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated)
|
||||
}
|
||||
|
||||
func pushItems(items: ContextController.Items) {
|
||||
let positionLock = self.getActionsStackPositionLock()
|
||||
self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), positionLock: positionLock, animated: true)
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
self.actionsStackNode.pop()
|
||||
}
|
||||
|
||||
private func getActionsStackPositionLock() -> CGFloat? {
|
||||
return self.actionsStackNode.frame.minY
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition,
|
||||
stateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
) {
|
||||
let contentActionsSpacing: CGFloat = 8.0
|
||||
let actionsSideInset: CGFloat = 6.0
|
||||
let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0
|
||||
let bottomInset: CGFloat = 10.0
|
||||
|
||||
let contentNode: ContentNode
|
||||
var contentTransition = transition
|
||||
|
||||
self.backgroundNode.updateColor(
|
||||
color: presentationData.theme.contextMenu.dimColor,
|
||||
enableBlur: true,
|
||||
forceKeepBlur: true,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
self.backgroundNode.update(size: layout.size, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) {
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
if let current = self.contentNode {
|
||||
contentNode = current
|
||||
} else {
|
||||
guard let takeInfo = self.source.takeView() else {
|
||||
return
|
||||
}
|
||||
contentNode = ContentNode(containingNode: takeInfo.contentContainingNode)
|
||||
contentNode.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace
|
||||
self.scrollNode.insertSubnode(contentNode, aboveSubnode: self.actionsStackNode)
|
||||
self.contentNode = contentNode
|
||||
contentTransition = .immediate
|
||||
}
|
||||
|
||||
var animateReactionsIn = false
|
||||
var contentTopInset: CGFloat = topInset
|
||||
var removedReactionContextNode: ReactionContextNode?
|
||||
if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty {
|
||||
if self.reactionContextNode == nil {
|
||||
let reactionContextNode = ReactionContextNode(context: reactionItems.context, theme: presentationData.theme, items: reactionItems.reactionItems)
|
||||
self.reactionContextNode = reactionContextNode
|
||||
self.addSubnode(reactionContextNode)
|
||||
|
||||
if transition.isAnimated {
|
||||
animateReactionsIn = true
|
||||
}
|
||||
|
||||
reactionContextNode.reactionSelected = { [weak self] reaction in
|
||||
guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else {
|
||||
return
|
||||
}
|
||||
controller.reactionSelected?(reaction)
|
||||
}
|
||||
}
|
||||
contentTopInset += 70.0
|
||||
} else if let reactionContextNode = self.reactionContextNode {
|
||||
self.reactionContextNode = nil
|
||||
removedReactionContextNode = reactionContextNode
|
||||
}
|
||||
|
||||
switch stateTransition {
|
||||
case .animateIn, .animateOut:
|
||||
contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
|
||||
case .none:
|
||||
if contentNode.storedGlobalFrame == nil {
|
||||
contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
|
||||
}
|
||||
}
|
||||
//let contentRectGlobalFrame = contentNode.storedGlobalFrame ?? convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
|
||||
let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingNode.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingNode.contentRect.height), size: contentNode.containingNode.contentRect.size)
|
||||
var contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingNode.contentRect.size.height), size: contentNode.containingNode.contentRect.size)
|
||||
if case .animateOut = stateTransition {
|
||||
contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height
|
||||
}
|
||||
|
||||
var defaultScrollY: CGFloat = 0.0
|
||||
if self.animatingOutState == nil {
|
||||
contentNode.update(
|
||||
presentationData: presentationData,
|
||||
size: contentNode.containingNode.bounds.size,
|
||||
transition: contentTransition
|
||||
)
|
||||
|
||||
let actionsConstrainedHeight: CGFloat
|
||||
if let actionsPositionLock = self.actionsStackNode.topPositionLock {
|
||||
actionsConstrainedHeight = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - actionsPositionLock
|
||||
} else {
|
||||
actionsConstrainedHeight = layout.size.height
|
||||
}
|
||||
|
||||
let actionsSize = self.actionsStackNode.update(
|
||||
presentationData: presentationData,
|
||||
constrainedSize: CGSize(width: layout.size.width, height: actionsConstrainedHeight),
|
||||
transition: transition
|
||||
)
|
||||
|
||||
if case .animateOut = stateTransition {
|
||||
} else {
|
||||
if contentRect.minY < contentTopInset {
|
||||
contentRect.origin.y = contentTopInset
|
||||
}
|
||||
var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height))
|
||||
if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom {
|
||||
combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height
|
||||
}
|
||||
if combinedBounds.minY < contentTopInset {
|
||||
combinedBounds.origin.y = contentTopInset
|
||||
}
|
||||
|
||||
contentRect.origin.y = combinedBounds.minY
|
||||
}
|
||||
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
var reactionContextNodeTransition = transition
|
||||
if reactionContextNode.frame.isEmpty {
|
||||
reactionContextNodeTransition = .immediate
|
||||
}
|
||||
reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect, transition: reactionContextNodeTransition)
|
||||
}
|
||||
if let removedReactionContextNode = removedReactionContextNode {
|
||||
removedReactionContextNode.animateOut(to: contentRect, animatingOutToReaction: false)
|
||||
transition.updateAlpha(node: removedReactionContextNode, alpha: 0.0, completion: { [weak removedReactionContextNode] _ in
|
||||
removedReactionContextNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect)
|
||||
|
||||
var actionsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.maxY + contentActionsSpacing), size: actionsSize)
|
||||
if contentRect.midX < layout.size.width / 2.0 {
|
||||
actionsFrame.origin.x = contentRect.minX + actionsSideInset - 3.0
|
||||
} else {
|
||||
actionsFrame.origin.x = contentRect.maxX - actionsSideInset - actionsSize.width
|
||||
}
|
||||
transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame)
|
||||
|
||||
contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size))
|
||||
|
||||
let contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom
|
||||
let contentSize = CGSize(width: layout.size.width, height: contentHeight)
|
||||
|
||||
if self.scrollNode.view.contentSize != contentSize {
|
||||
let previousContentOffset = self.scrollNode.view.contentOffset
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
if case .none = stateTransition, transition.isAnimated {
|
||||
let contentOffset = self.scrollNode.view.contentOffset
|
||||
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: previousContentOffset.y - contentOffset.y)
|
||||
}
|
||||
}
|
||||
|
||||
defaultScrollY = contentSize.height - layout.size.height
|
||||
if defaultScrollY < 0.0 {
|
||||
defaultScrollY = 0.0
|
||||
}
|
||||
|
||||
self.dismissTapNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: max(contentSize.height, layout.size.height)))
|
||||
}
|
||||
|
||||
switch stateTransition {
|
||||
case .animateIn:
|
||||
let duration: Double = 0.42
|
||||
let springDamping: CGFloat = 104.0
|
||||
|
||||
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: defaultScrollY)
|
||||
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
|
||||
self.clippingNode.layer.animateFrame(from: animateClippingFromContentAreaInScreenSpace, to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2)
|
||||
}
|
||||
|
||||
let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
|
||||
let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view)
|
||||
let animationInContentDistance = currentContentLocalFrame.maxY - currentContentScreenFrame.maxY
|
||||
|
||||
contentNode.layer.animateSpring(
|
||||
from: -animationInContentDistance as NSNumber, to: 0.0 as NSNumber,
|
||||
keyPath: "position.y",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
initialVelocity: 0.0,
|
||||
damping: springDamping,
|
||||
additive: true
|
||||
)
|
||||
|
||||
self.actionsStackNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05)
|
||||
self.actionsStackNode.layer.animateSpring(
|
||||
from: 0.01 as NSNumber,
|
||||
to: 1.0 as NSNumber,
|
||||
keyPath: "transform.scale",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
initialVelocity: 0.0,
|
||||
damping: springDamping,
|
||||
additive: false
|
||||
)
|
||||
|
||||
let actionsSize = self.actionsStackNode.bounds.size
|
||||
|
||||
let actionsPositionDeltaXDistance: CGFloat = 0.0
|
||||
let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing
|
||||
self.actionsStackNode.layer.animateSpring(
|
||||
from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)),
|
||||
to: NSValue(cgPoint: CGPoint()),
|
||||
keyPath: "position",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
initialVelocity: 0.0,
|
||||
damping: springDamping,
|
||||
additive: true
|
||||
)
|
||||
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
reactionContextNode.animateIn(from: currentContentScreenFrame)
|
||||
}
|
||||
|
||||
contentNode.containingNode.isExtractedToContextPreview = true
|
||||
contentNode.containingNode.isExtractedToContextPreviewUpdated?(true)
|
||||
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(true, transition)
|
||||
|
||||
contentNode.containingNode.layoutUpdated = { [weak self] _, animation in
|
||||
guard let strongSelf = self, let _ = strongSelf.contentNode else {
|
||||
return
|
||||
}
|
||||
|
||||
if let _ = strongSelf.animatingOutState {
|
||||
/*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view)
|
||||
if animatingOutState.currentContentScreenFrame != updatedContentScreenFrame {
|
||||
let offset = CGPoint(
|
||||
x: updatedContentScreenFrame.minX - animatingOutState.currentContentScreenFrame.minX,
|
||||
y: updatedContentScreenFrame.minY - animatingOutState.currentContentScreenFrame.minY
|
||||
)
|
||||
let _ = offset
|
||||
|
||||
//animation.animator.updatePosition(layer: contentNode.layer, position: contentNode.position.offsetBy(dx: offset.x, dy: offset.y), completion: nil)
|
||||
|
||||
animatingOutState.currentContentScreenFrame = updatedContentScreenFrame
|
||||
}*/
|
||||
} else {
|
||||
//strongSelf.requestUpdate(animation.transition)
|
||||
|
||||
/*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view)
|
||||
if let storedGlobalFrame = contentNode.storedGlobalFrame {
|
||||
let offset = CGPoint(
|
||||
x: updatedContentScreenFrame.minX - storedGlobalFrame.minX,
|
||||
y: updatedContentScreenFrame.maxY - storedGlobalFrame.maxY
|
||||
)
|
||||
|
||||
if !offset.x.isZero || !offset.y.isZero {
|
||||
//print("contentNode.frame = \(contentNode.frame)")
|
||||
//animation.animator.updateBounds(layer: contentNode.layer, bounds: contentNode.layer.bounds.offsetBy(dx: -offset.x, dy: -offset.y), completion: nil)
|
||||
}
|
||||
|
||||
//animatingOutState.currentContentScreenFrame = updatedContentScreenFrame
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
|
||||
public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)?
|
||||
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
|
||||
public var layoutUpdated: ((CGSize) -> Void)?
|
||||
public var updateDistractionFreeMode: ((Bool) -> Void)?
|
||||
public var requestDismiss: (() -> Void)*/
|
||||
case let .animateOut(result, completion):
|
||||
let duration: Double = 0.25
|
||||
|
||||
let putBackInfo = self.source.putBack()
|
||||
|
||||
if let putBackInfo = putBackInfo {
|
||||
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: putBackInfo.contentAreaInScreenSpace, duration: duration, removeOnCompletion: false)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
|
||||
|
||||
self.animatingOutState = AnimatingOutState(
|
||||
currentContentScreenFrame: currentContentScreenFrame
|
||||
)
|
||||
|
||||
let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view)
|
||||
|
||||
let animationInContentDistance: CGFloat
|
||||
|
||||
switch result {
|
||||
case .default, .custom:
|
||||
animationInContentDistance = currentContentLocalFrame.minY - currentContentScreenFrame.minY
|
||||
case .dismissWithoutContent:
|
||||
animationInContentDistance = 0.0
|
||||
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
print("animationInContentDistance: \(animationInContentDistance)")
|
||||
|
||||
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(false, transition)
|
||||
|
||||
contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: 0.0, dy: -animationInContentDistance)
|
||||
contentNode.offsetContainerNode.layer.animate(
|
||||
from: animationInContentDistance as NSNumber,
|
||||
to: 0.0 as NSNumber,
|
||||
keyPath: "position.y",
|
||||
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue,
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
additive: true,
|
||||
completion: { [weak self] _ in
|
||||
Queue.mainQueue().after(0.2 * UIView.animationDurationFactor(), {
|
||||
contentNode.containingNode.isExtractedToContextPreview = false
|
||||
contentNode.containingNode.isExtractedToContextPreviewUpdated?(false)
|
||||
|
||||
if let strongSelf = self, let contentNode = strongSelf.contentNode {
|
||||
contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode)
|
||||
}
|
||||
|
||||
completion()
|
||||
})
|
||||
}
|
||||
)
|
||||
/*Queue.mainQueue().after((duration + 0.2) * UIView.animationDurationFactor(), { [weak self] in
|
||||
contentNode.containingNode.isExtractedToContextPreview = false
|
||||
contentNode.containingNode.isExtractedToContextPreviewUpdated?(false)
|
||||
|
||||
if let strongSelf = self, let contentNode = strongSelf.contentNode {
|
||||
contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode)
|
||||
}
|
||||
|
||||
completion()
|
||||
})*/
|
||||
|
||||
self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
|
||||
self.actionsStackNode.layer.animate(
|
||||
from: 1.0 as NSNumber,
|
||||
to: 0.01 as NSNumber,
|
||||
keyPath: "transform.scale",
|
||||
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue,
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
removeOnCompletion: false
|
||||
)
|
||||
|
||||
let actionsSize = self.actionsStackNode.bounds.size
|
||||
|
||||
let actionsPositionDeltaXDistance: CGFloat = 0.0
|
||||
let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing
|
||||
self.actionsStackNode.layer.animate(
|
||||
from: NSValue(cgPoint: CGPoint()),
|
||||
to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)),
|
||||
keyPath: "position",
|
||||
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue,
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
removeOnCompletion: false,
|
||||
additive: true
|
||||
)
|
||||
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
|
||||
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
reactionContextNode.animateOut(to: currentContentScreenFrame, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut)
|
||||
}
|
||||
case .none:
|
||||
if animateReactionsIn, let reactionContextNode = self.reactionContextNode {
|
||||
reactionContextNode.animateIn(from: contentRect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
guard let reactionContextNode = self.reactionContextNode else {
|
||||
self.requestAnimateOut(.default, completion)
|
||||
return
|
||||
}
|
||||
|
||||
var contentCompleted = false
|
||||
var reactionCompleted = false
|
||||
let intermediateCompletion: () -> Void = {
|
||||
if contentCompleted && reactionCompleted {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
self.reactionContextNodeIsAnimatingOut = true
|
||||
reactionContextNode.willAnimateOutToReaction(value: value)
|
||||
|
||||
self.requestAnimateOut(.default, {
|
||||
contentCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.reactionContextNode?.removeFromSupernode()
|
||||
strongSelf.reactionContextNode = nil
|
||||
reactionCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
enum ContextControllerPresentationNodeStateTransition {
|
||||
case animateIn
|
||||
case animateOut(result: ContextMenuActionResult, completion: () -> Void)
|
||||
}
|
||||
|
||||
protocol ContextControllerPresentationNode: ASDisplayNode {
|
||||
func replaceItems(items: ContextController.Items, animated: Bool)
|
||||
func pushItems(items: ContextController.Items)
|
||||
func popItems()
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition,
|
||||
stateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
)
|
||||
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void)
|
||||
}
|
@ -44,6 +44,12 @@ public final class PeekController: ViewController, ContextControllerProtocol {
|
||||
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
|
||||
}
|
||||
|
||||
public func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
}
|
||||
|
||||
public func popItems() {
|
||||
}
|
||||
|
||||
private var controllerNode: PeekControllerNode {
|
||||
return self.displayNode as! PeekControllerNode
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0
|
||||
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, bottomInset: 0.0, transition: .immediate)
|
||||
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: .immediate)
|
||||
|
||||
let containerFrame: CGRect
|
||||
let actionsFrame: CGRect
|
||||
|
@ -7,16 +7,6 @@ import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
|
||||
let sourceWindowFrame = fromView.convert(frame, to: nil)
|
||||
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
|
||||
|
||||
if let fromWindow = fromView.window, let toWindow = toView.window {
|
||||
targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width
|
||||
}
|
||||
return targetWindowFrame
|
||||
}
|
||||
|
||||
final class PinchSourceGesture: UIPinchGestureRecognizer {
|
||||
private final class Target {
|
||||
var updated: (() -> Void)?
|
||||
|
@ -1693,7 +1693,7 @@ public final class ControlledTransition {
|
||||
self.curve = curve
|
||||
}
|
||||
|
||||
func merge(with other: NativeAnimator) {
|
||||
func merge(with other: NativeAnimator, forceRestart: Bool) {
|
||||
var removeAnimationIndices: [Int] = []
|
||||
for i in 0 ..< self.animations.count {
|
||||
let animation = self.animations[i]
|
||||
@ -1703,7 +1703,7 @@ public final class ControlledTransition {
|
||||
let otherAnimation = other.animations[j]
|
||||
|
||||
if animation.layer === otherAnimation.layer && animation.path == otherAnimation.path {
|
||||
if animation.toValue == otherAnimation.toValue {
|
||||
if animation.toValue == otherAnimation.toValue && !forceRestart {
|
||||
removeAnimationIndices.append(i)
|
||||
} else {
|
||||
removeOtherAnimationIndices.append(j)
|
||||
@ -1932,9 +1932,9 @@ public final class ControlledTransition {
|
||||
}
|
||||
}
|
||||
|
||||
public func merge(with other: ControlledTransition) {
|
||||
public func merge(with other: ControlledTransition, forceRestart: Bool) {
|
||||
if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator {
|
||||
animator.merge(with: otherAnimator)
|
||||
animator.merge(with: otherAnimator, forceRestart: forceRestart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode {
|
||||
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
|
||||
public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)?
|
||||
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
|
||||
public var layoutUpdated: ((CGSize) -> Void)?
|
||||
public var layoutUpdated: ((CGSize, ListViewItemUpdateAnimation) -> Void)?
|
||||
public var updateDistractionFreeMode: ((Bool) -> Void)?
|
||||
public var requestDismiss: (() -> Void)?
|
||||
|
||||
|
@ -2665,6 +2665,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
if !abs(updatedApparentHeight - previousApparentHeight).isZero {
|
||||
let currentAnimation = node.animationForKey("apparentHeight")
|
||||
if let currentAnimation = currentAnimation, let toFloat = currentAnimation.to as? CGFloat, toFloat.isEqual(to: updatedApparentHeight) {
|
||||
/*node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in
|
||||
if let node = node {
|
||||
node.animateFrameTransition(progress, currentValue)
|
||||
}
|
||||
})
|
||||
node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)*/
|
||||
} else {
|
||||
node.apparentHeight = previousApparentHeight
|
||||
node.animateFrameTransition(0.0, previousApparentHeight)
|
||||
@ -2733,7 +2739,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
|
||||
for itemNode in self.itemNodes {
|
||||
itemNode.beginPendingControlledTransitions(beginAt: timestamp)
|
||||
itemNode.beginPendingControlledTransitions(beginAt: timestamp, forceRestart: false)
|
||||
}
|
||||
|
||||
if hadInserts, let reorderNode = self.reorderNode, reorderNode.supernode != nil {
|
||||
|
@ -481,16 +481,16 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
|
||||
self.pendingControlledTransitions.append(transition)
|
||||
}
|
||||
|
||||
func beginPendingControlledTransitions(beginAt: Double) {
|
||||
func beginPendingControlledTransitions(beginAt: Double, forceRestart: Bool) {
|
||||
for transition in self.pendingControlledTransitions {
|
||||
self.addControlledTransition(transition: transition, beginAt: beginAt)
|
||||
self.addControlledTransition(transition: transition, beginAt: beginAt, forceRestart: forceRestart)
|
||||
}
|
||||
self.pendingControlledTransitions.removeAll()
|
||||
}
|
||||
|
||||
func addControlledTransition(transition: ControlledTransition, beginAt: Double) {
|
||||
func addControlledTransition(transition: ControlledTransition, beginAt: Double, forceRestart: Bool) {
|
||||
for controlledTransition in self.controlledTransitions {
|
||||
transition.merge(with: controlledTransition.transition)
|
||||
transition.merge(with: controlledTransition.transition, forceRestart: forceRestart)
|
||||
}
|
||||
self.controlledTransitions.append(ControlledTransitionContext(transition: transition, beginAt: beginAt))
|
||||
}
|
||||
|
@ -523,7 +523,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
|
||||
|
||||
self.addSubnode(itemNode)
|
||||
itemNode.frame = selfSourceRect
|
||||
@ -562,7 +562,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
additionalAnimationNode.visibility = true
|
||||
})
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: {
|
||||
self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: {
|
||||
mainAnimationCompleted = true
|
||||
intermediateCompletion()
|
||||
|
@ -4,7 +4,6 @@ import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
|
||||
public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reaction: String?) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction())
|
||||
@ -59,7 +58,7 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
|
||||
if messageId.namespace != Namespaces.Message.Cloud {
|
||||
return .fail(.generic)
|
||||
}
|
||||
return network.request(Api.functions.messages.sendReaction(flags: value == nil ? 0 : 1, peer: inputPeer, msgId: messageId.id, reaction: value))
|
||||
let signal: Signal<Never, RequestUpdateMessageReactionError> = network.request(Api.functions.messages.sendReaction(flags: value == nil ? 0 : 1, peer: inputPeer, msgId: messageId.id, reaction: value))
|
||||
|> mapError { _ -> RequestUpdateMessageReactionError in
|
||||
return .generic
|
||||
}
|
||||
@ -88,6 +87,11 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
|
||||
|> castError(RequestUpdateMessageReactionError.self)
|
||||
|> ignoreValues
|
||||
}
|
||||
#if DEBUG
|
||||
return signal |> delay(0.1, queue: .mainQueue())
|
||||
#else
|
||||
return signal
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,3 +229,234 @@ private func synchronizeMessageReactions(transaction: Transaction, postbox: Post
|
||||
}
|
||||
}
|
||||
|
||||
public extension EngineMessageReactionListContext.State {
|
||||
init(message: EngineMessage, reaction: String?) {
|
||||
var totalCount: Int = 0
|
||||
if let reactionsAttribute = message._asMessage().reactionsAttribute {
|
||||
for messageReaction in reactionsAttribute.reactions {
|
||||
if reaction == nil || messageReaction.value == reaction {
|
||||
totalCount += Int(messageReaction.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.init(
|
||||
totalCount: totalCount,
|
||||
items: [],
|
||||
canLoadMore: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public final class EngineMessageReactionListContext {
|
||||
public final class Item: Equatable {
|
||||
public let peer: EnginePeer
|
||||
public let reaction: String
|
||||
|
||||
init(
|
||||
peer: EnginePeer,
|
||||
reaction: String
|
||||
) {
|
||||
self.peer = peer
|
||||
self.reaction = reaction
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.reaction != rhs.reaction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public struct State: Equatable {
|
||||
public var totalCount: Int
|
||||
public var items: [Item]
|
||||
public var canLoadMore: Bool
|
||||
|
||||
public init(
|
||||
totalCount: Int,
|
||||
items: [Item],
|
||||
canLoadMore: Bool
|
||||
) {
|
||||
self.totalCount = totalCount
|
||||
self.items = items
|
||||
self.canLoadMore = canLoadMore
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
struct InternalState: Equatable {
|
||||
var totalCount: Int
|
||||
var items: [Item]
|
||||
var canLoadMore: Bool
|
||||
var nextOffset: String?
|
||||
}
|
||||
|
||||
let queue: Queue
|
||||
|
||||
let account: Account
|
||||
let message: EngineMessage
|
||||
let reaction: String?
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
var state: InternalState
|
||||
let statePromise = Promise<InternalState>()
|
||||
|
||||
var isLoadingMore: Bool = false
|
||||
|
||||
init(queue: Queue, account: Account, message: EngineMessage, reaction: String?) {
|
||||
self.queue = queue
|
||||
self.account = account
|
||||
self.message = message
|
||||
self.reaction = reaction
|
||||
|
||||
let initialState = EngineMessageReactionListContext.State(message: message, reaction: reaction)
|
||||
self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: true, nextOffset: nil)
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func loadMore() {
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
}
|
||||
self.isLoadingMore = true
|
||||
|
||||
let account = self.account
|
||||
let message = self.message
|
||||
let reaction = self.reaction
|
||||
let currentOffset = self.state.nextOffset
|
||||
let limit = self.state.items.isEmpty ? 50 : 100
|
||||
let signal: Signal<InternalState, NoError> = self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(message.id.peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<InternalState, NoError> in
|
||||
if message.id.namespace != Namespaces.Message.Cloud {
|
||||
return .single(InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil))
|
||||
}
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .single(InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil))
|
||||
}
|
||||
var flags: Int32 = 0
|
||||
if reaction != nil {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
if currentOffset != nil {
|
||||
flags |= 1 << 1
|
||||
}
|
||||
return account.network.request(Api.functions.messages.getMessageReactionsList(flags: flags, peer: inputPeer, id: message.id.id, reaction: reaction, offset: currentOffset, limit: Int32(limit)))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.messages.MessageReactionsList?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<InternalState, NoError> in
|
||||
return account.postbox.transaction { transaction -> InternalState in
|
||||
switch result {
|
||||
case let .messageReactionsList(_, count, reactions, users, nextOffset):
|
||||
var peers: [Peer] = []
|
||||
var peerPresences: [PeerId: PeerPresence] = [:]
|
||||
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
if let presence = TelegramUserPresence(apiUser: user) {
|
||||
peerPresences[telegramUser.id] = presence
|
||||
}
|
||||
}
|
||||
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
||||
|
||||
var items: [EngineMessageReactionListContext.Item] = []
|
||||
for reaction in reactions {
|
||||
switch reaction {
|
||||
case let .messageUserReaction(userId, reaction):
|
||||
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) {
|
||||
items.append(EngineMessageReactionListContext.Item(peer: EnginePeer(peer), reaction: reaction))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset)
|
||||
case .none:
|
||||
return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.disposable.set((signal
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var existingPeerIds = Set<EnginePeer.Id>()
|
||||
for item in strongSelf.state.items {
|
||||
existingPeerIds.insert(item.peer.id)
|
||||
}
|
||||
|
||||
for item in state.items {
|
||||
if existingPeerIds.contains(item.peer.id) {
|
||||
continue
|
||||
}
|
||||
existingPeerIds.insert(item.peer.id)
|
||||
strongSelf.state.items.append(item)
|
||||
}
|
||||
if state.canLoadMore {
|
||||
strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count)
|
||||
} else {
|
||||
strongSelf.state.totalCount = strongSelf.state.items.count
|
||||
}
|
||||
strongSelf.state.canLoadMore = state.canLoadMore
|
||||
strongSelf.state.nextOffset = state.nextOffset
|
||||
|
||||
strongSelf.isLoadingMore = false
|
||||
strongSelf.statePromise.set(.single(strongSelf.state))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
public var state: Signal<State, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.statePromise.get().start(next: { state in
|
||||
subscriber.putNext(State(
|
||||
totalCount: state.totalCount,
|
||||
items: state.items,
|
||||
canLoadMore: state.canLoadMore
|
||||
))
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
init(account: Account, message: EngineMessage, reaction: String?) {
|
||||
let queue = Queue()
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, account: account, message: message, reaction: reaction)
|
||||
})
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
self.impl.with { impl in
|
||||
impl.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,6 +205,9 @@ public final class TelegramUser: Peer, Equatable {
|
||||
}
|
||||
|
||||
public static func ==(lhs: TelegramUser, rhs: TelegramUser) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
|
@ -301,5 +301,9 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func messageReactionList(message: EngineMessage, reaction: String?) -> EngineMessageReactionListContext {
|
||||
return EngineMessageReactionListContext(account: self.account, message: message, reaction: reaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Reactions.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Reactions.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ContextReactionsIcon.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<svg width="19" height="21" viewBox="0 0 19 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.69287 3.40967C6.78076 3.40967 6.854 3.3584 6.87598 3.25586C7.05908 2.18652 7.05908 2.1792 8.14307 1.98145C8.25293 1.95215 8.31152 1.89355 8.31152 1.80566C8.31152 1.70312 8.25293 1.64453 8.15039 1.62256C7.05908 1.40283 7.08105 1.39551 6.87598 0.348145C6.854 0.245605 6.78809 0.187012 6.69287 0.187012C6.59766 0.187012 6.53174 0.25293 6.51709 0.34082C6.29736 1.41016 6.33398 1.41016 5.23535 1.62256C5.13281 1.64453 5.07422 1.71045 5.07422 1.80566C5.07422 1.88623 5.13281 1.95215 5.23535 1.98145C6.33398 2.19385 6.31934 2.18652 6.51709 3.26318C6.53174 3.35107 6.59766 3.40967 6.69287 3.40967ZM11.7686 5.37256C11.9077 5.37256 12.0029 5.28467 12.0103 5.14551C12.2227 3.44629 12.2959 3.40234 14.0244 3.12402C14.1782 3.10205 14.2661 3.02148 14.2661 2.88232C14.2661 2.75781 14.1782 2.66992 14.0537 2.64795C12.3105 2.31104 12.2227 2.32568 12.0103 0.626465C12.0029 0.487305 11.9077 0.399414 11.7686 0.399414C11.6514 0.399414 11.5562 0.487305 11.5415 0.619141C11.3145 2.34766 11.2632 2.39893 9.49805 2.64795C9.37354 2.6626 9.28564 2.75781 9.28564 2.88232C9.28564 3.01416 9.37354 3.10205 9.49805 3.12402C11.2705 3.46826 11.3145 3.46094 11.5415 5.16016C11.5562 5.28467 11.6514 5.37256 11.7686 5.37256ZM6.83203 4.68408C6.71484 4.79395 6.62695 4.91846 6.57568 5.05762C5.96777 4.64014 5.25732 4.71338 4.70801 5.2627C4.51025 5.46045 4.39307 5.70947 4.37109 5.96582C3.74854 5.54102 3.06738 5.59229 2.53271 6.11963C2.02002 6.63965 1.96875 7.34277 2.37158 7.95068C2.12256 7.96533 1.89551 8.0752 1.71973 8.2583C1.12646 8.84424 1.14111 9.65723 1.74902 10.2651L2.23975 10.7559C1.9834 10.7632 1.72705 10.8877 1.5293 11.0854C0.936035 11.6787 0.958008 12.5283 1.58789 13.1582L6.11426 17.6846C8.12109 19.6841 10.4941 20.0869 12.5449 18.981C13.7754 18.9297 14.9766 18.3804 16.0239 17.333C17.9282 15.4214 18.2432 13.063 16.9468 10.3311L15.2769 6.81543C14.9619 6.13428 14.4199 5.73877 13.7607 5.74609C13.4019 5.74609 12.7866 5.93652 12.5303 6.50781C12.0176 6.18555 11.3804 6.22949 10.897 6.63232L8.95605 4.69873C8.29688 4.03955 7.46924 4.0542 6.83203 4.68408ZM10.8311 9.26904L7.38135 5.82666C7.40332 5.74609 7.44727 5.67285 7.51318 5.60693C7.71094 5.40918 7.99658 5.4165 8.20898 5.62891L10.4136 7.8335C10.4282 7.96533 10.4575 8.09717 10.5161 8.229L10.9189 9.20312C10.9336 9.23242 10.9336 9.26172 10.9116 9.27637C10.8896 9.30566 10.853 9.29834 10.8311 9.26904ZM13.0942 17.04C11.2412 18.8931 8.92676 18.8418 6.854 16.769L2.51074 12.4258C2.31299 12.228 2.30566 11.9644 2.49609 11.7666C2.68652 11.5762 2.96484 11.5762 3.15527 11.7739L5.82129 14.4399C6.04102 14.6597 6.37793 14.6377 6.58301 14.4399C6.78809 14.2275 6.81006 13.8906 6.59033 13.6709L2.59131 9.67188C2.39355 9.48145 2.38623 9.21045 2.57666 9.02002C2.76709 8.82959 3.04541 8.82959 3.24316 9.02734L6.97119 12.7554C7.19092 12.9751 7.52783 12.9531 7.72559 12.748C7.93066 12.543 7.95996 12.2061 7.74023 11.9863L3.35303 7.60645C3.15527 7.40869 3.14795 7.13037 3.33838 6.93994C3.52881 6.74951 3.80713 6.75684 4.00488 6.95459L8.32617 11.2832C8.53857 11.4956 8.87549 11.481 9.08057 11.2759C9.29297 11.0635 9.30762 10.7266 9.09521 10.5142L5.42578 6.84473C5.22803 6.64697 5.22803 6.37598 5.41846 6.18555C5.60889 5.99512 5.87988 5.99512 6.07764 6.19287L11.2339 11.3491C11.5269 11.6494 11.8857 11.6274 12.1274 11.3857C12.3325 11.1733 12.4204 10.895 12.2666 10.4995L11.3511 8.08984C11.2339 7.78955 11.3438 7.54785 11.5708 7.44531C11.8125 7.33545 12.0396 7.44531 12.186 7.76025L13.8779 11.3784C15.0059 13.7808 14.5957 15.5386 13.0942 17.04ZM14.918 10.8804L13.3579 7.57715C13.3359 7.51123 13.3213 7.44531 13.3213 7.37207C13.3213 7.15967 13.4971 6.94727 13.7827 6.94727C13.9658 6.94727 14.1489 7.07178 14.2441 7.28418L15.9067 10.8291C17.0273 13.2241 16.6245 14.9966 15.123 16.4834C15.0791 16.5273 15.0352 16.5713 14.9912 16.6079C15.9653 14.9526 15.9434 13.0337 14.918 10.8804ZM2.40088 20.0649C2.51807 20.0649 2.58398 19.9917 2.60596 19.8745C2.8623 18.439 2.85498 18.4097 4.34912 18.1313C4.47363 18.1094 4.53955 18.0435 4.53955 17.9263C4.53955 17.8018 4.47363 17.7358 4.35645 17.7212C2.85498 17.4136 2.88428 17.3916 2.60596 15.9707C2.58398 15.8535 2.51807 15.7876 2.40088 15.7876C2.28369 15.7876 2.21777 15.8535 2.1958 15.9707C1.91016 17.4062 1.9541 17.4282 0.452637 17.7212C0.335449 17.7358 0.262207 17.8018 0.262207 17.9263C0.262207 18.0435 0.335449 18.1094 0.452637 18.1313C1.9541 18.4316 1.93213 18.4463 2.1958 19.8818C2.21777 19.9917 2.28369 20.0649 2.40088 20.0649Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.4 KiB |
@ -1064,8 +1064,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})
|
||||
} else if updatedReaction == nil {
|
||||
itemNode.awaitingAppliedReaction = (nil, {
|
||||
})
|
||||
controller?.dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -543,7 +543,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
return transaction.getCombinedPeerReadState(messages[0].id.peerId)
|
||||
}
|
||||
|
||||
let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32), NoError> = combineLatest(
|
||||
let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?), NoError> = combineLatest(
|
||||
loadLimits,
|
||||
loadStickerSaveStatusSignal,
|
||||
loadResourceStatusSignal,
|
||||
@ -552,9 +552,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
|> take(1),
|
||||
cachedData,
|
||||
readState,
|
||||
ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager)
|
||||
ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager),
|
||||
context.engine.stickers.availableReactions()
|
||||
)
|
||||
|> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32) in
|
||||
|> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips, availableReactions -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?) in
|
||||
let (limitsConfiguration, appConfig) = limitsAndAppConfig
|
||||
var canEdit = false
|
||||
if !isAction {
|
||||
@ -567,12 +568,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
isMessageRead = readState.isOutgoingMessageIndexRead(message.index)
|
||||
}
|
||||
|
||||
return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips)
|
||||
return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions)
|
||||
}
|
||||
|
||||
return dataSignal
|
||||
|> deliverOnMainQueue
|
||||
|> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips -> ContextController.Items in
|
||||
|> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions -> ContextController.Items in
|
||||
var actions: [ContextMenuItem] = []
|
||||
|
||||
var isPinnedMessages = false
|
||||
@ -1209,9 +1210,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
controllerInteraction.openPeer(stats.peers[0].id, .default, nil)
|
||||
})
|
||||
} else if !stats.peers.isEmpty || reactionCount != 0 {
|
||||
if reactionCount != 0, !"".isEmpty {
|
||||
let minHeight = c.getActionsMinHeight()
|
||||
c.setItems(.single(ContextController.Items(content: .custom(ReactionListContextMenuContent()), tip: nil)), minHeight: minHeight, previousActionsTransition: .slide(forward: true))
|
||||
if reactionCount != 0 {
|
||||
c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), back: { [weak c] in
|
||||
c?.popItems()
|
||||
}, openPeer: { [weak c] id in
|
||||
c?.dismiss(completion: {
|
||||
controllerInteraction.openPeer(id, .default, nil)
|
||||
})
|
||||
})), tip: nil)))
|
||||
} else {
|
||||
var subActions: [ContextMenuItem] = []
|
||||
|
||||
@ -1837,7 +1843,11 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
|
||||
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
if let reactionsAttribute = item.message.reactionsAttribute, !reactionsAttribute.reactions.isEmpty {
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.actionSheet.primaryTextColor)
|
||||
} else {
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: presentationData.theme.actionSheet.primaryTextColor)
|
||||
}
|
||||
|
||||
self.avatarsNode = AnimatedAvatarSetNode()
|
||||
self.avatarsContext = AnimatedAvatarSetContext()
|
||||
@ -2053,7 +2063,16 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
|
||||
let placeholderAvatarsContent: AnimatedAvatarSetContext.Content
|
||||
|
||||
var avatarsPeers: [EnginePeer] = []
|
||||
if let peers = self.currentStats?.peers {
|
||||
if let recentPeers = self.item.message.reactionsAttribute?.recentPeers, !recentPeers.isEmpty {
|
||||
for recentPeer in recentPeers {
|
||||
if let peer = self.item.message.peers[recentPeer.peerId] {
|
||||
avatarsPeers.append(EnginePeer(peer))
|
||||
if avatarsPeers.count == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let peers = self.currentStats?.peers {
|
||||
for i in 0 ..< min(3, peers.count) {
|
||||
avatarsPeers.append(peers[i])
|
||||
}
|
||||
|
@ -2209,6 +2209,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
|
||||
strongSelf.updateAccessibilityData(accessibilityData)
|
||||
|
||||
var animation = animation
|
||||
if strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
||||
animation = .System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .easeInOut, interactive: false))
|
||||
}
|
||||
|
||||
var legacyTransition: ContainedViewLayoutTransition = .immediate
|
||||
var useDisplayLinkAnimations = false
|
||||
if case let .System(duration, _) = animation {
|
||||
@ -2534,7 +2539,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
contentContainer?.containerNode.targetNodeForActivationProgressContentRect = CGRect(origin: CGPoint(x: backgroundFrame.minX + incomingOffset, y: 0.0), size: relativeFrame.size)
|
||||
|
||||
if previousContextFrame?.size != contentContainer?.containerNode.bounds.size || previousContextContentFrame != contentContainer?.sourceNode.contentRect {
|
||||
contentContainer?.sourceNode.layoutUpdated?(relativeFrame.size)
|
||||
contentContainer?.sourceNode.layoutUpdated?(relativeFrame.size, animation)
|
||||
}
|
||||
|
||||
var selectionInsets = UIEdgeInsets()
|
||||
@ -2724,16 +2729,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: animation.transition)
|
||||
strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: animation.transition)
|
||||
|
||||
if let type = strongSelf.backgroundNode.type {
|
||||
var incomingOffset: CGFloat = 0.0
|
||||
if let _ = strongSelf.backgroundNode.type {
|
||||
/*var incomingOffset: CGFloat = 0.0
|
||||
switch type {
|
||||
case .incoming:
|
||||
incomingOffset = 5.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
|
||||
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
|
||||
}*/
|
||||
//strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
|
||||
//strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
|
||||
if !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
||||
if let (rect, size) = strongSelf.absoluteRect {
|
||||
strongSelf.updateAbsoluteRect(rect, within: size)
|
||||
@ -2870,7 +2875,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
|
||||
|
||||
if previousContextFrame.size != strongSelf.mainContextSourceNode.bounds.size || previousContextContentFrame != strongSelf.mainContextSourceNode.contentRect {
|
||||
strongSelf.mainContextSourceNode.layoutUpdated?(strongSelf.mainContextSourceNode.bounds.size)
|
||||
strongSelf.mainContextSourceNode.layoutUpdated?(strongSelf.mainContextSourceNode.bounds.size, animation)
|
||||
}
|
||||
|
||||
strongSelf.updateSearchTextHighlightState()
|
||||
@ -2924,11 +2929,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
|
||||
override func shouldAnimateHorizontalFrameTransition() -> Bool {
|
||||
if let _ = self.backgroundFrameTransition {
|
||||
return false
|
||||
/*if let _ = self.backgroundFrameTransition {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
|
@ -285,7 +285,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode {
|
||||
|
||||
var currentContentRect = self.contextSourceNode.contentRect
|
||||
let contextSourceNode = self.contextSourceNode
|
||||
self.contextSourceNode.layoutUpdated = { [weak self, weak contextSourceNode] size in
|
||||
self.contextSourceNode.layoutUpdated = { [weak self, weak contextSourceNode] size, _ in
|
||||
guard let strongSelf = self, let contextSourceNode = contextSourceNode, strongSelf.contextSourceNode === contextSourceNode else {
|
||||
return
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user