Reaction improvements

This commit is contained in:
Ali 2021-12-15 02:05:14 +04:00
parent 9f64981aca
commit 7c8315218a
25 changed files with 2644 additions and 172 deletions

View File

@ -46,7 +46,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
var defaultReaction: String? var defaultReaction: String?
if let availableReactions = availableReactions { if let availableReactions = availableReactions {
for reaction in availableReactions.reactions { for reaction in availableReactions.reactions {
if reaction.title.lowercased().contains("heart") { if reaction.title.lowercased().contains("thumbs up") {
defaultReaction = reaction.value defaultReaction = reaction.value
} }
} }

View File

@ -20,6 +20,7 @@ swift_library(
"//submodules/WebPBinding:WebPBinding", "//submodules/WebPBinding:WebPBinding",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/ContextUI:ContextUI", "//submodules/ContextUI:ContextUI",
"//submodules/AvatarNode:AvatarNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -11,36 +11,793 @@ import UIKit
import WebPBinding import WebPBinding
import AnimatedAvatarSetNode import AnimatedAvatarSetNode
import ContextUI import ContextUI
import AvatarNode
public final class ReactionListContextMenuContent: ContextControllerItemsContent { private final class ReactionImageNode: ASImageNode {
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode { private var disposable: Disposable?
private let contentNode: ASDisplayNode let size: CGSize
override init() { init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) {
self.contentNode = ASDisplayNode() 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() super.init()
self.addSubnode(self.contentNode) self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource)
//self.contentNode.backgroundColor = .blue |> 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) { if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
let size = CGSize(width: min(260.0, constrainedWidth), height: maxHeight) if let image = WebP.convert(fromWebP: dataValue) {
strongSelf.image = image
let contentSize = CGSize(width: size.width, height: size.height + bottomInset + 14.0) }
//contentSize.height = 120.0 }
})
self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize) } else {
self.size = CGSize(width: 18.0, height: 18.0)
return (size, contentSize) 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
)
} }
} }

View File

@ -41,43 +41,7 @@ private enum ContextItemNode {
case separator(ASDisplayNode) case separator(ASDisplayNode)
} }
private protocol ContextInnerActionsContainerNode: ASDisplayNode { private final class InnerActionsContainerNode: 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 let blurBackground: Bool private let blurBackground: Bool
private let presentationData: PresentationData private let presentationData: PresentationData
private let containerNode: ASDisplayNode private let containerNode: ASDisplayNode
@ -225,7 +189,7 @@ private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerAction
gesture.isEnabled = self.panSelectionGestureEnabled 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 var minActionsWidth: CGFloat = 250.0
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth { if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
minActionsWidth = minimalWidth minActionsWidth = minimalWidth
@ -334,7 +298,7 @@ private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerAction
if let effectView = self.effectView { if let effectView = self.effectView {
transition.updateFrame(view: effectView, frame: bounds) transition.updateFrame(view: effectView, frame: bounds)
} }
return (size, size) return size
} }
func updateTheme(presentationData: PresentationData) { func updateTheme(presentationData: PresentationData) {
@ -517,12 +481,9 @@ final class ContextActionsContainerNode: ASDisplayNode {
private let shadowNode: ASImageNode private let shadowNode: ASImageNode
private let additionalShadowNode: ASImageNode? private let additionalShadowNode: ASImageNode?
private let additionalActionsNode: InnerActionsContainerNode? private let additionalActionsNode: InnerActionsContainerNode?
private let actionsNode: InnerActionsContainerNode
private let contentContainerNode: ASDisplayNode
private let actionsNode: ContextInnerActionsContainerNode
private let textSelectionTipNode: InnerTextSelectionTipContainerNode? private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
//private let scrollNode: ASScrollNode private let scrollNode: ASScrollNode
var panSelectionGestureEnabled: Bool = true { var panSelectionGestureEnabled: Bool = true {
didSet { didSet {
@ -545,11 +506,6 @@ final class ContextActionsContainerNode: ASDisplayNode {
self.shadowNode.contentMode = .scaleToFill self.shadowNode.contentMode = .scaleToFill
self.shadowNode.isHidden = true 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 var items = items
if case var .list(itemList) = items.content, let firstItem = itemList.first, case let .custom(_, additional) = firstItem, additional { if case var .list(itemList) = items.content, let firstItem = itemList.first, case let .custom(_, additional) = firstItem, additional {
let additionalShadowNode = ASImageNode() let additionalShadowNode = ASImageNode()
@ -568,8 +524,11 @@ final class ContextActionsContainerNode: ASDisplayNode {
self.additionalActionsNode = nil self.additionalActionsNode = nil
} }
switch items.content { var itemList: [ContextMenuItem] = []
case let .list(itemList): 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) self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: itemList, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground)
if let tip = items.tip { if let tip = items.tip {
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip) let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip)
@ -578,64 +537,58 @@ final class ContextActionsContainerNode: ASDisplayNode {
} else { } else {
self.textSelectionTipNode = nil 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.canCancelAllTouchesInViews = true
self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.clipsToBounds = false
if #available(iOS 11.0, *) { if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}*/ }
super.init() super.init()
self.addSubnode(self.shadowNode) self.addSubnode(self.shadowNode)
self.additionalShadowNode.flatMap(self.addSubnode) self.additionalShadowNode.flatMap(self.addSubnode)
self.additionalActionsNode.flatMap(self.contentContainerNode.addSubnode) self.additionalActionsNode.flatMap(self.scrollNode.addSubnode)
self.contentContainerNode.addSubnode(self.actionsNode) self.scrollNode.addSubnode(self.actionsNode)
self.textSelectionTipNode.flatMap(self.addSubnode) self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
self.addSubnode(self.contentContainerNode) 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 var widthClass = widthClass
if !self.blurBackground { if !self.blurBackground {
widthClass = .regular widthClass = .regular
} }
var contentSize = CGSize() 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 { 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) let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, constrainedHeight: constrainedHeight, minimalWidth: actionsSize.width, transition: transition)
contentSize = additionalActionsLayout.cleanSize 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)) transition.updateFrame(node: additionalShadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
additionalShadowNode.isHidden = widthClass == .compact 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 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)) transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
self.shadowNode.isHidden = widthClass == .compact self.shadowNode.isHidden = widthClass == .compact
contentSize.width = max(contentSize.width, actionsLayout.cleanSize.width) contentSize.width = max(contentSize.width, actionsSize.width)
contentSize.height += actionsLayout.cleanSize.height contentSize.height += actionsSize.height
transition.updateFrame(node: self.actionsNode, frame: bounds) transition.updateFrame(node: self.actionsNode, frame: bounds)
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
if let textSelectionTipNode = self.textSelectionTipNode { if let textSelectionTipNode = self.textSelectionTipNode {
contentSize.height += 8.0 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)) transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize))
contentSize.height += textSelectionTipSize.height contentSize.height += textSelectionTipSize.height
} }
@ -644,8 +597,8 @@ final class ContextActionsContainerNode: ASDisplayNode {
} }
func updateSize(containerSize: CGSize, contentSize: CGSize) { func updateSize(containerSize: CGSize, contentSize: CGSize) {
//self.scrollNode.view.contentSize = contentSize self.scrollNode.view.contentSize = contentSize
//self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize) self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
} }
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? { func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {

View File

@ -11,13 +11,15 @@ import AccountContext
private let animationDurationFactor: Double = 1.0 private let animationDurationFactor: Double = 1.0
public protocol ContextControllerProtocol { public protocol ContextControllerProtocol: AnyObject {
var useComplexItemsTransitionAnimation: Bool { get set } var useComplexItemsTransitionAnimation: Bool { get set }
var immediateItemsTransitionAnimation: Bool { get set } var immediateItemsTransitionAnimation: Bool { get set }
func getActionsMinHeight() -> ContextController.ActionsHeight? func getActionsMinHeight() -> ContextController.ActionsHeight?
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: 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 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)?) func dismiss(completion: (() -> Void)?)
} }
@ -158,7 +160,7 @@ public enum ContextMenuItem {
case separator 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) let sourceWindowFrame = fromView.convert(frame, to: nil)
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil) var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
@ -196,6 +198,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private let dismissNode: ASDisplayNode private let dismissNode: ASDisplayNode
private let dismissAccessibilityArea: AccessibilityAreaNode private let dismissAccessibilityArea: AccessibilityAreaNode
private var presentationNode: ContextControllerPresentationNode?
private var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition?
private let clippingNode: ASDisplayNode private let clippingNode: ASDisplayNode
private let scrollNode: ASScrollNode 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 { switch source {
case .reference, .extracted: case .reference, .extracted:
self.contentReady.set(.single(true)) self.contentReady.set(.single(true))
@ -480,6 +480,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.initializeContent() 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.dismissAccessibilityArea.activate = { [weak self] in
self?.dimNodeTapped() self?.dimNodeTapped()
return true return true
@ -523,7 +528,40 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
} }
case let .extracted(source): 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 { if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode {
self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace) self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace)
@ -553,7 +591,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
takenViewInfo.contentContainingNode.isExtractedToContextPreviewUpdated?(true) 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)) 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): case let .controller(source):
let transitionInfo = source.transitionInfo() let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() { if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() {
@ -574,9 +612,21 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
func animateIn() { func animateIn() {
self.gesture?.endPressedAppearance() self.gesture?.endPressedAppearance()
self.hapticFeedback.impact() 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 { switch self.source {
case .reference: case .reference:
break break
@ -759,6 +809,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.beganAnimatingOut() 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 transitionDuration: Double = 0.2
var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut 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) { 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 { guard let reactionContextNode = self.reactionContextNode else {
self.animateOut(result: .default, completion: completion) self.animateOut(result: .default, completion: completion)
return return
@ -1207,6 +1274,16 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
} }
private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { 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 { if let _ = self.currentItems, !self.didCompleteAnimationIn && self.getController()?.immediateItemsTransitionAnimation == true {
return return
} }
@ -1248,7 +1325,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode)
if let layout = self.validLayout { 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 { } else {
previousActionsContainerNode.removeFromSupernode() 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) { func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData self.presentationData = presentationData
@ -1283,6 +1376,20 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.validLayout = layout 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 var actionsContainerTransition = transition
if previousActionsContainerNode != nil { if previousActionsContainerNode != nil {
actionsContainerTransition = .immediate actionsContainerTransition = .immediate
@ -1340,7 +1447,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) 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 let adjustedActionsSize = realActionsSize
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize)
@ -1437,7 +1544,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
constrainedActionsBottomInset = 0.0 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 let adjustedActionsSize = realActionsSize
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: 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) 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 let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth
var contentUnscaledSize: CGSize var contentUnscaledSize: CGSize
if case .compact = layout.metrics.widthClass { if case .compact = layout.metrics.widthClass {
@ -1800,6 +1907,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
return nil 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 reactionContextNode = self.reactionContextNode {
if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) { if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) {
return result return result
@ -1965,11 +2076,16 @@ public enum ContextContentSource {
} }
public protocol ContextControllerItemsNode: ASDisplayNode { 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 { 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 { 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) { public func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData self.presentationData = presentationData
if self.isNodeLoaded { if self.isNodeLoaded {

View File

@ -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)
}
}

View File

@ -6,7 +6,568 @@ import TelegramPresentationData
import TextSelectionNode import TextSelectionNode
import TelegramCore import TelegramCore
import SwiftSignalKit 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()
})
}
} }

View File

@ -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)
}

View File

@ -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 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 { private var controllerNode: PeekControllerNode {
return self.displayNode as! PeekControllerNode return self.displayNode as! PeekControllerNode
} }

View File

@ -158,7 +158,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
} }
let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0 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 containerFrame: CGRect
let actionsFrame: CGRect let actionsFrame: CGRect

View File

@ -7,16 +7,6 @@ import TextSelectionNode
import TelegramCore import TelegramCore
import SwiftSignalKit 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 { final class PinchSourceGesture: UIPinchGestureRecognizer {
private final class Target { private final class Target {
var updated: (() -> Void)? var updated: (() -> Void)?

View File

@ -1693,7 +1693,7 @@ public final class ControlledTransition {
self.curve = curve self.curve = curve
} }
func merge(with other: NativeAnimator) { func merge(with other: NativeAnimator, forceRestart: Bool) {
var removeAnimationIndices: [Int] = [] var removeAnimationIndices: [Int] = []
for i in 0 ..< self.animations.count { for i in 0 ..< self.animations.count {
let animation = self.animations[i] let animation = self.animations[i]
@ -1703,7 +1703,7 @@ public final class ControlledTransition {
let otherAnimation = other.animations[j] let otherAnimation = other.animations[j]
if animation.layer === otherAnimation.layer && animation.path == otherAnimation.path { if animation.layer === otherAnimation.layer && animation.path == otherAnimation.path {
if animation.toValue == otherAnimation.toValue { if animation.toValue == otherAnimation.toValue && !forceRestart {
removeAnimationIndices.append(i) removeAnimationIndices.append(i)
} else { } else {
removeOtherAnimationIndices.append(j) 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 { if let animator = self.animator as? NativeAnimator, let otherAnimator = other.animator as? NativeAnimator {
animator.merge(with: otherAnimator) animator.merge(with: otherAnimator, forceRestart: forceRestart)
} }
} }
} }

View File

@ -13,7 +13,7 @@ public final class ContextExtractedContentContainingNode: ASDisplayNode {
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)? public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)? public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)?
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> 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 updateDistractionFreeMode: ((Bool) -> Void)?
public var requestDismiss: (() -> Void)? public var requestDismiss: (() -> Void)?

View File

@ -2665,6 +2665,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if !abs(updatedApparentHeight - previousApparentHeight).isZero { if !abs(updatedApparentHeight - previousApparentHeight).isZero {
let currentAnimation = node.animationForKey("apparentHeight") let currentAnimation = node.animationForKey("apparentHeight")
if let currentAnimation = currentAnimation, let toFloat = currentAnimation.to as? CGFloat, toFloat.isEqual(to: updatedApparentHeight) { 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 { } else {
node.apparentHeight = previousApparentHeight node.apparentHeight = previousApparentHeight
node.animateFrameTransition(0.0, previousApparentHeight) node.animateFrameTransition(0.0, previousApparentHeight)
@ -2733,7 +2739,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
for itemNode in self.itemNodes { for itemNode in self.itemNodes {
itemNode.beginPendingControlledTransitions(beginAt: timestamp) itemNode.beginPendingControlledTransitions(beginAt: timestamp, forceRestart: false)
} }
if hadInserts, let reorderNode = self.reorderNode, reorderNode.supernode != nil { if hadInserts, let reorderNode = self.reorderNode, reorderNode.supernode != nil {

View File

@ -481,16 +481,16 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
self.pendingControlledTransitions.append(transition) self.pendingControlledTransitions.append(transition)
} }
func beginPendingControlledTransitions(beginAt: Double) { func beginPendingControlledTransitions(beginAt: Double, forceRestart: Bool) {
for transition in self.pendingControlledTransitions { for transition in self.pendingControlledTransitions {
self.addControlledTransition(transition: transition, beginAt: beginAt) self.addControlledTransition(transition: transition, beginAt: beginAt, forceRestart: forceRestart)
} }
self.pendingControlledTransitions.removeAll() self.pendingControlledTransitions.removeAll()
} }
func addControlledTransition(transition: ControlledTransition, beginAt: Double) { func addControlledTransition(transition: ControlledTransition, beginAt: Double, forceRestart: Bool) {
for controlledTransition in self.controlledTransitions { 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)) self.controlledTransitions.append(ControlledTransitionContext(transition: transition, beginAt: beginAt))
} }

View File

@ -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 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) self.addSubnode(itemNode)
itemNode.frame = selfSourceRect itemNode.frame = selfSourceRect
@ -562,7 +562,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
additionalAnimationNode.visibility = true 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: { self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: {
mainAnimationCompleted = true mainAnimationCompleted = true
intermediateCompletion() intermediateCompletion()

View File

@ -4,7 +4,6 @@ import SwiftSignalKit
import TelegramApi import TelegramApi
import MtProtoKit import MtProtoKit
public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reaction: String?) -> Signal<Never, NoError> { public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reaction: String?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in return account.postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction()) 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 { if messageId.namespace != Namespaces.Message.Cloud {
return .fail(.generic) 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 |> mapError { _ -> RequestUpdateMessageReactionError in
return .generic return .generic
} }
@ -88,6 +87,11 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
|> castError(RequestUpdateMessageReactionError.self) |> castError(RequestUpdateMessageReactionError.self)
|> ignoreValues |> 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()
}
}
}

View File

@ -205,6 +205,9 @@ public final class TelegramUser: Peer, Equatable {
} }
public static func ==(lhs: TelegramUser, rhs: TelegramUser) -> Bool { public static func ==(lhs: TelegramUser, rhs: TelegramUser) -> Bool {
if lhs === rhs {
return true
}
if lhs.id != rhs.id { if lhs.id != rhs.id {
return false return false
} }

View File

@ -301,5 +301,9 @@ public extension TelegramEngine {
} }
} }
} }
public func messageReactionList(message: EngineMessage, reaction: String?) -> EngineMessageReactionListContext {
return EngineMessageReactionListContext(account: self.account, message: message, reaction: reaction)
}
} }
} }

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ContextReactionsIcon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -1064,8 +1064,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
} else if updatedReaction == nil { } else if updatedReaction == nil {
itemNode.awaitingAppliedReaction = (nil, { itemNode.awaitingAppliedReaction = (nil, {
})
controller?.dismiss() controller?.dismiss()
})
} }
} }
} }

View File

@ -543,7 +543,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return transaction.getCombinedPeerReadState(messages[0].id.peerId) 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, loadLimits,
loadStickerSaveStatusSignal, loadStickerSaveStatusSignal,
loadResourceStatusSignal, loadResourceStatusSignal,
@ -552,9 +552,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|> take(1), |> take(1),
cachedData, cachedData,
readState, 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 let (limitsConfiguration, appConfig) = limitsAndAppConfig
var canEdit = false var canEdit = false
if !isAction { if !isAction {
@ -567,12 +568,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
isMessageRead = readState.isOutgoingMessageIndexRead(message.index) 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 return dataSignal
|> deliverOnMainQueue |> 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 actions: [ContextMenuItem] = []
var isPinnedMessages = false var isPinnedMessages = false
@ -1209,9 +1210,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
controllerInteraction.openPeer(stats.peers[0].id, .default, nil) controllerInteraction.openPeer(stats.peers[0].id, .default, nil)
}) })
} else if !stats.peers.isEmpty || reactionCount != 0 { } else if !stats.peers.isEmpty || reactionCount != 0 {
if reactionCount != 0, !"".isEmpty { if reactionCount != 0 {
let minHeight = c.getActionsMinHeight() c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), back: { [weak c] in
c.setItems(.single(ContextController.Items(content: .custom(ReactionListContextMenuContent()), tip: nil)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) c?.popItems()
}, openPeer: { [weak c] id in
c?.dismiss(completion: {
controllerInteraction.openPeer(id, .default, nil)
})
})), tip: nil)))
} else { } else {
var subActions: [ContextMenuItem] = [] var subActions: [ContextMenuItem] = []
@ -1837,7 +1843,11 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
self.iconNode = ASImageNode() 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.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: presentationData.theme.actionSheet.primaryTextColor)
}
self.avatarsNode = AnimatedAvatarSetNode() self.avatarsNode = AnimatedAvatarSetNode()
self.avatarsContext = AnimatedAvatarSetContext() self.avatarsContext = AnimatedAvatarSetContext()
@ -2053,7 +2063,16 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
let placeholderAvatarsContent: AnimatedAvatarSetContext.Content let placeholderAvatarsContent: AnimatedAvatarSetContext.Content
var avatarsPeers: [EnginePeer] = [] 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) { for i in 0 ..< min(3, peers.count) {
avatarsPeers.append(peers[i]) avatarsPeers.append(peers[i])
} }

View File

@ -2209,6 +2209,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
strongSelf.updateAccessibilityData(accessibilityData) 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 legacyTransition: ContainedViewLayoutTransition = .immediate
var useDisplayLinkAnimations = false var useDisplayLinkAnimations = false
if case let .System(duration, _) = animation { 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) 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 { 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() var selectionInsets = UIEdgeInsets()
@ -2724,16 +2729,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: animation.transition) strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: animation.transition)
strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: animation.transition) strongSelf.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: animation.transition)
if let type = strongSelf.backgroundNode.type { if let _ = strongSelf.backgroundNode.type {
var incomingOffset: CGFloat = 0.0 /*var incomingOffset: CGFloat = 0.0
switch type { switch type {
case .incoming: case .incoming:
incomingOffset = 5.0 incomingOffset = 5.0
default: default:
break break
} }*/
strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0) //strongSelf.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect //strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
if !strongSelf.mainContextSourceNode.isExtractedToContextPreview { if !strongSelf.mainContextSourceNode.isExtractedToContextPreview {
if let (rect, size) = strongSelf.absoluteRect { if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size) strongSelf.updateAbsoluteRect(rect, within: size)
@ -2870,7 +2875,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect strongSelf.mainContainerNode.targetNodeForActivationProgressContentRect = strongSelf.mainContextSourceNode.contentRect
if previousContextFrame.size != strongSelf.mainContextSourceNode.bounds.size || previousContextContentFrame != 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() strongSelf.updateSearchTextHighlightState()
@ -2924,11 +2929,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
override func shouldAnimateHorizontalFrameTransition() -> Bool { override func shouldAnimateHorizontalFrameTransition() -> Bool {
if let _ = self.backgroundFrameTransition { return false
/*if let _ = self.backgroundFrameTransition {
return true return true
} else { } else {
return false return false
} }*/
} }
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {

View File

@ -285,7 +285,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode {
var currentContentRect = self.contextSourceNode.contentRect var currentContentRect = self.contextSourceNode.contentRect
let contextSourceNode = self.contextSourceNode 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 { guard let strongSelf = self, let contextSourceNode = contextSourceNode, strongSelf.contextSourceNode === contextSourceNode else {
return return
} }