diff --git a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift index bf82d48648..308ffda5af 100644 --- a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift +++ b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift @@ -312,7 +312,7 @@ public final class AnimatedAvatarSetView: UIView { self.unclippedView = UIImageView() self.clippedView = UIImageView() - super.init() + super.init(frame: CGRect()) self.addSubview(self.unclippedView) self.addSubview(self.clippedView) @@ -397,7 +397,7 @@ public final class AnimatedAvatarSetView: UIView { private var contentViews: [AnimatedAvatarSetContext.Content.Item.Key: ContentView] = [:] - public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), customSpacing: CGFloat? = nil, animated: Bool, synchronousLoad: Bool) -> CGSize { + public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), customSpacing: CGFloat? = nil, animation: ListViewItemUpdateAnimation, synchronousLoad: Bool) -> CGSize { var contentWidth: CGFloat = 0.0 let contentHeight: CGFloat = itemSize.height @@ -408,13 +408,6 @@ public final class AnimatedAvatarSetView: UIView { spacing = 10.0 } - let transition: ContainedViewLayoutTransition - if animated { - transition = .animated(duration: 0.2, curve: .easeInOut) - } else { - transition = .immediate - } - var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = [] var index = 0 for i in 0 ..< content.items.count { @@ -427,15 +420,15 @@ public final class AnimatedAvatarSetView: UIView { let itemView: ContentView if let current = self.contentViews[key] { itemView = current - itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: animated) - transition.updateFrame(layer: itemView.layer, frame: itemFrame) + itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: animation.isAnimated) + animation.animator.updateFrame(layer: itemView.layer, frame: itemFrame, completion: nil) } else { itemView = ContentView(context: context, peer: item.peer, placeholderColor: item.placeholderColor, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing) self.addSubview(itemView) self.contentViews[key] = itemView itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: false) itemView.frame = itemFrame - if animated { + if animation.isAnimated { itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) itemView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) } @@ -454,10 +447,14 @@ public final class AnimatedAvatarSetView: UIView { guard let itemView = self.contentViews.removeValue(forKey: key) else { continue } - itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemView] _ in - itemView?.removeFromSuperview() - }) - itemView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + if animation.isAnimated { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemView] _ in + itemView?.removeFromSuperview() + }) + itemView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } else { + itemView.removeFromSuperview() + } } return CGSize(width: contentWidth, height: contentHeight) diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 7cc6946621..f8750eeecb 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -366,7 +366,7 @@ public final class CallListController: TelegramBaseController { } } - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(ContextController.Items(items: items)), gesture: nil) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) self.presentInGlobalOverlay(contextController) } @@ -482,7 +482,7 @@ public final class CallListController: TelegramBaseController { }) }))) - let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(CallListTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture) + let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(CallListTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 27c70dd60f..a2746c9fcc 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -250,10 +250,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) - c.setItems(.single(ContextController.Items(items: updatedItems)), minHeight: nil) + c.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil) }))) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 1bdd92a0a9..00fe2f8e1a 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -840,12 +840,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController case let .groupReference(groupId, _, _, _, _): let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId._asGroup(), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupId._asGroup(), chatListController: strongSelf) |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupId._asGroup(), chatListController: strongSelf) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) case let .peer(_, peer, _, _, _, _, _, _, promoInfo, _, _, _): let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -869,7 +869,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -1096,7 +1096,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }))) } - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode, keepInPlace: keepInPlace)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode, keepInPlace: keepInPlace)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } @@ -2890,7 +2890,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index b66f3235ef..ec56305d9c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -788,7 +788,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return items } - let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items |> map { ContextController.Items(items: $0) }, recognizer: nil, gesture: gesture) + let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture) self.presentInGlobalOverlay?(controller, nil) } @@ -852,7 +852,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay?(contextController, nil) case .instantPage: break diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD index 990820163e..0f1f2ccde5 100644 --- a/submodules/Components/ReactionButtonListComponent/BUILD +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/WebPBinding:WebPBinding", + "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 86c93bbbeb..846ee21db2 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -1,4 +1,5 @@ import Foundation +import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit @@ -8,6 +9,426 @@ import AccountContext import TelegramPresentationData import UIKit import WebPBinding +import AnimatedAvatarSetNode + +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +private final class SimpleLayer: CALayer { + override func action(forKey event: String) -> CAAction? { + return nullAction + } + + override init() { + super.init() + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +fileprivate final class CounterLayer: CALayer { + fileprivate final class Layout { + struct Spec: Equatable { + let clippingHeight: CGFloat + var stringComponents: [String] + var backgroundColor: UInt32 + var foregroundColor: UInt32 + } + + let spec: Spec + let size: CGSize + + let image: UIImage + + init( + spec: Spec, + size: CGSize, + image: UIImage + ) { + self.spec = spec + self.size = size + self.image = image + } + + static func calculate(spec: Spec, previousLayout: Layout?) -> Layout { + let image: UIImage + if let previousLayout = previousLayout, previousLayout.spec == spec { + image = previousLayout.image + } else { + let textColor = UIColor(argb: spec.foregroundColor) + let string = NSAttributedString(string: spec.stringComponents.joined(separator: ""), font: Font.medium(11.0), textColor: textColor) + let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + image = generateImage(CGSize(width: boundingRect.size.width, height: spec.clippingHeight), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + /*context.setFillColor(UIColor(argb: spec.backgroundColor).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + if textColor.alpha < 1.0 { + context.setBlendMode(.copy) + }*/ + context.translateBy(x: 0.0, y: (size.height - boundingRect.size.height) / 2.0) + UIGraphicsPushContext(context) + string.draw(at: CGPoint()) + UIGraphicsPopContext() + })! + } + + return Layout( + spec: spec, + size: image.size, + image: image + ) + } + } + + var layout: Layout? + + override init(layer: Any) { + super.init(layer: layer) + } + + override init() { + super.init() + + self.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func action(forKey event: String) -> CAAction? { + return nullAction + } + + func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { + /*if animation.isAnimated, let previousContents = self.contents { + self.animate(from: previousContents as! CGImage, to: layout.image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) + } else {*/ + self.contents = layout.image.cgImage + //} + + self.layout = layout + } +} + +public final class ReactionButtonAsyncView: UIButton { + fileprivate final class Layout { + struct Spec: Equatable { + var component: ReactionButtonComponent + } + + let spec: Spec + + let backgroundColor: UInt32 + let clippingHeight: CGFloat + let sideInsets: CGFloat + + let imageFrame: CGRect + + let counter: CounterLayer.Layout? + let counterFrame: CGRect? + + let backgroundImage: UIImage + + let size: CGSize + + init( + spec: Spec, + backgroundColor: UInt32, + clippingHeight: CGFloat, + sideInsets: CGFloat, + imageFrame: CGRect, + counter: CounterLayer.Layout?, + counterFrame: CGRect?, + backgroundImage: UIImage, + size: CGSize + ) { + self.spec = spec + self.backgroundColor = backgroundColor + self.clippingHeight = clippingHeight + self.sideInsets = sideInsets + self.imageFrame = imageFrame + self.counter = counter + self.counterFrame = counterFrame + self.backgroundImage = backgroundImage + self.size = size + } + + static func calculate(spec: Spec, currentLayout: Layout?, currentCounter: CounterLayer.Layout?) -> Layout { + let clippingHeight: CGFloat = 22.0 + let sideInsets: CGFloat = 8.0 + let height: CGFloat = 30.0 + let spacing: CGFloat = 4.0 + + let defaultImageSize = CGSize(width: 22.0, height: 22.0) + let imageSize: CGSize + if let file = spec.component.reaction.iconFile { + imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize + } else { + imageSize = defaultImageSize + } + + var counterComponents: [String] = [] + for character in "\(spec.component.count)" { + counterComponents.append(String(character)) + } + + let backgroundColor = spec.component.isSelected ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground + + let imageFrame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize) + + var previousDisplayCounter: String? + if let currentLayout = currentLayout { + if currentLayout.spec.component.avatarPeers.isEmpty { + previousDisplayCounter = "\(spec.component.count)" + } + } + var currentDisplayCounter: String? + if spec.component.avatarPeers.isEmpty { + currentDisplayCounter = "\(spec.component.count)" + } + + let backgroundImage: UIImage + if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter { + backgroundImage = currentLayout.backgroundImage + } else { + backgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + + context.setFillColor(UIColor(argb: backgroundColor).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: height, height: height))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - height, y: 0.0), size: CGSize(width: height, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: height / 2.0, y: 0.0), size: CGSize(width: size.width - height, height: size.height))) + + context.setBlendMode(.normal) + + if let currentDisplayCounter = currentDisplayCounter { + let textColor = UIColor(argb: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground) + let string = NSAttributedString(string: currentDisplayCounter, font: Font.medium(11.0), textColor: textColor) + let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + if textColor.alpha < 1.0 { + context.setBlendMode(.copy) + } + string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0)) + } + + UIGraphicsPopContext() + })!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0)) + } + + var counter: CounterLayer.Layout? + var counterFrame: CGRect? + + var size = CGSize(width: imageSize.width + sideInsets * 2.0, height: height) + if !spec.component.avatarPeers.isEmpty { + size.width += 4.0 + 24.0 + if spec.component.avatarPeers.count > 1 { + size.width += CGFloat(spec.component.avatarPeers.count - 1) * 12.0 + } else { + size.width -= 2.0 + } + } else { + let counterSpec = CounterLayer.Layout.Spec( + clippingHeight: clippingHeight, + stringComponents: counterComponents, + backgroundColor: backgroundColor, + foregroundColor: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground + ) + let counterValue: CounterLayer.Layout + if let currentCounter = currentCounter, currentCounter.spec == counterSpec { + counterValue = currentCounter + } else { + counterValue = CounterLayer.Layout.calculate( + spec: counterSpec, + previousLayout: currentCounter + ) + } + counter = counterValue + size.width += spacing + counterValue.size.width + counterFrame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - counterValue.size.height) / 2.0)), size: counterValue.size) + } + + return Layout( + spec: spec, + backgroundColor: backgroundColor, + clippingHeight: clippingHeight, + sideInsets: sideInsets, + imageFrame: imageFrame, + counter: counter, + counterFrame: counterFrame, + backgroundImage: backgroundImage, + size: size + ) + } + } + + private var layout: Layout? + + public let iconView: UIImageView + private var counterLayer: CounterLayer? + private var avatarsView: AnimatedAvatarSetView? + + private let iconImageDisposable = MetaDisposable() + + override init(frame: CGRect) { + self.iconView = UIImageView() + self.iconView.isUserInteractionEnabled = false + + super.init(frame: CGRect()) + + self.addSubview(self.iconView) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + deinit { + self.iconImageDisposable.dispose() + } + + @objc private func pressed() { + guard let layout = self.layout else { + return + } + layout.spec.component.action(layout.spec.component.reaction.value) + } + + fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { + let backgroundCapInsets = layout.backgroundImage.capInsets + if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { + self.layer.contentsScale = layout.backgroundImage.scale + self.layer.contents = layout.backgroundImage.cgImage + } else { + ASDisplayNodeSetResizableContents(self.layer, layout.backgroundImage) + } + + animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil) + + if self.layout?.spec.component.reaction != layout.spec.component.reaction { + if let file = layout.spec.component.reaction.iconFile { + self.iconImageDisposable.set((layout.spec.component.context.account.postbox.mediaBox.resourceData(file.resource) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + + if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = WebP.convert(fromWebP: dataValue) { + strongSelf.iconView.image = image + } + } + })) + } + } + + if let counter = layout.counter, let counterFrame = layout.counterFrame { + let counterLayer: CounterLayer + var counterAnimation = animation + if let current = self.counterLayer { + counterLayer = current + } else { + counterAnimation = .None + counterLayer = CounterLayer() + self.counterLayer = counterLayer + //self.layer.addSublayer(counterLayer) + if animation.isAnimated { + counterLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + counterAnimation.animator.updateFrame(layer: counterLayer, frame: counterFrame, completion: nil) + counterLayer.apply(layout: counter, animation: counterAnimation) + } else if let counterLayer = self.counterLayer { + self.counterLayer = nil + if animation.isAnimated { + animation.animator.updateAlpha(layer: counterLayer, alpha: 0.0, completion: { [weak counterLayer] _ in + counterLayer?.removeFromSuperlayer() + }) + } else { + counterLayer.removeFromSuperlayer() + } + } + + if !layout.spec.component.avatarPeers.isEmpty { + let avatarsView: AnimatedAvatarSetView + if let current = self.avatarsView { + avatarsView = current + } else { + avatarsView = AnimatedAvatarSetView() + avatarsView.isUserInteractionEnabled = false + self.avatarsView = avatarsView + self.addSubview(avatarsView) + } + let content = AnimatedAvatarSetContext().update(peers: layout.spec.component.avatarPeers, animated: false) + let avatarsSize = avatarsView.update( + context: layout.spec.component.context, + content: content, + itemSize: CGSize(width: 24.0, height: 24.0), + customSpacing: 10.0, + animation: animation, + synchronousLoad: false + ) + animation.animator.updateFrame(layer: avatarsView.layer, frame: CGRect(origin: CGPoint(x: layout.imageFrame.maxX + 4.0, y: floorToScreenPixels((layout.size.height - avatarsSize.height) / 2.0)), size: CGSize(width: avatarsSize.width, height: avatarsSize.height)), completion: nil) + } else if let avatarsView = self.avatarsView { + self.avatarsView = nil + if animation.isAnimated { + animation.animator.updateAlpha(layer: avatarsView.layer, alpha: 0.0, completion: { [weak avatarsView] _ in + avatarsView?.removeFromSuperview() + }) + animation.animator.updateScale(layer: avatarsView.layer, scale: 0.01, completion: nil) + } else { + avatarsView.removeFromSuperview() + } + } + + self.layout = layout + } + + public static func asyncLayout(_ view: ReactionButtonAsyncView?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView) { + let currentLayout = view?.layout + + return { component in + let spec = Layout.Spec(component: component) + + let layout: Layout + if let currentLayout = currentLayout, currentLayout.spec == spec { + layout = currentLayout + } else { + layout = Layout.calculate(spec: spec, currentLayout: currentLayout, currentCounter: currentLayout?.counter) + } + + return (size: layout.size, apply: { animation in + var animation = animation + let updatedView: ReactionButtonAsyncView + if let view = view { + updatedView = view + } else { + updatedView = ReactionButtonAsyncView() + animation = .None + } + + updatedView.apply(layout: layout, animation: animation) + + return updatedView + }) + } + } +} public final class ReactionButtonComponent: Component { public struct ViewTag: Equatable { @@ -60,6 +481,7 @@ public final class ReactionButtonComponent: Component { public let context: AccountContext public let colors: Colors public let reaction: Reaction + public let avatarPeers: [EnginePeer] public let count: Int public let isSelected: Bool public let action: (String) -> Void @@ -68,6 +490,7 @@ public final class ReactionButtonComponent: Component { context: AccountContext, colors: Colors, reaction: Reaction, + avatarPeers: [EnginePeer], count: Int, isSelected: Bool, action: @escaping (String) -> Void @@ -75,6 +498,7 @@ public final class ReactionButtonComponent: Component { self.context = context self.colors = colors self.reaction = reaction + self.avatarPeers = avatarPeers self.count = count self.isSelected = isSelected self.action = action @@ -90,6 +514,9 @@ public final class ReactionButtonComponent: Component { if lhs.reaction != rhs.reaction { return false } + if lhs.avatarPeers != rhs.avatarPeers { + return false + } if lhs.count != rhs.count { return false } @@ -247,19 +674,136 @@ public final class ReactionButtonComponent: Component { } } +public final class ReactionButtonsAsyncLayoutContainer { + public struct Result { + public struct Item { + public var size: CGSize + } + + public var items: [Item] + public var apply: (ListViewItemUpdateAnimation) -> ApplyResult + } + + public struct ApplyResult { + public struct Item { + public var value: String + public var view: ReactionButtonAsyncView + public var size: CGSize + } + + public var items: [Item] + public var removedViews: [ReactionButtonAsyncView] + } + + public private(set) var buttons: [String: ReactionButtonAsyncView] = [:] + + public init() { + } + + public func update( + context: AccountContext, + action: @escaping (String) -> Void, + reactions: [ReactionButtonsLayoutContainer.Reaction], + colors: ReactionButtonComponent.Colors, + constrainedWidth: CGFloat + ) -> Result { + var items: [Result.Item] = [] + var applyItems: [(key: String, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView)] = [] + + var validIds = Set() + for reaction in reactions.sorted(by: { lhs, rhs in + var lhsCount = lhs.count + if lhs.isSelected { + lhsCount -= 1 + } + var rhsCount = rhs.count + if rhs.isSelected { + rhsCount -= 1 + } + if lhsCount != rhsCount { + return lhsCount > rhsCount + } + return lhs.reaction.value < rhs.reaction.value + }) { + validIds.insert(reaction.reaction.value) + + var avatarPeers = reaction.peers + for i in 0 ..< avatarPeers.count { + if avatarPeers[i].id == context.account.peerId { + let peer = avatarPeers[i] + avatarPeers.remove(at: i) + avatarPeers.insert(peer, at: 0) + break + } + } + + let viewLayout = ReactionButtonAsyncView.asyncLayout(self.buttons[reaction.reaction.value]) + let (size, apply) = viewLayout(ReactionButtonComponent( + context: context, + colors: colors, + reaction: reaction.reaction, + avatarPeers: avatarPeers, + count: reaction.count, + isSelected: reaction.isSelected, + action: action + )) + + items.append(Result.Item( + size: size + )) + applyItems.append((reaction.reaction.value, size, apply)) + } + + var removeIds: [String] = [] + for (id, _) in self.buttons { + if !validIds.contains(id) { + removeIds.append(id) + } + } + var removedViews: [ReactionButtonAsyncView] = [] + for id in removeIds { + if let view = self.buttons.removeValue(forKey: id) { + removedViews.append(view) + } + } + + return Result( + items: items, + apply: { animation in + var items: [ApplyResult.Item] = [] + for (key, size, apply) in applyItems { + let view = apply(animation) + items.append(ApplyResult.Item(value: key, view: view, size: size)) + + if let current = self.buttons[key] { + assert(current === view) + } else { + self.buttons[key] = view + } + } + + return ApplyResult(items: items, removedViews: removedViews) + } + ) + } +} + public final class ReactionButtonsLayoutContainer { public struct Reaction { public var reaction: ReactionButtonComponent.Reaction public var count: Int + public var peers: [EnginePeer] public var isSelected: Bool public init( reaction: ReactionButtonComponent.Reaction, count: Int, + peers: [EnginePeer], isSelected: Bool ) { self.reaction = reaction self.count = count + self.peers = peers self.isSelected = isSelected } } @@ -322,6 +866,7 @@ public final class ReactionButtonsLayoutContainer { context: context, colors: colors, reaction: reaction.reaction, + avatarPeers: reaction.peers, count: reaction.count, isSelected: reaction.isSelected, action: action diff --git a/submodules/Components/ReactionListContextMenuContent/BUILD b/submodules/Components/ReactionListContextMenuContent/BUILD new file mode 100644 index 0000000000..c82e970bbb --- /dev/null +++ b/submodules/Components/ReactionListContextMenuContent/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ReactionListContextMenuContent", + module_name = "ReactionListContextMenuContent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/WebPBinding:WebPBinding", + "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", + "//submodules/ContextUI:ContextUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift new file mode 100644 index 0000000000..bd2d28c56e --- /dev/null +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -0,0 +1,46 @@ +import Foundation +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData +import UIKit +import WebPBinding +import AnimatedAvatarSetNode +import ContextUI + +public final class ReactionListContextMenuContent: ContextControllerItemsContent { + final class ItemsNode: ASDisplayNode, ContextControllerItemsNode { + private let contentNode: ASDisplayNode + + override init() { + self.contentNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.contentNode) + //self.contentNode.backgroundColor = .blue + } + + 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) + } + } + + public init() { + } + + public func node() -> ContextControllerItemsNode { + return ItemsNode() + } +} diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 94b4324e24..60c06c62c2 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -542,7 +542,7 @@ public class ContactsController: ViewController { }) }))) - let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ContactsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture) + let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ContactsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index c049b31264..e0fa6d6573 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -174,7 +174,7 @@ final class ContactsControllerNode: ASDisplayNode { } let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController) |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) contactsController.presentInGlobalOverlay(contextController) } diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index f8108bf7af..1a90096f49 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -41,7 +41,43 @@ private enum ContextItemNode { case separator(ASDisplayNode) } -private final class InnerActionsContainerNode: 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 let blurBackground: Bool private let presentationData: PresentationData private let containerNode: ASDisplayNode @@ -189,7 +225,7 @@ private final class InnerActionsContainerNode: ASDisplayNode { gesture.isEnabled = self.panSelectionGestureEnabled } - func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> CGSize { + func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) { var minActionsWidth: CGFloat = 250.0 if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth { minActionsWidth = minimalWidth @@ -298,7 +334,7 @@ private final class InnerActionsContainerNode: ASDisplayNode { if let effectView = self.effectView { transition.updateFrame(view: effectView, frame: bounds) } - return size + return (size, size) } func updateTheme(presentationData: PresentationData) { @@ -481,9 +517,12 @@ final class ContextActionsContainerNode: ASDisplayNode { private let shadowNode: ASImageNode private let additionalShadowNode: ASImageNode? private let additionalActionsNode: InnerActionsContainerNode? - private let actionsNode: InnerActionsContainerNode + + private let contentContainerNode: ASDisplayNode + + private let actionsNode: ContextInnerActionsContainerNode private let textSelectionTipNode: InnerTextSelectionTipContainerNode? - private let scrollNode: ASScrollNode + //private let scrollNode: ASScrollNode var panSelectionGestureEnabled: Bool = true { didSet { @@ -506,8 +545,13 @@ 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 let firstItem = items.items.first, case let .custom(_, additional) = firstItem, additional { + if case var .list(itemList) = items.content, let firstItem = itemList.first, case let .custom(_, additional) = firstItem, additional { let additionalShadowNode = ASImageNode() additionalShadowNode.displaysAsynchronously = false additionalShadowNode.displayWithoutProcessing = true @@ -517,72 +561,81 @@ final class ContextActionsContainerNode: ASDisplayNode { self.additionalShadowNode = additionalShadowNode self.additionalActionsNode = InnerActionsContainerNode(presentationData: presentationData, items: [firstItem], getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground) - items.items.removeFirst() + itemList.removeFirst() + items.content = .list(itemList) } else { self.additionalShadowNode = nil self.additionalActionsNode = nil } - self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items.items, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground) - if let tip = items.tip { - let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip) - textSelectionTipNode.isUserInteractionEnabled = false - self.textSelectionTipNode = textSelectionTipNode - } else { + switch items.content { + case let .list(itemList): + 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) + textSelectionTipNode.isUserInteractionEnabled = false + self.textSelectionTipNode = textSelectionTipNode + } 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.scrollNode.addSubnode) - self.scrollNode.addSubnode(self.actionsNode) - self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode) - self.addSubnode(self.scrollNode) + self.additionalActionsNode.flatMap(self.contentContainerNode.addSubnode) + self.contentContainerNode.addSubnode(self.actionsNode) + self.textSelectionTipNode.flatMap(self.addSubnode) + self.addSubnode(self.contentContainerNode) } - func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { + func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { var widthClass = widthClass if !self.blurBackground { widthClass = .regular } var contentSize = CGSize() - let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, minimalWidth: nil, transition: transition) + let actionsLayout = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, bottomInset: bottomInset, minimalWidth: nil, transition: transition) if let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode { - let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, constrainedHeight: constrainedHeight, minimalWidth: actionsSize.width, transition: transition) - contentSize = additionalActionsSize + 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 bounds = CGRect(origin: CGPoint(), size: additionalActionsSize) + let bounds = CGRect(origin: CGPoint(), size: additionalActionsLayout.cleanSize) 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: additionalActionsSize)) + transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsLayout.cleanSize)) contentSize.height += 8.0 } - let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsSize) + let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsLayout.visibleSize) transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0)) self.shadowNode.isHidden = widthClass == .compact - contentSize.width = max(contentSize.width, actionsSize.width) - contentSize.height += actionsSize.height + contentSize.width = max(contentSize.width, actionsLayout.cleanSize.width) + contentSize.height += actionsLayout.cleanSize.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: actionsSize.width, transition: transition) + let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, width: actionsLayout.cleanSize.width, transition: transition) transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize)) contentSize.height += textSelectionTipSize.height } @@ -591,8 +644,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? { diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 95fae64665..d7908a2483 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -293,7 +293,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } self.blurBackground = blurBackground - self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(items: []), getController: { [weak controller] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(), getController: { [weak controller] in return controller }, actionSelected: { result in beginDismiss(result) @@ -1216,7 +1216,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let reactionContextNode = self.reactionContextNode { self.reactionContextNode = nil - reactionContextNode.removeFromSupernode() + reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in + reactionContextNode?.removeFromSupernode() + }) } if !items.reactionItems.isEmpty, let context = items.context { @@ -1338,7 +1340,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, transition: actionsContainerTransition) + 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 adjustedActionsSize = realActionsSize self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) @@ -1425,7 +1427,17 @@ 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, transition: actionsContainerTransition) + let constrainedActionsHeight: CGFloat + let constrainedActionsBottomInset: CGFloat + if let currentActionsMinHeight = self.currentActionsMinHeight { + constrainedActionsBottomInset = actionsBottomInset + layout.intrinsicInsets.bottom + constrainedActionsHeight = layout.size.height - currentActionsMinHeight.minY - constrainedActionsBottomInset + } else { + constrainedActionsHeight = layout.size.height + 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 adjustedActionsSize = realActionsSize self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) @@ -1590,7 +1602,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, transition: actionsContainerTransition) + let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, bottomInset: 0.0, transition: actionsContainerTransition) let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth var contentUnscaledSize: CGSize if case .compact = layout.metrics.widthClass { @@ -1952,22 +1964,35 @@ public enum ContextContentSource { case controller(ContextControllerContentSource) } +public protocol ContextControllerItemsNode: ASDisplayNode { + func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, visibleSize: CGSize) +} + +public protocol ContextControllerItemsContent: AnyObject { + func node() -> ContextControllerItemsNode +} + public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol { public struct Items { - public var items: [ContextMenuItem] + public enum Content { + case list([ContextMenuItem]) + case custom(ContextControllerItemsContent) + } + + public var content: Content public var context: AccountContext? public var reactionItems: [ReactionContextItem] public var tip: Tip? - public init(items: [ContextMenuItem], context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], tip: Tip? = nil) { - self.items = items + public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], tip: Tip? = nil) { + self.content = content self.context = context self.reactionItems = reactionItems self.tip = tip } public init() { - self.items = [] + self.content = .list([]) self.context = nil self.reactionItems = [] self.tip = nil diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift new file mode 100644 index 0000000000..177810dce9 --- /dev/null +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -0,0 +1,12 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TextSelectionNode +import TelegramCore +import SwiftSignalKit + +final class ContextControllerExtractedPresentationNode: ASDisplayNode { + +} \ No newline at end of file diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index 8ec2d0dbe3..f9b79671e2 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -73,7 +73,7 @@ final class PeekControllerNode: ViewControllerTracingNode { var feedbackTapImpl: (() -> Void)? var activatedActionImpl: (() -> Void)? var requestLayoutImpl: (() -> Void)? - self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(items: content.menuItems()), getController: { [weak controller] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(content: .list(content.menuItems())), getController: { [weak controller] in return controller }, actionSelected: { result in activatedActionImpl?() @@ -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, transition: .immediate) + 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 containerFrame: CGRect let actionsFrame: CGRect @@ -341,7 +341,7 @@ final class PeekControllerNode: ViewControllerTracingNode { self.contentNodeHasValidLayout = false let previousActionsContainerNode = self.actionsContainerNode - self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: ContextController.Items(items: content.menuItems()), getController: { [weak self] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: ContextController.Items(content: .list(content.menuItems())), getController: { [weak self] in return self?.controller }, actionSelected: { [weak self] result in self?.requestDismiss() diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 5972f08c09..243b453c03 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2359,7 +2359,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) self.isShowingContextMenuPromise.set(true) controller.presentInGlobalOverlay(contextController) @@ -2414,7 +2414,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } - c.setItems(strongSelf.contextMenuSpeedItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuSpeedItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) if let (message, _, _) = strongSelf.contentInfo() { @@ -2532,7 +2532,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { c.dismiss(completion: nil) return } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) return items diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index eb4bb08215..56932c07f2 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -419,7 +419,7 @@ public final class InviteLinkInviteController: ViewController { }) }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }, copyLink: { [weak self] invite in UIPasteboard.general.string = invite.link diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 1b41a5d81f..3ef8645c8a 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -584,7 +584,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, createLink: { let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, completion: { invite in @@ -783,7 +783,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, openAdmin: { admin in let controller = inviteLinkListController(context: context, peerId: peerId, admin: admin) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index eb85dc41e0..e51a053dde 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -647,7 +647,7 @@ public final class InviteLinkViewController: ViewController { }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 40621b28db..ac1a55704c 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -268,7 +268,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio // dismissPromise.set(true) // } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }) }) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift b/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift index dadde64128..2b86040633 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift @@ -456,7 +456,7 @@ public final class InviteRequestsSearchContainerNode: SearchDisplayControllerCon // dismissPromise.set(true) // } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlay(contextController) }) }) diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 263c141bb6..3031968d2a 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -1139,7 +1139,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta }) }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, manageInviteLinks: { let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil) diff --git a/submodules/PeerInfoUI/Sources/PeerReportController.swift b/submodules/PeerInfoUI/Sources/PeerReportController.swift index eb537851ff..bd18b1a8cd 100644 --- a/submodules/PeerInfoUI/Sources/PeerReportController.swift +++ b/submodules/PeerInfoUI/Sources/PeerReportController.swift @@ -158,7 +158,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro backAction(c) }))) } - contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) } else { contextController?.dismiss(completion: nil) parent.view.endEditing(true) diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 6996ba315d..079ac3937c 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -494,7 +494,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { chatController.canReadHistory.set(false) let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in presentControllerImpl?(c, nil) - }) |> map { ContextController.Items(items: $0) }, gesture: gesture) + }) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, expandUsers: { expandedPromise.set(true) diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index b30920db79..947ee21720 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -646,7 +646,7 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }, colorContextAction: { isCurrent, reference, accentColor, node, gesture in @@ -883,7 +883,7 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme } } } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 1ac143541c..6d1fa05934 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -680,7 +680,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }, colorContextAction: { isCurrent, reference, accentColor, node, gesture in @@ -917,7 +917,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } } } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 0571023873..9616925a99 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -523,7 +523,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD }) }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } return controller diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 304b18d52c..5b1c3eb82f 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -555,7 +555,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi return } let items: Signal<[ContextMenuItem], NoError> = self.contextMenuSpeedItems() - let contextController = ContextController(account: self.context.account, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) self.presentInGlobalOverlay?(contextController) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 7e425f86a7..ab826cd71b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1791,7 +1791,7 @@ public final class VoiceChatController: ViewController { dismissPromise.set(true) } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) contextController.useComplexItemsTransitionAnimation = true strongSelf.controller?.presentInGlobalOverlay(contextController) }, getPeerVideo: { [weak self] endpointId, position in @@ -2463,7 +2463,7 @@ public final class VoiceChatController: ViewController { private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() if let controller = self.controller { - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) controller.presentInGlobalOverlay(contextController) } } @@ -2492,7 +2492,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuDisplayAsItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuDisplayAsItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) items.append(.separator) break @@ -2525,7 +2525,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuAudioItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuAudioItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) } @@ -2562,7 +2562,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuPermissionItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuPermissionItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) } } @@ -2832,7 +2832,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) return .single(items) } @@ -2927,7 +2927,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) return items } @@ -2973,7 +2973,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) } return .single(items) diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index d7a3d850fa..cbf81ee67a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -46,6 +46,25 @@ extension ReactionsMessageAttribute { } } +public func mergedMessageReactionsAndPeers(message: Message) -> (reactions: [MessageReaction], peers: [(String, EnginePeer)]) { + guard let attribute = mergedMessageReactions(attributes: message.attributes) else { + return ([], []) + } + + var recentPeers = attribute.recentPeers.compactMap { recentPeer -> (String, EnginePeer)? in + if let peer = message.peers[recentPeer.peerId] { + return (recentPeer.value, EnginePeer(peer)) + } else { + return nil + } + } + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { + recentPeers.removeAll() + } + + return (attribute.reactions, recentPeers) +} + public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsMessageAttribute? { var current: ReactionsMessageAttribute? var pending: PendingReactionsMessageAttribute? @@ -59,7 +78,7 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM if let pending = pending { var reactions = current?.reactions ?? [] - let recentPeers = current?.recentPeers ?? [] + var recentPeers = current?.recentPeers ?? [] if let value = pending.value { var found = false for i in 0 ..< reactions.count { @@ -75,6 +94,17 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM reactions.append(MessageReaction(value: value, count: 1, isSelected: true)) } } + if let accountPeerId = pending.accountPeerId { + for i in 0 ..< recentPeers.count { + if recentPeers[i].peerId == accountPeerId { + recentPeers.remove(at: i) + break + } + } + if let value = pending.value { + recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: value, peerId: accountPeerId)) + } + } for i in (0 ..< reactions.count).reversed() { if reactions[i].isSelected, pending.value != reactions[i].value { if reactions[i].count == 1 { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a533578702..67d2ccfa5a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -3240,7 +3240,7 @@ func replayFinalState( added = true updatedReactions = attribute.withUpdatedResults(reactions) - if updatedReactions.reactions == attribute.reactions { + if updatedReactions == attribute { return .skip } attributes[j] = updatedReactions diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 454c3b44b2..81c5c47e0a 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -5,8 +5,8 @@ import TelegramApi import MtProtoKit -public func updateMessageReactionsInteractively(postbox: Postbox, messageId: MessageId, reaction: String?) -> Signal { - return postbox.transaction { transaction -> Void in +public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reaction: String?) -> Signal { + return account.postbox.transaction { transaction -> Void in transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction()) transaction.updateMessage(messageId, update: { currentMessage in var storeForwardInfo: StoreMessageForwardInfo? @@ -20,7 +20,7 @@ public func updateMessageReactionsInteractively(postbox: Postbox, messageId: Mes break loop } } - attributes.append(PendingReactionsMessageAttribute(value: reaction)) + attributes.append(PendingReactionsMessageAttribute(accountPeerId: account.peerId, value: reaction)) return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index f575e7adb0..c416b3257b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -54,6 +54,10 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { public let reactions: [MessageReaction] public let recentPeers: [RecentPeer] + public var associatedPeerIds: [PeerId] { + return self.recentPeers.map(\.peerId) + } + public init(reactions: [MessageReaction], recentPeers: [RecentPeer]) { self.reactions = reactions self.recentPeers = recentPeers @@ -81,17 +85,33 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { } public final class PendingReactionsMessageAttribute: MessageAttribute { + public let accountPeerId: PeerId? public let value: String? - public init(value: String?) { + public var associatedPeerIds: [PeerId] { + if let accountPeerId = self.accountPeerId { + return [accountPeerId] + } else { + return [] + } + } + + public init(accountPeerId: PeerId?, value: String?) { + self.accountPeerId = accountPeerId self.value = value } required public init(decoder: PostboxDecoder) { + self.accountPeerId = decoder.decodeOptionalInt64ForKey("ap").flatMap(PeerId.init) self.value = decoder.decodeOptionalStringForKey("v") } public func encode(_ encoder: PostboxEncoder) { + if let accountPeerId = self.accountPeerId { + encoder.encodeInt64(accountPeerId.toInt64(), forKey: "ap") + } else { + encoder.encodeNil(forKey: "ap") + } if let value = self.value { encoder.encodeString(value, forKey: "v") } else { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index dbecb9e167..d0f2a6d0de 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -250,6 +250,7 @@ swift_library( "//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent", "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", "//submodules/QrCodeUI:QrCodeUI", + "//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6064ac4bdd..6137ced5ba 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -956,9 +956,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ).start(next: { actions, availableReactions, chatTextSelectionTips in var actions = actions - guard let strongSelf = self, !actions.items.isEmpty else { + guard let strongSelf = self else { return } + switch actions.content { + case let .list(itemList): + if itemList.isEmpty { + return + } + case .custom: + break + } var tip: ContextController.Tip? @@ -1062,7 +1070,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start() + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction).start() } strongSelf.forEachController({ controller in @@ -1191,7 +1199,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: updatedReaction).start() + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction).start() } }, activateMessagePinch: { [weak self] sourceNode in guard let strongSelf = self else { @@ -2383,7 +2391,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }))) - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: true)), items: .single(ContextController.Items(items: actions)), recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in if let controller = controller as? TooltipScreen { @@ -2460,7 +2468,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G f(.dismissWithoutContent) }))) - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: topMessage, selectAll: true)), items: .single(ContextController.Items(items: actions)), recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in if let controller = controller as? TooltipScreen { @@ -2891,7 +2899,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }) }, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in @@ -3197,7 +3205,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! @@ -6179,7 +6187,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(items: $0) }) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) contextController.dismissedForCancel = { [weak chatController] in if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds { var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] @@ -6903,7 +6911,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) - contextController.setItems(.single(ContextController.Items(items: contextItems)), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil) } return } else { @@ -6922,7 +6930,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) - contextController.setItems(.single(ContextController.Items(items: contextItems)), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil) return } else { @@ -7657,7 +7665,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }, joinGroupCall: { [weak self] activeCall in guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { @@ -7719,7 +7727,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId), false)) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceNode: node, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceNode: node, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: true, { @@ -11899,7 +11907,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(id: .timestamp(timestamp), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } ) @@ -13439,7 +13447,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if canDisplayContextMenu, let contextController = contextController { - contextController.setItems(.single(ContextController.Items(items: contextItems)), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil) } else { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 420fdfe947..32fa565505 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -23,6 +23,7 @@ import AnimatedAvatarSetNode import AvatarNode import AdUI import TelegramNotices +import ReactionListContextMenuContent private struct MessageContextMenuData { let starStatus: Bool? @@ -357,7 +358,7 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState, func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil) -> Signal { guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { - return .single(ContextController.Items(items: [])) + return .single(ContextController.Items(content: .list([]))) } if messages.count == 1, let _ = messages[0].adAttribute { @@ -428,7 +429,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } - return .single(ContextController.Items(items: actions)) + return .single(ContextController.Items(content: .list(actions))) } var loadStickerSaveStatus: MediaId? @@ -1169,62 +1170,80 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } } + + let canViewStats = canViewReadStats(message: message, isMessageRead: isMessageRead, appConfig: appConfig) + var reactionCount = 0 + for reaction in mergedMessageReactionsAndPeers(message: message).reactions { + reactionCount += Int(reaction.count) + } - if let peer = message.peers[message.id.peerId], canViewReadStats(message: message, isMessageRead: isMessageRead, appConfig: appConfig) { + if let peer = message.peers[message.id.peerId], (canViewStats || reactionCount != 0) { var hasReadReports = false if let channel = peer as? TelegramChannel { if case .group = channel.info { if let cachedData = cachedData as? CachedChannelData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 50 { hasReadReports = true } + } else { + reactionCount = 0 } } else if let group = peer as? TelegramGroup { if group.participantCount <= 50 { hasReadReports = true } } + + var readStats = readStats + if !canViewStats { + readStats = MessageReadStats(peers: []) + } - if hasReadReports { + if hasReadReports || reactionCount != 0 { if !actions.isEmpty { actions.insert(.separator, at: 0) } actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats in - if stats.peers.count == 1 { + if reactionCount == 0 && stats.peers.count == 1 { c.dismiss(completion: { controllerInteraction.openPeer(stats.peers[0].id, .default, nil) }) - } else if !stats.peers.isEmpty { - var subActions: [ContextMenuItem] = [] + } else if !stats.peers.isEmpty || reactionCount != 0 { + if reactionCount != 0 { + let minHeight = c.getActionsMinHeight() + c.setItems(.single(ContextController.Items(content: .custom(ReactionListContextMenuContent()), tip: nil)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) + } else { + var subActions: [ContextMenuItem] = [] - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - subActions.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { controller, _ in - controller.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction, readStats: stats), minHeight: nil, previousActionsTransition: .slide(forward: false)) - }))) - - subActions.append(.separator) - - for peer in stats.peers { - let avatarSignal = peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 30.0, height: 30.0)) - - subActions.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 30.0, height: 30.0), signal: avatarSignal), action: { _, f in - c.dismiss(completion: { - controllerInteraction.openPeer(peer.id, .default, nil) - }) + subActions.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, action: { controller, _ in + controller.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction, readStats: stats), minHeight: nil, previousActionsTransition: .slide(forward: false)) }))) - } - var tip: ContextController.Tip? - if messageViewsPrivacyTips < 3 { - tip = .messageViewsPrivacy - let _ = ApplicationSpecificNotice.incrementMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager).start() - } + subActions.append(.separator) - let minHeight = c.getActionsMinHeight() - c.setItems(.single(ContextController.Items(items: subActions, tip: tip)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) + for peer in stats.peers { + let avatarSignal = peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 30.0, height: 30.0)) + + subActions.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 30.0, height: 30.0), signal: avatarSignal), action: { _, f in + c.dismiss(completion: { + controllerInteraction.openPeer(peer.id, .default, nil) + }) + }))) + } + + var tip: ContextController.Tip? + if messageViewsPrivacyTips < 3 { + tip = .messageViewsPrivacy + let _ = ApplicationSpecificNotice.incrementMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager).start() + } + + let minHeight = c.getActionsMinHeight() + c.setItems(.single(ContextController.Items(content: .list(subActions), tip: tip)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) + } } else { f(.default) } @@ -1232,7 +1251,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - return ContextController.Items(items: actions, tip: nil) + return ContextController.Items(content: .list(actions), tip: nil) } } @@ -1849,11 +1868,16 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } } self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + + var reactionCount = 0 + for reaction in mergedMessageReactionsAndPeers(message: self.item.message).reactions { + reactionCount += Int(reaction.count) + } if let currentStats = self.currentStats { - self.buttonNode.isUserInteractionEnabled = !currentStats.peers.isEmpty + self.buttonNode.isUserInteractionEnabled = !currentStats.peers.isEmpty || reactionCount != 0 } else { - self.buttonNode.isUserInteractionEnabled = false + self.buttonNode.isUserInteractionEnabled = reactionCount != 0 self.disposable = (item.context.engine.messages.messageReadStats(id: item.message.id) |> deliverOnMainQueue).start(next: { [weak self] value in @@ -1911,36 +1935,78 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus let calculatedWidth = min(constrainedWidth, 250.0) let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize) + + var reactionCount = 0 + for reaction in mergedMessageReactionsAndPeers(message: self.item.message).reactions { + reactionCount += Int(reaction.count) + } if let currentStats = self.currentStats { if currentStats.peers.isEmpty { - var text = self.presentationData.strings.Conversation_ContextMenuNoViews - for media in self.item.message.media { - if let file = media as? TelegramMediaFile { - if file.isVoice { - text = self.presentationData.strings.Conversation_ContextMenuNobodyListened - } else if file.isInstantVideo { - text = self.presentationData.strings.Conversation_ContextMenuNobodyWatched + if reactionCount != 0 { + //TODO:localize + let text: String + if reactionCount == 1 { + text = "1 reaction" + } else { + text = "\(reactionCount) reactions" + } + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + } else { + var text = self.presentationData.strings.Conversation_ContextMenuNoViews + for media in self.item.message.media { + if let file = media as? TelegramMediaFile { + if file.isVoice { + text = self.presentationData.strings.Conversation_ContextMenuNobodyListened + } else if file.isInstantVideo { + text = self.presentationData.strings.Conversation_ContextMenuNobodyWatched + } } } - } - self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.secondaryColor) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.secondaryColor) + } } else if currentStats.peers.count == 1 { - self.textNode.attributedText = NSAttributedString(string: currentStats.peers[0].displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + if reactionCount != 0 { + //TODO:localize + let text: String + if reactionCount == 1 { + text = "1 reacted" + } else { + text = "\(reactionCount) reacted" + } + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + } else { + self.textNode.attributedText = NSAttributedString(string: currentStats.peers[0].displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + } } else { - var text = self.presentationData.strings.Conversation_ContextMenuSeen(Int32(currentStats.peers.count)) - for media in self.item.message.media { - if let file = media as? TelegramMediaFile { - if file.isVoice { - text = self.presentationData.strings.Conversation_ContextMenuListened(Int32(currentStats.peers.count)) - } else if file.isInstantVideo { - text = self.presentationData.strings.Conversation_ContextMenuWatched(Int32(currentStats.peers.count)) + if reactionCount != 0 { + //TODO:localize + let text: String + if reactionCount >= currentStats.peers.count { + if reactionCount == 1 { + text = "1 reacted" + } else { + text = "\(reactionCount) reacted" + } + } else { + text = "\(reactionCount)/\(currentStats.peers.count) reacted" + } + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + } else { + var text = self.presentationData.strings.Conversation_ContextMenuSeen(Int32(currentStats.peers.count)) + for media in self.item.message.media { + if let file = media as? TelegramMediaFile { + if file.isVoice { + text = self.presentationData.strings.Conversation_ContextMenuListened(Int32(currentStats.peers.count)) + } else if file.isInstantVideo { + text = self.presentationData.strings.Conversation_ContextMenuWatched(Int32(currentStats.peers.count)) + } } } - } - self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + } } } else { self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 0f925b1b93..5524919cbb 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1465,7 +1465,7 @@ final class ChatMediaInputNode: ChatInputNode { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil) }) } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index e9bdaa240c..c443c5136c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -706,6 +706,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let backgroundNode = self.backgroundNode { backgroundNode.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize, transition: .immediate) } + + if let reactionButtonsNode = self.reactionButtonsNode { + var reactionButtonsNodeFrame = reactionButtonsNode.frame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } } } @@ -713,6 +721,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } + + if let reactionButtonsNode = self.reactionButtonsNode { + reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } } override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { @@ -926,7 +938,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let _ = attribute as? EditedMessageAttribute, isEmoji { edited = true @@ -956,7 +968,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring @@ -1098,14 +1111,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? if !reactions.reactions.isEmpty { - let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right + let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left * 2.0 + params.rightInset let maxReactionsWidth = params.width - totalInset let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( context: item.context, presentationData: item.presentationData, + presentationContext: item.controllerInteraction.presentationContext, availableReactions: item.associatedData.availableReactions, reactions: reactions, + message: item.message, isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), constrainedWidth: maxReactionsWidth )) @@ -1400,12 +1415,32 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } reactionButtonsNode.frame = reactionButtonsFrame + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { reactionButtonsNode.animateIn(animation: animation) } } else { animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: animation.transition) + } } } else if let reactionButtonsNode = strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index da3525ce53..339ac262f2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -322,7 +322,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: message) for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -498,7 +498,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { type: statusType, edited: edited, viewCount: viewCount, - dateReactions: dateReactions, + dateReactions: dateReactionsAndPeers.reactions, + dateReactionPeers: dateReactionsAndPeers.peers, dateReplies: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, dateText: dateText @@ -637,7 +638,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), constrainedSize: textConstrainedSize, availableReactions: associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index fac916d2b8..82b407c15e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -1543,7 +1543,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: message) for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -1592,7 +1592,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring @@ -1777,8 +1778,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( context: item.context, presentationData: item.presentationData, + presentationContext: item.controllerInteraction.presentationContext, availableReactions: item.associatedData.availableReactions, reactions: bubbleReactions, + message: item.message, isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), constrainedWidth: maximumNodeWidth )) @@ -2826,8 +2829,30 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if animation.isAnimated { reactionButtonsNode.animateIn(animation: animation) } + + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } } else { animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) + + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: animation.transition) + } } } else if let reactionButtonsNode = strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = nil @@ -3759,6 +3784,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode for contentNode in self.contentNodes { contentNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + contentNode.frame.minX, y: rect.minY + contentNode.frame.minY), size: rect.size), within: containerSize) } + + if let reactionButtonsNode = self.reactionButtonsNode { + var reactionButtonsNodeFrame = reactionButtonsNode.frame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } } override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { @@ -3773,6 +3806,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode for contentNode in self.contentNodes { contentNode.applyAbsoluteOffset(value: value, animationCurve: animationCurve, duration: duration) } + + if let reactionButtonsNode = self.reactionButtonsNode { + reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } } private func applyAbsoluteOffsetSpringInternal(value: CGFloat, duration: Double, damping: CGFloat) { @@ -3781,6 +3818,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode for contentNode in self.contentNodes { contentNode.applyAbsoluteOffsetSpring(value: value, duration: duration, damping: damping) } + + if let reactionButtonsNode = self.reactionButtonsNode { + reactionButtonsNode.offsetSpring(value: value, duration: duration, damping: damping) + } } override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 871d909ba8..8c6be2f6fe 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -163,7 +163,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -213,7 +213,8 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: nil), constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 2c8ac8becc..68d66a57b3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -144,6 +144,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var constrainedSize: CGSize var availableReactions: AvailableReactions? var reactions: [MessageReaction] + var reactionPeers: [(String, EnginePeer)] var replyCount: Int var isPinned: Bool var hasAutoremove: Bool @@ -159,6 +160,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { constrainedSize: CGSize, availableReactions: AvailableReactions?, reactions: [MessageReaction], + reactionPeers: [(String, EnginePeer)], replyCount: Int, isPinned: Bool, hasAutoremove: Bool @@ -173,6 +175,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { self.availableReactions = availableReactions self.constrainedSize = constrainedSize self.reactions = reactions + self.reactionPeers = reactionPeers self.replyCount = replyCount self.isPinned = isPinned self.hasAutoremove = hasAutoremove @@ -188,7 +191,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private let dateNode: TextNode private var impressionIcon: ASImageNode? private var reactionNodes: [String: StatusReactionNode] = [:] - private let reactionButtonsContainer = ReactionButtonsLayoutContainer() + private let reactionButtonsContainer = ReactionButtonsAsyncLayoutContainer() private var reactionCountNode: TextNode? private var reactionButtonNode: HighlightTrackingButtonNode? private var repliesIcon: ASImageNode? @@ -617,14 +620,14 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let resultingWidth: CGFloat let resultingHeight: CGFloat - let reactionButtons: ReactionButtonsLayoutContainer.Result + let reactionButtonsResult: ReactionButtonsAsyncLayoutContainer.Result switch arguments.layoutInput { case .standalone: verticalReactionsInset = 0.0 verticalInset = 0.0 resultingWidth = layoutSize.width resultingHeight = layoutSize.height - reactionButtons = reactionButtonsContainer.update( + reactionButtonsResult = reactionButtonsContainer.update( context: arguments.context, action: { value in guard let strongSelf = self else { @@ -634,12 +637,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { }, reactions: [], colors: reactionColors, - constrainedWidth: arguments.constrainedSize.width, - transition: .immediate + constrainedWidth: arguments.constrainedSize.width ) case let .trailingContent(contentWidth, reactionSettings): if let reactionSettings = reactionSettings, !reactionSettings.displayInline { - reactionButtons = reactionButtonsContainer.update( + reactionButtonsResult = reactionButtonsContainer.update( context: arguments.context, action: { value in guard let strongSelf = self else { @@ -659,21 +661,31 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } + var peers: [EnginePeer] = [] + for (value, peer) in arguments.reactionPeers { + if value == reaction.value { + peers.append(peer) + } + } + if peers.count != Int(reaction.count) { + peers.removeAll() + } + return ReactionButtonsLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile ), count: Int(reaction.count), + peers: peers, isSelected: reaction.isSelected ) }, colors: reactionColors, - constrainedWidth: arguments.constrainedSize.width, - transition: .immediate + constrainedWidth: arguments.constrainedSize.width ) } else { - reactionButtons = reactionButtonsContainer.update( + reactionButtonsResult = reactionButtonsContainer.update( context: arguments.context, action: { value in guard let strongSelf = self else { @@ -683,14 +695,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { }, reactions: [], colors: reactionColors, - constrainedWidth: arguments.constrainedSize.width, - transition: .immediate + constrainedWidth: arguments.constrainedSize.width ) } var reactionButtonsSize = CGSize() var currentRowWidth: CGFloat = 0.0 - for item in reactionButtons.items { + for item in reactionButtonsResult.items { if currentRowWidth + item.size.width > arguments.constrainedSize.width { reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) if !reactionButtonsSize.height.isZero { @@ -705,12 +716,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } currentRowWidth += item.size.width } - if !currentRowWidth.isZero && !reactionButtons.items.isEmpty { + if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty { reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) if !reactionButtonsSize.height.isZero { reactionButtonsSize.height += 6.0 } - reactionButtonsSize.height += reactionButtons.items[0].size.height + reactionButtonsSize.height += reactionButtonsResult.items[0].size.height } if reactionButtonsSize.width.isZero { @@ -758,6 +769,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { strongSelf.type = arguments.type strongSelf.layoutSize = layoutSize + let reactionButtons = reactionButtonsResult.apply(animation) + var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset) for item in reactionButtons.items { if reactionButtonPosition.x + item.size.width > boundingWidth { @@ -1136,9 +1149,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return node.iconView } } - for (_, button) in self.reactionButtonsContainer.buttons { - if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View { - return result.iconView + for (key, button) in self.reactionButtonsContainer.buttons { + if key == value { + return button.iconView } } return nil diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 8b8447208f..4027a3fc32 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -556,8 +556,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( context: item.context, presentationData: item.presentationData, + presentationContext: item.controllerInteraction.presentationContext, availableReactions: item.associatedData.availableReactions, reactions: reactions, + message: item.message, isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), constrainedWidth: maxReactionsWidth )) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 3bde6244d4..87cbc47714 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -430,7 +430,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: topMessage.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: topMessage) for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -458,7 +458,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: !shouldDisplayInlineDateReactions(message: message))), constrainedSize: constrainedSize, availableReactions: associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: isPinned && !associatedData.isInPinnedListMode, hasAutoremove: message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index bf2ada8be6..c033782e6a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -262,7 +262,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let sentViaBot = false var viewCount: Int? = nil var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -299,7 +299,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 386b98b333..ecac91cdca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -69,6 +69,7 @@ struct ChatMessageDateAndStatus { var edited: Bool var viewCount: Int? var dateReactions: [MessageReaction] + var dateReactionPeers: [(String, EnginePeer)] var dateReplies: Int var isPinned: Bool var dateText: String @@ -518,6 +519,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), availableReactions: associatedData.availableReactions, reactions: dateAndStatus.dateReactions, + reactionPeers: dateAndStatus.dateReactionPeers, replyCount: dateAndStatus.dateReplies, isPinned: dateAndStatus.isPinned, hasAutoremove: message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index ee08023fe9..1e0f25f693 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -183,7 +183,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -255,7 +255,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index ec7aa4bac3..5cc7bc50fb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -159,7 +159,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { if case .mosaic = preparePosition { @@ -207,7 +207,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { type: statusType, edited: edited, viewCount: viewCount, - dateReactions: dateReactions, + dateReactions: dateReactionsAndPeers.reactions, + dateReactionPeers: dateReactionsAndPeers.peers, dateReplies: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, dateText: dateText diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index 6c9c6c6238..8363389824 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1024,7 +1024,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -1075,7 +1075,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 95bc1fea51..d3f853a2b8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -11,6 +11,7 @@ import AnimatedCountLabelNode import AnimatedAvatarSetNode import ReactionButtonListComponent import AccountContext +import WallpaperBackgroundNode final class MessageReactionButtonsNode: ASDisplayNode { enum DisplayType { @@ -24,20 +25,29 @@ final class MessageReactionButtonsNode: ASDisplayNode { case right } - private let container: ReactionButtonsLayoutContainer + private var bubbleBackgroundNode: WallpaperBubbleBackgroundNode? + private let container: ReactionButtonsAsyncLayoutContainer + private let backgroundMaskView: UIView + private var backgroundMaskButtons: [String: UIView] = [:] var reactionSelected: ((String) -> Void)? override init() { - self.container = ReactionButtonsLayoutContainer() + self.container = ReactionButtonsAsyncLayoutContainer() + self.backgroundMaskView = UIView() super.init() } + func update() { + } + func prepareUpdate( context: AccountContext, presentationData: ChatPresentationData, + presentationContext: ChatPresentationContext, availableReactions: AvailableReactions?, reactions: ReactionsMessageAttribute, + message: Message, alignment: DisplayAlignment, constrainedWidth: CGFloat, type: DisplayType @@ -61,13 +71,13 @@ final class MessageReactionButtonsNode: ASDisplayNode { case .freeform: reactionColors = ReactionButtonComponent.Colors( deselectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, - selectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, - deselectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb, - selectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb + selectedBackground: UIColor(white: 1.0, alpha: 0.8).argb, + deselectedForeground: UIColor(white: 1.0, alpha: 1.0).argb, + selectedForeground: UIColor(white: 0.0, alpha: 0.1).argb ) } - let reactionButtons = self.container.update( + let reactionButtonsResult = self.container.update( context: context, action: { [weak self] value in guard let strongSelf = self else { @@ -87,23 +97,39 @@ final class MessageReactionButtonsNode: ASDisplayNode { } } + var peers: [EnginePeer] = [] + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { + } else { + for recentPeer in reactions.recentPeers { + if recentPeer.value == reaction.value { + if let peer = message.peers[recentPeer.peerId] { + peers.append(EnginePeer(peer)) + } + } + } + } + + if peers.count != Int(reaction.count) { + peers.removeAll() + } + return ReactionButtonsLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile ), count: Int(reaction.count), + peers: peers, isSelected: reaction.isSelected ) }, colors: reactionColors, - constrainedWidth: constrainedWidth, - transition: .immediate + constrainedWidth: constrainedWidth ) var reactionButtonsSize = CGSize() var currentRowWidth: CGFloat = 0.0 - for item in reactionButtons.items { + for item in reactionButtonsResult.items { if currentRowWidth + item.size.width > constrainedWidth { reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) if !reactionButtonsSize.height.isZero { @@ -118,12 +144,12 @@ final class MessageReactionButtonsNode: ASDisplayNode { } currentRowWidth += item.size.width } - if !currentRowWidth.isZero && !reactionButtons.items.isEmpty { + if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty { reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) if !reactionButtonsSize.height.isZero { reactionButtonsSize.height += 6.0 } - reactionButtonsSize.height += reactionButtons.items[0].size.height + reactionButtonsSize.height += reactionButtonsResult.items[0].size.height } let topInset: CGFloat = 0.0 @@ -136,6 +162,38 @@ final class MessageReactionButtonsNode: ASDisplayNode { return } + let backgroundInsets: CGFloat = 10.0 + + switch type { + case .freeform: + if let backgroundNode = presentationContext.backgroundNode, backgroundNode.hasBubbleBackground(for: .free) { + let bubbleBackgroundFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -backgroundInsets, dy: -backgroundInsets) + if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode { + animation.animator.updateFrame(layer: bubbleBackgroundNode.layer, frame: bubbleBackgroundFrame, completion: nil) + if let (rect, containerSize) = strongSelf.absoluteRect { + bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: animation.transition) + } + } else if strongSelf.bubbleBackgroundNode == nil { + if let bubbleBackgroundNode = backgroundNode.makeBubbleBackground(for: .free) { + strongSelf.bubbleBackgroundNode = bubbleBackgroundNode + bubbleBackgroundNode.view.mask = strongSelf.backgroundMaskView + strongSelf.insertSubnode(bubbleBackgroundNode, at: 0) + bubbleBackgroundNode.frame = bubbleBackgroundFrame + } + } + } else { + if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode { + strongSelf.bubbleBackgroundNode = nil + bubbleBackgroundNode.removeFromSupernode() + } + } + case .incoming, .outgoing: + if let bubbleBackgroundNode = strongSelf.bubbleBackgroundNode { + strongSelf.bubbleBackgroundNode = nil + bubbleBackgroundNode.removeFromSupernode() + } + } + var reactionButtonPosition: CGPoint switch alignment { case .left: @@ -143,7 +201,13 @@ final class MessageReactionButtonsNode: ASDisplayNode { case .right: reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset) } + + let reactionButtons = reactionButtonsResult.apply(animation) + + var validIds = Set() for item in reactionButtons.items { + validIds.insert(item.value) + switch alignment { case .left: if reactionButtonPosition.x + item.size.width > boundingWidth { @@ -166,7 +230,20 @@ final class MessageReactionButtonsNode: ASDisplayNode { itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size) reactionButtonPosition.x -= item.size.width + 6.0 } - + + let itemMaskFrame = itemFrame.offsetBy(dx: backgroundInsets, dy: backgroundInsets) + + let itemMaskView: UIView + if let current = strongSelf.backgroundMaskButtons[item.value] { + itemMaskView = current + } else { + itemMaskView = UIView() + itemMaskView.backgroundColor = .black + itemMaskView.clipsToBounds = true + itemMaskView.layer.cornerRadius = 15.0 + strongSelf.backgroundMaskButtons[item.value] = itemMaskView + } + if item.view.superview == nil { strongSelf.view.addSubview(item.view) if animation.isAnimated { @@ -177,6 +254,35 @@ final class MessageReactionButtonsNode: ASDisplayNode { } else { animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil) } + + if itemMaskView.superview == nil { + strongSelf.backgroundMaskView.addSubview(itemMaskView) + if animation.isAnimated { + itemMaskView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + itemMaskView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + itemMaskView.frame = itemMaskFrame + } else { + animation.animator.updateFrame(layer: itemMaskView.layer, frame: itemMaskFrame, completion: nil) + } + } + + var removeMaskIds: [String] = [] + for (id, view) in strongSelf.backgroundMaskButtons { + if !validIds.contains(id) { + removeMaskIds.append(id) + if animation.isAnimated { + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + for id in removeMaskIds { + strongSelf.backgroundMaskButtons.removeValue(forKey: id) } for view in reactionButtons.removedViews { @@ -186,17 +292,47 @@ final class MessageReactionButtonsNode: ASDisplayNode { view?.removeFromSuperview() }) } else { - view.removeFromSuperview() + view.removeFromSuperview() } } }) }) } + private var absoluteRect: (CGRect, CGSize)? + + func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + self.absoluteRect = (rect, containerSize) + + if let bubbleBackgroundNode = self.bubbleBackgroundNode { + bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: transition) + } + } + + func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { + self.absoluteRect = (rect, containerSize) + + if let bubbleBackgroundNode = self.bubbleBackgroundNode { + bubbleBackgroundNode.update(rect: rect, within: containerSize, transition: transition) + } + } + + func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let bubbleBackgroundNode = self.bubbleBackgroundNode { + bubbleBackgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } + } + + func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + if let bubbleBackgroundNode = self.bubbleBackgroundNode { + bubbleBackgroundNode.offsetSpring(value: value, duration: duration, damping: damping) + } + } + func reactionTargetView(value: String) -> UIView? { - for (_, button) in self.container.buttons { - if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View { - return result.iconView + for (key, button) in self.container.buttons { + if key == value { + return button.iconView } } return nil @@ -258,7 +394,8 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode let buttonsUpdate = buttonsNode.prepareUpdate( context: item.context, presentationData: item.presentationData, - availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, alignment: .left, constrainedWidth: constrainedSize.width, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing) + presentationContext: item.controllerInteraction.presentationContext, + availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, message: item.message, alignment: .left, constrainedWidth: constrainedSize.width, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing) return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in var boundingSize = CGSize() @@ -334,23 +471,29 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { final class Arguments { let context: AccountContext let presentationData: ChatPresentationData + let presentationContext: ChatPresentationContext let availableReactions: AvailableReactions? let reactions: ReactionsMessageAttribute + let message: Message let isIncoming: Bool let constrainedWidth: CGFloat init( context: AccountContext, presentationData: ChatPresentationData, + presentationContext: ChatPresentationContext, availableReactions: AvailableReactions?, reactions: ReactionsMessageAttribute, + message: Message, isIncoming: Bool, constrainedWidth: CGFloat ) { self.context = context self.presentationData = presentationData + self.presentationContext = presentationContext self.availableReactions = availableReactions self.reactions = reactions + self.message = message self.isIncoming = isIncoming self.constrainedWidth = constrainedWidth } @@ -378,8 +521,10 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { let buttonsUpdate = node.buttonsNode.prepareUpdate( context: arguments.context, presentationData: arguments.presentationData, + presentationContext: arguments.presentationContext, availableReactions: arguments.availableReactions, reactions: arguments.reactions, + message: arguments.message, alignment: arguments.isIncoming ? .left : .right, constrainedWidth: arguments.constrainedWidth, type: .freeform @@ -421,4 +566,20 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { } return nil } + + func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + self.buttonsNode.update(rect: rect, within: containerSize, transition: transition) + } + + func update(rect: CGRect, within containerSize: CGSize, transition: CombinedTransition) { + self.buttonsNode.update(rect: rect, within: containerSize, transition: transition) + } + + func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + self.buttonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } + + func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + self.buttonsNode.offsetSpring(value: value, duration: duration, damping: damping) + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index db3defa635..13df9e7b0c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -53,7 +53,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { var viewCount: Int? var rawText = "" var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden @@ -123,7 +123,8 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: false)), constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 0224ea8e4a..856bf74daa 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -253,6 +253,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let backgroundNode = self.backgroundNode { backgroundNode.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize, transition: .immediate) } + + if let reactionButtonsNode = self.reactionButtonsNode { + var reactionButtonsNodeFrame = reactionButtonsNode.frame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } } } @@ -260,6 +268,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } + + if let reactionButtonsNode = self.reactionButtonsNode { + reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } } override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { @@ -465,7 +477,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var edited = false var viewCount: Int? = nil var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let _ = attribute as? EditedMessageAttribute, isEmoji { edited = true @@ -495,7 +507,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: item.message.isSelfExpiring @@ -642,14 +655,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? if !reactions.reactions.isEmpty { - let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right + let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left * 2.0 + params.rightInset let maxReactionsWidth = params.width - totalInset let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( context: item.context, presentationData: item.presentationData, + presentationContext: item.controllerInteraction.presentationContext, availableReactions: item.associatedData.availableReactions, reactions: reactions, + message: item.message, isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), constrainedWidth: maxReactionsWidth )) @@ -978,8 +993,30 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if animation.isAnimated { reactionButtonsNode.animateIn(animation: animation) } + + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } } else { animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) + + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: animation.transition) + } } } else if let reactionButtonsNode = strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 25f9b9c690..afcbf68a04 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -119,7 +119,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -305,7 +305,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { layoutInput: dateLayoutInput, constrainedSize: textConstrainedSize, availableReactions: item.associatedData.availableReactions, - reactions: dateReactions, + reactions: dateReactionsAndPeers.reactions, + reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, hasAutoremove: item.message.isSelfExpiring diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 9887415263..b158091475 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -234,7 +234,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe if let message = messages.first { let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peerId), subject: .message(id: .id(message.id), highlight: true, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(items: [])), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list([]))), gesture: gesture) presentInGlobalOverlay(contextController) } else { gesture?.cancel() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index dd3348be10..f6c28de76b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1826,7 +1826,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } - return ContextController.Items(items: items) + return ContextController.Items(content: .list(items)) }, minHeight: nil) }))) } @@ -1843,7 +1843,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }))) } - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) }, updateMessageReaction: { _, _ in @@ -1963,7 +1963,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } - return ContextController.Items(items: items) + return ContextController.Items(content: .list(items)) }, minHeight: nil) }))) } @@ -1986,7 +1986,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node, sourceRect: rect)), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node, sourceRect: rect)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) case .instantPage: break @@ -2213,7 +2213,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self?.chatInterfaceInteraction.openPeer(peer.id, .default, nil) })) ] - let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) controller.presentInGlobalOverlay(contextController) } @@ -2845,7 +2845,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }, synchronousLoad: true) galleryController.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) } @@ -3491,7 +3491,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.view.endEditing(true) if let sourceNode = self.headerNode.buttonNodes[.mute]?.referenceNode { - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -3717,7 +3717,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }, action: { [weak self] c, f in self?.openReport(user: false, contextController: c, backAction: { c in if let mainItemsImpl = mainItemsImpl { - c.setItems(mainItemsImpl() |> map { ContextController.Items(items: $0) }, minHeight: nil) + c.setItems(mainItemsImpl() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) } }) }))) @@ -3773,7 +3773,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if let sourceNode = self.headerNode.buttonNodes[.more]?.referenceNode { let items = mainItemsImpl?() ?? .single([]) - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(items: $0) }, gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -4434,7 +4434,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }))) if let contextController = contextController { - contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) } else { strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) if let (layout, navigationHeight) = strongSelf.validLayout { @@ -4442,7 +4442,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller { - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -4530,7 +4530,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } if let contextController = contextController { - contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) + contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) } else { strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) if let (layout, navigationHeight) = strongSelf.validLayout { @@ -4538,7 +4538,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller { - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -5620,7 +5620,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let contextController = ContextController(account: accountContext.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node)), items: accountContextMenuItems(context: accountContext, logout: { [weak self] in self?.logoutAccount(id: id) - }) |> map { ContextController.Items(items: $0) }, gesture: gesture) + }) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) self.controller?.presentInGlobalOverlay(contextController) } else { gesture?.cancel() @@ -6108,7 +6108,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }))) } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.passthroughTouchEvent = { sourceView, point in guard let strongSelf = self else { return .ignore @@ -6223,7 +6223,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(id: .id(index.id), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) } ) @@ -7307,7 +7307,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { }))) } - let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture) + let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } }