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?
if let availableReactions = availableReactions {
for reaction in availableReactions.reactions {
if reaction.title.lowercased().contains("heart") {
if reaction.title.lowercased().contains("thumbs up") {
defaultReaction = reaction.value
}
}

View File

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

View File

@ -11,36 +11,793 @@ import UIKit
import WebPBinding
import AnimatedAvatarSetNode
import ContextUI
import AvatarNode
public final class ReactionListContextMenuContent: ContextControllerItemsContent {
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode {
private let contentNode: ASDisplayNode
private final class ReactionImageNode: ASImageNode {
private var disposable: Disposable?
let size: CGSize
override init() {
self.contentNode = ASDisplayNode()
init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) {
var file: TelegramMediaFile?
if let availableReactions = availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction {
file = availableReaction.staticIcon
break
}
}
}
if let file = file {
self.size = file.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0)
super.init()
self.addSubnode(self.contentNode)
//self.contentNode.backgroundColor = .blue
self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) {
let size = CGSize(width: min(260.0, constrainedWidth), height: maxHeight)
let contentSize = CGSize(width: size.width, height: size.height + bottomInset + 14.0)
//contentSize.height = 120.0
self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
return (size, contentSize)
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = WebP.convert(fromWebP: dataValue) {
strongSelf.image = image
}
}
})
} else {
self.size = CGSize(width: 18.0, height: 18.0)
super.init()
}
}
public init() {
}
public func node() -> ContextControllerItemsNode {
return ItemsNode()
deinit {
self.disposable?.dispose()
}
}
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)
}
private protocol ContextInnerActionsContainerNode: ASDisplayNode {
var panSelectionGestureEnabled: Bool { get set }
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize)
func updateTheme(presentationData: PresentationData)
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol?
}
private final class InnerCustomActionsContainerNode: ASDisplayNode, ContextInnerActionsContainerNode {
private let node: ContextControllerItemsNode
var panSelectionGestureEnabled: Bool = false
init(content: ContextControllerItemsContent) {
self.node = content.node()
super.init()
self.addSubnode(self.node)
}
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) {
let nodeLayout = self.node.update(constrainedWidth: constrainedWidth, maxHeight: constrainedHeight, bottomInset: bottomInset, transition: transition)
transition.updateFrame(node: self.node, frame: CGRect(origin: CGPoint(), size: nodeLayout.cleanSize))
return (nodeLayout.cleanSize, nodeLayout.visibleSize)
}
func updateTheme(presentationData: PresentationData) {
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {
return nil
}
}
private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerActionsContainerNode {
private final class InnerActionsContainerNode: ASDisplayNode {
private let blurBackground: Bool
private let presentationData: PresentationData
private let containerNode: ASDisplayNode
@ -225,7 +189,7 @@ private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerAction
gesture.isEnabled = self.panSelectionGestureEnabled
}
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) {
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> CGSize {
var minActionsWidth: CGFloat = 250.0
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
minActionsWidth = minimalWidth
@ -334,7 +298,7 @@ private final class InnerActionsContainerNode: ASDisplayNode, ContextInnerAction
if let effectView = self.effectView {
transition.updateFrame(view: effectView, frame: bounds)
}
return (size, size)
return size
}
func updateTheme(presentationData: PresentationData) {
@ -517,12 +481,9 @@ final class ContextActionsContainerNode: ASDisplayNode {
private let shadowNode: ASImageNode
private let additionalShadowNode: ASImageNode?
private let additionalActionsNode: InnerActionsContainerNode?
private let contentContainerNode: ASDisplayNode
private let actionsNode: ContextInnerActionsContainerNode
private let actionsNode: InnerActionsContainerNode
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
//private let scrollNode: ASScrollNode
private let scrollNode: ASScrollNode
var panSelectionGestureEnabled: Bool = true {
didSet {
@ -545,11 +506,6 @@ final class ContextActionsContainerNode: ASDisplayNode {
self.shadowNode.contentMode = .scaleToFill
self.shadowNode.isHidden = true
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.clipsToBounds = true
self.contentContainerNode.cornerRadius = 14.0
self.contentContainerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
var items = items
if case var .list(itemList) = items.content, let firstItem = itemList.first, case let .custom(_, additional) = firstItem, additional {
let additionalShadowNode = ASImageNode()
@ -568,8 +524,11 @@ final class ContextActionsContainerNode: ASDisplayNode {
self.additionalActionsNode = nil
}
switch items.content {
case let .list(itemList):
var itemList: [ContextMenuItem] = []
if case let .list(list) = items.content {
itemList = list
}
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: itemList, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground)
if let tip = items.tip {
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip)
@ -578,64 +537,58 @@ final class ContextActionsContainerNode: ASDisplayNode {
} else {
self.textSelectionTipNode = nil
}
case let .custom(customContent):
self.actionsNode = InnerCustomActionsContainerNode(content: customContent)
self.textSelectionTipNode = nil
}
/*self.scrollNode = ASScrollNode()
self.scrollNode = ASScrollNode()
self.scrollNode.canCancelAllTouchesInViews = true
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.clipsToBounds = false
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}*/
}
super.init()
self.addSubnode(self.shadowNode)
self.additionalShadowNode.flatMap(self.addSubnode)
self.additionalActionsNode.flatMap(self.contentContainerNode.addSubnode)
self.contentContainerNode.addSubnode(self.actionsNode)
self.textSelectionTipNode.flatMap(self.addSubnode)
self.addSubnode(self.contentContainerNode)
self.additionalActionsNode.flatMap(self.scrollNode.addSubnode)
self.scrollNode.addSubnode(self.actionsNode)
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
self.addSubnode(self.scrollNode)
}
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
var widthClass = widthClass
if !self.blurBackground {
widthClass = .regular
}
var contentSize = CGSize()
let actionsLayout = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, bottomInset: bottomInset, minimalWidth: nil, transition: transition)
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, minimalWidth: nil, transition: transition)
if let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode {
let additionalActionsLayout = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsLayout.cleanSize.width, constrainedHeight: constrainedHeight, bottomInset: 0.0, minimalWidth: actionsLayout.cleanSize.width, transition: transition)
contentSize = additionalActionsLayout.cleanSize
let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, constrainedHeight: constrainedHeight, minimalWidth: actionsSize.width, transition: transition)
contentSize = additionalActionsSize
let bounds = CGRect(origin: CGPoint(), size: additionalActionsLayout.cleanSize)
let bounds = CGRect(origin: CGPoint(), size: additionalActionsSize)
transition.updateFrame(node: additionalShadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
additionalShadowNode.isHidden = widthClass == .compact
transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsLayout.cleanSize))
transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsSize))
contentSize.height += 8.0
}
let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsLayout.visibleSize)
let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsSize)
transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
self.shadowNode.isHidden = widthClass == .compact
contentSize.width = max(contentSize.width, actionsLayout.cleanSize.width)
contentSize.height += actionsLayout.cleanSize.height
contentSize.width = max(contentSize.width, actionsSize.width)
contentSize.height += actionsSize.height
transition.updateFrame(node: self.actionsNode, frame: bounds)
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
if let textSelectionTipNode = self.textSelectionTipNode {
contentSize.height += 8.0
let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsLayout.cleanSize.width, transition: transition)
let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsSize.width, transition: transition)
transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize))
contentSize.height += textSelectionTipSize.height
}
@ -644,8 +597,8 @@ final class ContextActionsContainerNode: ASDisplayNode {
}
func updateSize(containerSize: CGSize, contentSize: CGSize) {
//self.scrollNode.view.contentSize = contentSize
//self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
self.scrollNode.view.contentSize = contentSize
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {

View File

@ -11,13 +11,15 @@ import AccountContext
private let animationDurationFactor: Double = 1.0
public protocol ContextControllerProtocol {
public protocol ContextControllerProtocol: AnyObject {
var useComplexItemsTransitionAnimation: Bool { get set }
var immediateItemsTransitionAnimation: Bool { get set }
func getActionsMinHeight() -> ContextController.ActionsHeight?
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?)
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition)
func pushItems(items: Signal<ContextController.Items, NoError>)
func popItems()
func dismiss(completion: (() -> Void)?)
}
@ -158,7 +160,7 @@ public enum ContextMenuItem {
case separator
}
private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
let sourceWindowFrame = fromView.convert(frame, to: nil)
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
@ -196,6 +198,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private let dismissNode: ASDisplayNode
private let dismissAccessibilityArea: AccessibilityAreaNode
private var presentationNode: ContextControllerPresentationNode?
private var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition?
private let clippingNode: ASDisplayNode
private let scrollNode: ASScrollNode
@ -466,11 +471,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale)
}))
switch source {
case .reference, .extracted:
self.contentReady.set(.single(true))
@ -480,6 +480,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.initializeContent()
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale)
}))
self.dismissAccessibilityArea.activate = { [weak self] in
self?.dimNodeTapped()
return true
@ -523,7 +528,40 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
}
case let .extracted(source):
let takenViewInfo = source.takeView()
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
return self?.getController()
},
requestUpdate: { [weak self] transition in
guard let strongSelf = self else {
return
}
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(
layout: validLayout,
transition: transition,
previousActionsContainerNode: nil
)
}
},
requestDismiss: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.dismissedForCancel?()
strongSelf.beginDismiss(result)
},
requestAnimateOut: { [weak self] result, completion in
guard let strongSelf = self else {
return
}
strongSelf.animateOut(result: result, completion: completion)
},
source: source
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
/*let takenViewInfo = source.takeView()
if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode {
self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace)
@ -553,7 +591,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
takenViewInfo.contentContainingNode.isExtractedToContextPreviewUpdated?(true)
self.originalProjectedContentViewFrame = (convertFrame(takenViewInfo.contentContainingNode.frame, from: parentSupernode.view, to: self.view), convertFrame(takenViewInfo.contentContainingNode.contentRect, from: takenViewInfo.contentContainingNode.view, to: self.view))
}
}*/
case let .controller(source):
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo, let (sourceNode, sourceNodeRect) = transitionInfo.sourceNode() {
@ -574,9 +612,21 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
func animateIn() {
self.gesture?.endPressedAppearance()
self.hapticFeedback.impact()
if let _ = self.presentationNode {
self.didCompleteAnimationIn = true
self.currentPresentationStateTransition = .animateIn
if let validLayout = self.validLayout {
self.updateLayout(
layout: validLayout,
transition: .animated(duration: 0.5, curve: .spring),
previousActionsContainerNode: nil
)
}
return
}
switch self.source {
case .reference:
break
@ -759,6 +809,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.beganAnimatingOut()
if let _ = self.presentationNode {
self.currentPresentationStateTransition = .animateOut(result: initialResult, completion: completion)
if let validLayout = self.validLayout {
self.updateLayout(
layout: validLayout,
transition: .animated(duration: 0.25, curve: .easeInOut),
previousActionsContainerNode: nil
)
}
return
}
var transitionDuration: Double = 0.2
var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
@ -1152,6 +1214,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
if let presentationNode = self.presentationNode {
presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: completion)
return
}
guard let reactionContextNode = self.reactionContextNode else {
self.animateOut(result: .default, completion: completion)
return
@ -1207,6 +1274,16 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
if let presentationNode = self.presentationNode {
presentationNode.replaceItems(items: items, animated: self.didCompleteAnimationIn)
if !self.didSetItemsReady {
self.didSetItemsReady = true
self.itemsReady.set(.single(true))
}
return
}
if let _ = self.currentItems, !self.didCompleteAnimationIn && self.getController()?.immediateItemsTransitionAnimation == true {
return
}
@ -1248,7 +1325,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode)
if let layout = self.validLayout {
self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition)
self.updateLayout(layout: layout, transition: self.didSetItemsReady ? .animated(duration: 0.3, curve: .spring) : .immediate, previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition)
} else {
previousActionsContainerNode.removeFromSupernode()
}
@ -1259,6 +1336,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
func pushItems(items: Signal<ContextController.Items, NoError>) {
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
guard let strongSelf = self, let presentationNode = strongSelf.presentationNode else {
return
}
presentationNode.pushItems(items: items)
}))
}
func popItems() {
if let presentationNode = self.presentationNode {
presentationNode.popItems()
}
}
func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData
@ -1283,6 +1376,20 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.validLayout = layout
let presentationStateTransition = self.currentPresentationStateTransition
self.currentPresentationStateTransition = .none
if let presentationNode = self.presentationNode {
transition.updateFrame(node: presentationNode, frame: CGRect(origin: CGPoint(), size: layout.size))
presentationNode.update(
presentationData: self.presentationData,
layout: layout,
transition: transition,
stateTransition: presentationStateTransition
)
return
}
var actionsContainerTransition = transition
if previousActionsContainerNode != nil {
actionsContainerTransition = .immediate
@ -1340,7 +1447,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, bottomInset: 0.0, transition: actionsContainerTransition)
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition)
let adjustedActionsSize = realActionsSize
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize)
@ -1437,7 +1544,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
constrainedActionsBottomInset = 0.0
}
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: constrainedActionsHeight, bottomInset: constrainedActionsBottomInset, transition: actionsContainerTransition)
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: constrainedActionsHeight, transition: actionsContainerTransition)
let adjustedActionsSize = realActionsSize
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize)
@ -1602,7 +1709,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
constrainedWidth = floor(layout.size.width / 2.0)
}
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, bottomInset: 0.0, transition: actionsContainerTransition)
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition)
let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth
var contentUnscaledSize: CGSize
if case .compact = layout.metrics.widthClass {
@ -1800,6 +1907,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
return nil
}
if let presentationNode = self.presentationNode {
return presentationNode.hitTest(self.view.convert(point, to: presentationNode.view), with: event)
}
if let reactionContextNode = self.reactionContextNode {
if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) {
return result
@ -1965,11 +2076,16 @@ public enum ContextContentSource {
}
public protocol ContextControllerItemsNode: ASDisplayNode {
func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize)
func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat)
var apparentHeight: CGFloat { get }
}
public protocol ContextControllerItemsContent: AnyObject {
func node() -> ContextControllerItemsNode
func node(
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerItemsNode
}
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol {
@ -2182,6 +2298,20 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
}
public func pushItems(items: Signal<ContextController.Items, NoError>) {
if !self.isNodeLoaded {
return
}
self.controllerNode.pushItems(items: items)
}
public func popItems() {
if !self.isNodeLoaded {
return
}
self.controllerNode.popItems()
}
public func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData
if self.isNodeLoaded {

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 TelegramCore
import SwiftSignalKit
import ReactionSelectionNode
final class ContextControllerExtractedPresentationNode: ASDisplayNode {
final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode {
private final class ContentNode: ASDisplayNode {
let offsetContainerNode: ASDisplayNode
let containingNode: ContextExtractedContentContainingNode
var animateClippingFromContentAreaInScreenSpace: CGRect?
var storedGlobalFrame: CGRect?
init(containingNode: ContextExtractedContentContainingNode) {
self.offsetContainerNode = ASDisplayNode()
self.containingNode = containingNode
super.init()
self.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.containingNode.contentNode)
}
func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.containingNode.contentRect.contains(point) {
return nil
}
return self.view
}
}
private final class AnimatingOutState {
var currentContentScreenFrame: CGRect
init(
currentContentScreenFrame: CGRect
) {
self.currentContentScreenFrame = currentContentScreenFrame
}
}
private let getController: () -> ContextControllerProtocol?
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let requestDismiss: (ContextMenuActionResult) -> Void
private let requestAnimateOut: (ContextMenuActionResult, @escaping () -> Void) -> Void
private let source: ContextExtractedContentSource
private let backgroundNode: NavigationBackgroundNode
private let dismissTapNode: ASDisplayNode
private let clippingNode: ASDisplayNode
private let scrollNode: ASScrollNode
private var reactionContextNode: ReactionContextNode?
private var reactionContextNodeIsAnimatingOut: Bool = false
private var contentNode: ContentNode?
private let contentRectDebugNode: ASDisplayNode
private let actionsStackNode: ContextControllerActionsStackNode
private var animatingOutState: AnimatingOutState?
init(
getController: @escaping () -> ContextControllerProtocol?,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void,
source: ContextExtractedContentSource
) {
self.getController = getController
self.requestUpdate = requestUpdate
self.requestDismiss = requestDismiss
self.requestAnimateOut = requestAnimateOut
self.source = source
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
self.dismissTapNode = ASDisplayNode()
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.scrollNode = ASScrollNode()
self.scrollNode.canCancelAllTouchesInViews = true
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.showsVerticalScrollIndicator = false
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.contentRectDebugNode = ASDisplayNode()
self.contentRectDebugNode.isUserInteractionEnabled = false
self.contentRectDebugNode.backgroundColor = UIColor.red.withAlphaComponent(0.2)
self.actionsStackNode = ContextControllerActionsStackNode(
getController: getController,
requestDismiss: { result in
requestDismiss(result)
},
requestUpdate: requestUpdate
)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.dismissTapNode)
self.scrollNode.addSubnode(self.actionsStackNode)
/*#if DEBUG
self.scrollNode.addSubnode(self.contentRectDebugNode)
#endif*/
self.dismissTapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapGesture(_:))))
}
@objc func dismissTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.requestDismiss(.default)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
if let reactionContextNode = self.reactionContextNode {
if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) {
return result
}
}
return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event)
} else {
return nil
}
}
func replaceItems(items: ContextController.Items, animated: Bool) {
self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated)
}
func pushItems(items: ContextController.Items) {
let positionLock = self.getActionsStackPositionLock()
self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), positionLock: positionLock, animated: true)
}
func popItems() {
self.actionsStackNode.pop()
}
private func getActionsStackPositionLock() -> CGFloat? {
return self.actionsStackNode.frame.minY
}
func update(
presentationData: PresentationData,
layout: ContainerViewLayout,
transition: ContainedViewLayoutTransition,
stateTransition: ContextControllerPresentationNodeStateTransition?
) {
let contentActionsSpacing: CGFloat = 8.0
let actionsSideInset: CGFloat = 6.0
let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0
let bottomInset: CGFloat = 10.0
let contentNode: ContentNode
var contentTransition = transition
self.backgroundNode.updateColor(
color: presentationData.theme.contextMenu.dimColor,
enableBlur: true,
forceKeepBlur: true,
transition: .immediate
)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.backgroundNode.update(size: layout.size, transition: transition)
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) {
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
}
if let current = self.contentNode {
contentNode = current
} else {
guard let takeInfo = self.source.takeView() else {
return
}
contentNode = ContentNode(containingNode: takeInfo.contentContainingNode)
contentNode.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace
self.scrollNode.insertSubnode(contentNode, aboveSubnode: self.actionsStackNode)
self.contentNode = contentNode
contentTransition = .immediate
}
var animateReactionsIn = false
var contentTopInset: CGFloat = topInset
var removedReactionContextNode: ReactionContextNode?
if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty {
if self.reactionContextNode == nil {
let reactionContextNode = ReactionContextNode(context: reactionItems.context, theme: presentationData.theme, items: reactionItems.reactionItems)
self.reactionContextNode = reactionContextNode
self.addSubnode(reactionContextNode)
if transition.isAnimated {
animateReactionsIn = true
}
reactionContextNode.reactionSelected = { [weak self] reaction in
guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else {
return
}
controller.reactionSelected?(reaction)
}
}
contentTopInset += 70.0
} else if let reactionContextNode = self.reactionContextNode {
self.reactionContextNode = nil
removedReactionContextNode = reactionContextNode
}
switch stateTransition {
case .animateIn, .animateOut:
contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
case .none:
if contentNode.storedGlobalFrame == nil {
contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
}
}
//let contentRectGlobalFrame = contentNode.storedGlobalFrame ?? convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingNode.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingNode.contentRect.height), size: contentNode.containingNode.contentRect.size)
var contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingNode.contentRect.size.height), size: contentNode.containingNode.contentRect.size)
if case .animateOut = stateTransition {
contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height
}
var defaultScrollY: CGFloat = 0.0
if self.animatingOutState == nil {
contentNode.update(
presentationData: presentationData,
size: contentNode.containingNode.bounds.size,
transition: contentTransition
)
let actionsConstrainedHeight: CGFloat
if let actionsPositionLock = self.actionsStackNode.topPositionLock {
actionsConstrainedHeight = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - actionsPositionLock
} else {
actionsConstrainedHeight = layout.size.height
}
let actionsSize = self.actionsStackNode.update(
presentationData: presentationData,
constrainedSize: CGSize(width: layout.size.width, height: actionsConstrainedHeight),
transition: transition
)
if case .animateOut = stateTransition {
} else {
if contentRect.minY < contentTopInset {
contentRect.origin.y = contentTopInset
}
var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height))
if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom {
combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height
}
if combinedBounds.minY < contentTopInset {
combinedBounds.origin.y = contentTopInset
}
contentRect.origin.y = combinedBounds.minY
}
if let reactionContextNode = self.reactionContextNode {
var reactionContextNodeTransition = transition
if reactionContextNode.frame.isEmpty {
reactionContextNodeTransition = .immediate
}
reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size))
reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect, transition: reactionContextNodeTransition)
}
if let removedReactionContextNode = removedReactionContextNode {
removedReactionContextNode.animateOut(to: contentRect, animatingOutToReaction: false)
transition.updateAlpha(node: removedReactionContextNode, alpha: 0.0, completion: { [weak removedReactionContextNode] _ in
removedReactionContextNode?.removeFromSupernode()
})
}
transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect)
var actionsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.maxY + contentActionsSpacing), size: actionsSize)
if contentRect.midX < layout.size.width / 2.0 {
actionsFrame.origin.x = contentRect.minX + actionsSideInset - 3.0
} else {
actionsFrame.origin.x = contentRect.maxX - actionsSideInset - actionsSize.width
}
transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame)
contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size))
let contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom
let contentSize = CGSize(width: layout.size.width, height: contentHeight)
if self.scrollNode.view.contentSize != contentSize {
let previousContentOffset = self.scrollNode.view.contentOffset
self.scrollNode.view.contentSize = contentSize
if case .none = stateTransition, transition.isAnimated {
let contentOffset = self.scrollNode.view.contentOffset
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: previousContentOffset.y - contentOffset.y)
}
}
defaultScrollY = contentSize.height - layout.size.height
if defaultScrollY < 0.0 {
defaultScrollY = 0.0
}
self.dismissTapNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: max(contentSize.height, layout.size.height)))
}
switch stateTransition {
case .animateIn:
let duration: Double = 0.42
let springDamping: CGFloat = 104.0
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: defaultScrollY)
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
self.clippingNode.layer.animateFrame(from: animateClippingFromContentAreaInScreenSpace, to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2)
}
let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view)
let animationInContentDistance = currentContentLocalFrame.maxY - currentContentScreenFrame.maxY
contentNode.layer.animateSpring(
from: -animationInContentDistance as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.y",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
self.actionsStackNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05)
self.actionsStackNode.layer.animateSpring(
from: 0.01 as NSNumber,
to: 1.0 as NSNumber,
keyPath: "transform.scale",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: false
)
let actionsSize = self.actionsStackNode.bounds.size
let actionsPositionDeltaXDistance: CGFloat = 0.0
let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing
self.actionsStackNode.layer.animateSpring(
from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)),
to: NSValue(cgPoint: CGPoint()),
keyPath: "position",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
if let reactionContextNode = self.reactionContextNode {
reactionContextNode.animateIn(from: currentContentScreenFrame)
}
contentNode.containingNode.isExtractedToContextPreview = true
contentNode.containingNode.isExtractedToContextPreviewUpdated?(true)
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(true, transition)
contentNode.containingNode.layoutUpdated = { [weak self] _, animation in
guard let strongSelf = self, let _ = strongSelf.contentNode else {
return
}
if let _ = strongSelf.animatingOutState {
/*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view)
if animatingOutState.currentContentScreenFrame != updatedContentScreenFrame {
let offset = CGPoint(
x: updatedContentScreenFrame.minX - animatingOutState.currentContentScreenFrame.minX,
y: updatedContentScreenFrame.minY - animatingOutState.currentContentScreenFrame.minY
)
let _ = offset
//animation.animator.updatePosition(layer: contentNode.layer, position: contentNode.position.offsetBy(dx: offset.x, dy: offset.y), completion: nil)
animatingOutState.currentContentScreenFrame = updatedContentScreenFrame
}*/
} else {
//strongSelf.requestUpdate(animation.transition)
/*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view)
if let storedGlobalFrame = contentNode.storedGlobalFrame {
let offset = CGPoint(
x: updatedContentScreenFrame.minX - storedGlobalFrame.minX,
y: updatedContentScreenFrame.maxY - storedGlobalFrame.maxY
)
if !offset.x.isZero || !offset.y.isZero {
//print("contentNode.frame = \(contentNode.frame)")
//animation.animator.updateBounds(layer: contentNode.layer, bounds: contentNode.layer.bounds.offsetBy(dx: -offset.x, dy: -offset.y), completion: nil)
}
//animatingOutState.currentContentScreenFrame = updatedContentScreenFrame
}*/
}
}
/*
public var updateAbsoluteRect: ((CGRect, CGSize) -> Void)?
public var applyAbsoluteOffset: ((CGPoint, ContainedViewLayoutTransitionCurve, Double) -> Void)?
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
public var layoutUpdated: ((CGSize) -> Void)?
public var updateDistractionFreeMode: ((Bool) -> Void)?
public var requestDismiss: (() -> Void)*/
case let .animateOut(result, completion):
let duration: Double = 0.25
let putBackInfo = self.source.putBack()
if let putBackInfo = putBackInfo {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: putBackInfo.contentAreaInScreenSpace, duration: duration, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, removeOnCompletion: false)
}
let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
self.animatingOutState = AnimatingOutState(
currentContentScreenFrame: currentContentScreenFrame
)
let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view)
let animationInContentDistance: CGFloat
switch result {
case .default, .custom:
animationInContentDistance = currentContentLocalFrame.minY - currentContentScreenFrame.minY
case .dismissWithoutContent:
animationInContentDistance = 0.0
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
}
print("animationInContentDistance: \(animationInContentDistance)")
contentNode.containingNode.willUpdateIsExtractedToContextPreview?(false, transition)
contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: 0.0, dy: -animationInContentDistance)
contentNode.offsetContainerNode.layer.animate(
from: animationInContentDistance as NSNumber,
to: 0.0 as NSNumber,
keyPath: "position.y",
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue,
duration: duration,
delay: 0.0,
additive: true,
completion: { [weak self] _ in
Queue.mainQueue().after(0.2 * UIView.animationDurationFactor(), {
contentNode.containingNode.isExtractedToContextPreview = false
contentNode.containingNode.isExtractedToContextPreviewUpdated?(false)
if let strongSelf = self, let contentNode = strongSelf.contentNode {
contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode)
}
completion()
})
}
)
/*Queue.mainQueue().after((duration + 0.2) * UIView.animationDurationFactor(), { [weak self] in
contentNode.containingNode.isExtractedToContextPreview = false
contentNode.containingNode.isExtractedToContextPreviewUpdated?(false)
if let strongSelf = self, let contentNode = strongSelf.contentNode {
contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode)
}
completion()
})*/
self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
self.actionsStackNode.layer.animate(
from: 1.0 as NSNumber,
to: 0.01 as NSNumber,
keyPath: "transform.scale",
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue,
duration: duration,
delay: 0.0,
removeOnCompletion: false
)
let actionsSize = self.actionsStackNode.bounds.size
let actionsPositionDeltaXDistance: CGFloat = 0.0
let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing
self.actionsStackNode.layer.animate(
from: NSValue(cgPoint: CGPoint()),
to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)),
keyPath: "position",
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue,
duration: duration,
delay: 0.0,
removeOnCompletion: false,
additive: true
)
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
if let reactionContextNode = self.reactionContextNode {
reactionContextNode.animateOut(to: currentContentScreenFrame, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut)
}
case .none:
if animateReactionsIn, let reactionContextNode = self.reactionContextNode {
reactionContextNode.animateIn(from: contentRect)
}
}
}
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
guard let reactionContextNode = self.reactionContextNode else {
self.requestAnimateOut(.default, completion)
return
}
var contentCompleted = false
var reactionCompleted = false
let intermediateCompletion: () -> Void = {
if contentCompleted && reactionCompleted {
completion()
}
}
self.reactionContextNodeIsAnimatingOut = true
reactionContextNode.willAnimateOutToReaction(value: value)
self.requestAnimateOut(.default, {
contentCompleted = true
intermediateCompletion()
})
reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.reactionContextNode?.removeFromSupernode()
strongSelf.reactionContextNode = nil
reactionCompleted = true
intermediateCompletion()
})
}
}

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 pushItems(items: Signal<ContextController.Items, NoError>) {
}
public func popItems() {
}
private var controllerNode: 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 actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, bottomInset: 0.0, transition: .immediate)
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: .immediate)
let containerFrame: CGRect
let actionsFrame: CGRect

View File

@ -7,16 +7,6 @@ import TextSelectionNode
import TelegramCore
import SwiftSignalKit
private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
let sourceWindowFrame = fromView.convert(frame, to: nil)
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
if let fromWindow = fromView.window, let toWindow = toView.window {
targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width
}
return targetWindowFrame
}
final class PinchSourceGesture: UIPinchGestureRecognizer {
private final class Target {
var updated: (() -> Void)?

View File

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

View File

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

View File

@ -2665,6 +2665,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if !abs(updatedApparentHeight - previousApparentHeight).isZero {
let currentAnimation = node.animationForKey("apparentHeight")
if let currentAnimation = currentAnimation, let toFloat = currentAnimation.to as? CGFloat, toFloat.isEqual(to: updatedApparentHeight) {
/*node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in
if let node = node {
node.animateFrameTransition(progress, currentValue)
}
})
node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp)*/
} else {
node.apparentHeight = previousApparentHeight
node.animateFrameTransition(0.0, previousApparentHeight)
@ -2733,7 +2739,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
}
for itemNode in self.itemNodes {
itemNode.beginPendingControlledTransitions(beginAt: timestamp)
itemNode.beginPendingControlledTransitions(beginAt: timestamp, forceRestart: false)
}
if hadInserts, let reorderNode = self.reorderNode, reorderNode.supernode != nil {

View File

@ -481,16 +481,16 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode {
self.pendingControlledTransitions.append(transition)
}
func beginPendingControlledTransitions(beginAt: Double) {
func beginPendingControlledTransitions(beginAt: Double, forceRestart: Bool) {
for transition in self.pendingControlledTransitions {
self.addControlledTransition(transition: transition, beginAt: beginAt)
self.addControlledTransition(transition: transition, beginAt: beginAt, forceRestart: forceRestart)
}
self.pendingControlledTransitions.removeAll()
}
func addControlledTransition(transition: ControlledTransition, beginAt: Double) {
func addControlledTransition(transition: ControlledTransition, beginAt: Double, forceRestart: Bool) {
for controlledTransition in self.controlledTransitions {
transition.merge(with: controlledTransition.transition)
transition.merge(with: controlledTransition.transition, forceRestart: forceRestart)
}
self.controlledTransitions.append(ControlledTransitionContext(transition: transition, beginAt: beginAt))
}

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 transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .linear)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
self.addSubnode(itemNode)
itemNode.frame = selfSourceRect
@ -562,7 +562,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
additionalAnimationNode.visibility = true
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: {
self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: {
mainAnimationCompleted = true
intermediateCompletion()

View File

@ -4,7 +4,6 @@ import SwiftSignalKit
import TelegramApi
import MtProtoKit
public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reaction: String?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction())
@ -59,7 +58,7 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
if messageId.namespace != Namespaces.Message.Cloud {
return .fail(.generic)
}
return network.request(Api.functions.messages.sendReaction(flags: value == nil ? 0 : 1, peer: inputPeer, msgId: messageId.id, reaction: value))
let signal: Signal<Never, RequestUpdateMessageReactionError> = network.request(Api.functions.messages.sendReaction(flags: value == nil ? 0 : 1, peer: inputPeer, msgId: messageId.id, reaction: value))
|> mapError { _ -> RequestUpdateMessageReactionError in
return .generic
}
@ -88,6 +87,11 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st
|> castError(RequestUpdateMessageReactionError.self)
|> ignoreValues
}
#if DEBUG
return signal |> delay(0.1, queue: .mainQueue())
#else
return signal
#endif
}
}
@ -225,3 +229,234 @@ private func synchronizeMessageReactions(transaction: Transaction, postbox: Post
}
}
public extension EngineMessageReactionListContext.State {
init(message: EngineMessage, reaction: String?) {
var totalCount: Int = 0
if let reactionsAttribute = message._asMessage().reactionsAttribute {
for messageReaction in reactionsAttribute.reactions {
if reaction == nil || messageReaction.value == reaction {
totalCount += Int(messageReaction.count)
}
}
}
self.init(
totalCount: totalCount,
items: [],
canLoadMore: true
)
}
}
public final class EngineMessageReactionListContext {
public final class Item: Equatable {
public let peer: EnginePeer
public let reaction: String
init(
peer: EnginePeer,
reaction: String
) {
self.peer = peer
self.reaction = reaction
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.reaction != rhs.reaction {
return false
}
return true
}
}
public struct State: Equatable {
public var totalCount: Int
public var items: [Item]
public var canLoadMore: Bool
public init(
totalCount: Int,
items: [Item],
canLoadMore: Bool
) {
self.totalCount = totalCount
self.items = items
self.canLoadMore = canLoadMore
}
}
private final class Impl {
struct InternalState: Equatable {
var totalCount: Int
var items: [Item]
var canLoadMore: Bool
var nextOffset: String?
}
let queue: Queue
let account: Account
let message: EngineMessage
let reaction: String?
let disposable = MetaDisposable()
var state: InternalState
let statePromise = Promise<InternalState>()
var isLoadingMore: Bool = false
init(queue: Queue, account: Account, message: EngineMessage, reaction: String?) {
self.queue = queue
self.account = account
self.message = message
self.reaction = reaction
let initialState = EngineMessageReactionListContext.State(message: message, reaction: reaction)
self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: true, nextOffset: nil)
self.loadMore()
}
deinit {
assert(self.queue.isCurrent())
self.disposable.dispose()
}
func loadMore() {
if self.isLoadingMore {
return
}
self.isLoadingMore = true
let account = self.account
let message = self.message
let reaction = self.reaction
let currentOffset = self.state.nextOffset
let limit = self.state.items.isEmpty ? 50 : 100
let signal: Signal<InternalState, NoError> = self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(message.id.peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<InternalState, NoError> in
if message.id.namespace != Namespaces.Message.Cloud {
return .single(InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil))
}
guard let inputPeer = inputPeer else {
return .single(InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil))
}
var flags: Int32 = 0
if reaction != nil {
flags |= 1 << 0
}
if currentOffset != nil {
flags |= 1 << 1
}
return account.network.request(Api.functions.messages.getMessageReactionsList(flags: flags, peer: inputPeer, id: message.id.id, reaction: reaction, offset: currentOffset, limit: Int32(limit)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.MessageReactionsList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<InternalState, NoError> in
return account.postbox.transaction { transaction -> InternalState in
switch result {
case let .messageReactionsList(_, count, reactions, users, nextOffset):
var peers: [Peer] = []
var peerPresences: [PeerId: PeerPresence] = [:]
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
if let presence = TelegramUserPresence(apiUser: user) {
peerPresences[telegramUser.id] = presence
}
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
var items: [EngineMessageReactionListContext.Item] = []
for reaction in reactions {
switch reaction {
case let .messageUserReaction(userId, reaction):
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) {
items.append(EngineMessageReactionListContext.Item(peer: EnginePeer(peer), reaction: reaction))
}
}
}
return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset)
case .none:
return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil)
}
}
}
}
self.disposable.set((signal
|> deliverOn(self.queue)).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
var existingPeerIds = Set<EnginePeer.Id>()
for item in strongSelf.state.items {
existingPeerIds.insert(item.peer.id)
}
for item in state.items {
if existingPeerIds.contains(item.peer.id) {
continue
}
existingPeerIds.insert(item.peer.id)
strongSelf.state.items.append(item)
}
if state.canLoadMore {
strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count)
} else {
strongSelf.state.totalCount = strongSelf.state.items.count
}
strongSelf.state.canLoadMore = state.canLoadMore
strongSelf.state.nextOffset = state.nextOffset
strongSelf.isLoadingMore = false
strongSelf.statePromise.set(.single(strongSelf.state))
}))
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.statePromise.get().start(next: { state in
subscriber.putNext(State(
totalCount: state.totalCount,
items: state.items,
canLoadMore: state.canLoadMore
))
}))
}
return disposable
}
}
init(account: Account, message: EngineMessage, reaction: String?) {
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, account: account, message: message, reaction: reaction)
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
}

View File

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

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 {
itemNode.awaitingAppliedReaction = (nil, {
})
controller?.dismiss()
})
}
}
}

View File

@ -543,7 +543,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return transaction.getCombinedPeerReadState(messages[0].id.peerId)
}
let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32), NoError> = combineLatest(
let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?), NoError> = combineLatest(
loadLimits,
loadStickerSaveStatusSignal,
loadResourceStatusSignal,
@ -552,9 +552,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|> take(1),
cachedData,
readState,
ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager)
ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager),
context.engine.stickers.availableReactions()
)
|> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32) in
|> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips, availableReactions -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?) in
let (limitsConfiguration, appConfig) = limitsAndAppConfig
var canEdit = false
if !isAction {
@ -567,12 +568,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
isMessageRead = readState.isOutgoingMessageIndexRead(message.index)
}
return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips)
return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions)
}
return dataSignal
|> deliverOnMainQueue
|> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips -> ContextController.Items in
|> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions -> ContextController.Items in
var actions: [ContextMenuItem] = []
var isPinnedMessages = false
@ -1209,9 +1210,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
controllerInteraction.openPeer(stats.peers[0].id, .default, nil)
})
} else if !stats.peers.isEmpty || reactionCount != 0 {
if reactionCount != 0, !"".isEmpty {
let minHeight = c.getActionsMinHeight()
c.setItems(.single(ContextController.Items(content: .custom(ReactionListContextMenuContent()), tip: nil)), minHeight: minHeight, previousActionsTransition: .slide(forward: true))
if reactionCount != 0 {
c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), back: { [weak c] in
c?.popItems()
}, openPeer: { [weak c] id in
c?.dismiss(completion: {
controllerInteraction.openPeer(id, .default, nil)
})
})), tip: nil)))
} else {
var subActions: [ContextMenuItem] = []
@ -1837,7 +1843,11 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
self.iconNode = ASImageNode()
if let reactionsAttribute = item.message.reactionsAttribute, !reactionsAttribute.reactions.isEmpty {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.actionSheet.primaryTextColor)
} else {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: presentationData.theme.actionSheet.primaryTextColor)
}
self.avatarsNode = AnimatedAvatarSetNode()
self.avatarsContext = AnimatedAvatarSetContext()
@ -2053,7 +2063,16 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
let placeholderAvatarsContent: AnimatedAvatarSetContext.Content
var avatarsPeers: [EnginePeer] = []
if let peers = self.currentStats?.peers {
if let recentPeers = self.item.message.reactionsAttribute?.recentPeers, !recentPeers.isEmpty {
for recentPeer in recentPeers {
if let peer = self.item.message.peers[recentPeer.peerId] {
avatarsPeers.append(EnginePeer(peer))
if avatarsPeers.count == 3 {
break
}
}
}
} else if let peers = self.currentStats?.peers {
for i in 0 ..< min(3, peers.count) {
avatarsPeers.append(peers[i])
}

View File

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

View File

@ -285,7 +285,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode {
var currentContentRect = self.contextSourceNode.contentRect
let contextSourceNode = self.contextSourceNode
self.contextSourceNode.layoutUpdated = { [weak self, weak contextSourceNode] size in
self.contextSourceNode.layoutUpdated = { [weak self, weak contextSourceNode] size, _ in
guard let strongSelf = self, let contextSourceNode = contextSourceNode, strongSelf.contextSourceNode === contextSourceNode else {
return
}