Temp: reaction improvements

This commit is contained in:
Ali 2021-12-10 19:34:57 +04:00
parent ec675606ee
commit 4a8f28b866
55 changed files with 1383 additions and 251 deletions

View File

@ -312,7 +312,7 @@ public final class AnimatedAvatarSetView: UIView {
self.unclippedView = UIImageView() self.unclippedView = UIImageView()
self.clippedView = UIImageView() self.clippedView = UIImageView()
super.init() super.init(frame: CGRect())
self.addSubview(self.unclippedView) self.addSubview(self.unclippedView)
self.addSubview(self.clippedView) self.addSubview(self.clippedView)
@ -397,7 +397,7 @@ public final class AnimatedAvatarSetView: UIView {
private var contentViews: [AnimatedAvatarSetContext.Content.Item.Key: ContentView] = [:] 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 var contentWidth: CGFloat = 0.0
let contentHeight: CGFloat = itemSize.height let contentHeight: CGFloat = itemSize.height
@ -408,13 +408,6 @@ public final class AnimatedAvatarSetView: UIView {
spacing = 10.0 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 validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
var index = 0 var index = 0
for i in 0 ..< content.items.count { for i in 0 ..< content.items.count {
@ -427,15 +420,15 @@ public final class AnimatedAvatarSetView: UIView {
let itemView: ContentView let itemView: ContentView
if let current = self.contentViews[key] { if let current = self.contentViews[key] {
itemView = current itemView = current
itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: animated) itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: animation.isAnimated)
transition.updateFrame(layer: itemView.layer, frame: itemFrame) animation.animator.updateFrame(layer: itemView.layer, frame: itemFrame, completion: nil)
} else { } else {
itemView = ContentView(context: context, peer: item.peer, placeholderColor: item.placeholderColor, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing) itemView = ContentView(context: context, peer: item.peer, placeholderColor: item.placeholderColor, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing)
self.addSubview(itemView) self.addSubview(itemView)
self.contentViews[key] = itemView self.contentViews[key] = itemView
itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: false) itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
itemView.frame = itemFrame itemView.frame = itemFrame
if animated { if animation.isAnimated {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) itemView.layer.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 { guard let itemView = self.contentViews.removeValue(forKey: key) else {
continue continue
} }
if animation.isAnimated {
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemView] _ in itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemView] _ in
itemView?.removeFromSuperview() itemView?.removeFromSuperview()
}) })
itemView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) itemView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
} else {
itemView.removeFromSuperview()
}
} }
return CGSize(width: contentWidth, height: contentHeight) return CGSize(width: contentWidth, height: contentHeight)

View File

@ -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) 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) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
} }
} }

View File

@ -250,10 +250,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in }, 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)
}))) })))
} }
} }

View File

@ -840,12 +840,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
case let .groupReference(groupId, _, _, _, _): case let .groupReference(groupId, _, _, _, _):
let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId._asGroup(), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId._asGroup(), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false)
chatListController.navigationPresentation = .master 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) strongSelf.presentInGlobalOverlay(contextController)
case let .peer(_, peer, _, _, _, _, _, _, promoInfo, _, _, _): 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)) 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) 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) strongSelf.presentInGlobalOverlay(contextController)
} }
} }
@ -869,7 +869,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) 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) 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) 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) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
}) })
} }

View File

@ -788,7 +788,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
return items 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) self.presentInGlobalOverlay?(controller, nil)
} }
@ -852,7 +852,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
switch previewData { switch previewData {
case let .gallery(gallery): case let .gallery(gallery):
gallery.setHintWillBePresentedInPreviewingContext(true) 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) strongSelf.presentInGlobalOverlay?(contextController, nil)
case .instantPage: case .instantPage:
break break

View File

@ -18,6 +18,7 @@ swift_library(
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/WebPBinding:WebPBinding", "//submodules/WebPBinding:WebPBinding",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1,4 +1,5 @@
import Foundation import Foundation
import AsyncDisplayKit
import Display import Display
import ComponentFlow import ComponentFlow
import SwiftSignalKit import SwiftSignalKit
@ -8,6 +9,426 @@ import AccountContext
import TelegramPresentationData import TelegramPresentationData
import UIKit import UIKit
import WebPBinding 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 final class ReactionButtonComponent: Component {
public struct ViewTag: Equatable { public struct ViewTag: Equatable {
@ -60,6 +481,7 @@ public final class ReactionButtonComponent: Component {
public let context: AccountContext public let context: AccountContext
public let colors: Colors public let colors: Colors
public let reaction: Reaction public let reaction: Reaction
public let avatarPeers: [EnginePeer]
public let count: Int public let count: Int
public let isSelected: Bool public let isSelected: Bool
public let action: (String) -> Void public let action: (String) -> Void
@ -68,6 +490,7 @@ public final class ReactionButtonComponent: Component {
context: AccountContext, context: AccountContext,
colors: Colors, colors: Colors,
reaction: Reaction, reaction: Reaction,
avatarPeers: [EnginePeer],
count: Int, count: Int,
isSelected: Bool, isSelected: Bool,
action: @escaping (String) -> Void action: @escaping (String) -> Void
@ -75,6 +498,7 @@ public final class ReactionButtonComponent: Component {
self.context = context self.context = context
self.colors = colors self.colors = colors
self.reaction = reaction self.reaction = reaction
self.avatarPeers = avatarPeers
self.count = count self.count = count
self.isSelected = isSelected self.isSelected = isSelected
self.action = action self.action = action
@ -90,6 +514,9 @@ public final class ReactionButtonComponent: Component {
if lhs.reaction != rhs.reaction { if lhs.reaction != rhs.reaction {
return false return false
} }
if lhs.avatarPeers != rhs.avatarPeers {
return false
}
if lhs.count != rhs.count { if lhs.count != rhs.count {
return false 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<String>()
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 final class ReactionButtonsLayoutContainer {
public struct Reaction { public struct Reaction {
public var reaction: ReactionButtonComponent.Reaction public var reaction: ReactionButtonComponent.Reaction
public var count: Int public var count: Int
public var peers: [EnginePeer]
public var isSelected: Bool public var isSelected: Bool
public init( public init(
reaction: ReactionButtonComponent.Reaction, reaction: ReactionButtonComponent.Reaction,
count: Int, count: Int,
peers: [EnginePeer],
isSelected: Bool isSelected: Bool
) { ) {
self.reaction = reaction self.reaction = reaction
self.count = count self.count = count
self.peers = peers
self.isSelected = isSelected self.isSelected = isSelected
} }
} }
@ -322,6 +866,7 @@ public final class ReactionButtonsLayoutContainer {
context: context, context: context,
colors: colors, colors: colors,
reaction: reaction.reaction, reaction: reaction.reaction,
avatarPeers: reaction.peers,
count: reaction.count, count: reaction.count,
isSelected: reaction.isSelected, isSelected: reaction.isSelected,
action: action action: action

View File

@ -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",
],
)

View File

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

View File

@ -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) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
} }
} }

View File

@ -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)) 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) 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) contactsController.presentInGlobalOverlay(contextController)
} }

View File

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

View File

@ -293,7 +293,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
} }
self.blurBackground = blurBackground 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 return controller
}, actionSelected: { result in }, actionSelected: { result in
beginDismiss(result) beginDismiss(result)
@ -1216,7 +1216,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if let reactionContextNode = self.reactionContextNode { if let reactionContextNode = self.reactionContextNode {
self.reactionContextNode = nil 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 { 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 isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, 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 let adjustedActionsSize = realActionsSize
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: 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 isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, 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 let adjustedActionsSize = realActionsSize
self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: 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) 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 let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth
var contentUnscaledSize: CGSize var contentUnscaledSize: CGSize
if case .compact = layout.metrics.widthClass { if case .compact = layout.metrics.widthClass {
@ -1952,22 +1964,35 @@ public enum ContextContentSource {
case controller(ContextControllerContentSource) 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 final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol {
public struct Items { 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 context: AccountContext?
public var reactionItems: [ReactionContextItem] public var reactionItems: [ReactionContextItem]
public var tip: Tip? public var tip: Tip?
public init(items: [ContextMenuItem], context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], tip: Tip? = nil) { public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], tip: Tip? = nil) {
self.items = items self.content = content
self.context = context self.context = context
self.reactionItems = reactionItems self.reactionItems = reactionItems
self.tip = tip self.tip = tip
} }
public init() { public init() {
self.items = [] self.content = .list([])
self.context = nil self.context = nil
self.reactionItems = [] self.reactionItems = []
self.tip = nil self.tip = nil

View File

@ -0,0 +1,12 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TextSelectionNode
import TelegramCore
import SwiftSignalKit
final class ContextControllerExtractedPresentationNode: ASDisplayNode {
}

View File

@ -73,7 +73,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
var feedbackTapImpl: (() -> Void)? var feedbackTapImpl: (() -> Void)?
var activatedActionImpl: (() -> Void)? var activatedActionImpl: (() -> Void)?
var requestLayoutImpl: (() -> 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 return controller
}, actionSelected: { result in }, actionSelected: { result in
activatedActionImpl?() activatedActionImpl?()
@ -158,7 +158,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
} }
let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0 let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, 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 containerFrame: CGRect
let actionsFrame: CGRect let actionsFrame: CGRect
@ -341,7 +341,7 @@ final class PeekControllerNode: ViewControllerTracingNode {
self.contentNodeHasValidLayout = false self.contentNodeHasValidLayout = false
let previousActionsContainerNode = self.actionsContainerNode 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 return self?.controller
}, actionSelected: { [weak self] result in }, actionSelected: { [weak self] result in
self?.requestDismiss() self?.requestDismiss()

View File

@ -2359,7 +2359,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return 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) self.isShowingContextMenuPromise.set(true)
controller.presentInGlobalOverlay(contextController) controller.presentInGlobalOverlay(contextController)
@ -2414,7 +2414,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return 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() { if let (message, _, _) = strongSelf.contentInfo() {
@ -2532,7 +2532,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
c.dismiss(completion: nil) c.dismiss(completion: nil)
return 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 return items

View File

@ -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) self?.controller?.presentInGlobalOverlay(contextController)
}, copyLink: { [weak self] invite in }, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link

View File

@ -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) presentInGlobalOverlayImpl?(contextController)
}, createLink: { }, createLink: {
let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, completion: { invite in 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) presentInGlobalOverlayImpl?(contextController)
}, openAdmin: { admin in }, openAdmin: { admin in
let controller = inviteLinkListController(context: context, peerId: peerId, admin: admin) let controller = inviteLinkListController(context: context, peerId: peerId, admin: admin)

View File

@ -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) self?.controller?.presentInGlobalOverlay(contextController)
}) })

View File

@ -268,7 +268,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio
// dismissPromise.set(true) // 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) presentInGlobalOverlayImpl?(contextController)
}) })
}) })

View File

@ -456,7 +456,7 @@ public final class InviteRequestsSearchContainerNode: SearchDisplayControllerCon
// dismissPromise.set(true) // 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) presentInGlobalOverlay(contextController)
}) })
}) })

View File

@ -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) presentInGlobalOverlayImpl?(contextController)
}, manageInviteLinks: { }, manageInviteLinks: {
let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil) let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil)

View File

@ -158,7 +158,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
backAction(c) backAction(c)
}))) })))
} }
contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil)
} else { } else {
contextController?.dismiss(completion: nil) contextController?.dismiss(completion: nil)
parent.view.endEditing(true) parent.view.endEditing(true)

View File

@ -494,7 +494,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
chatController.canReadHistory.set(false) 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 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) presentControllerImpl?(c, nil)
}) |> map { ContextController.Items(items: $0) }, gesture: gesture) }) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
presentInGlobalOverlayImpl?(contextController) presentInGlobalOverlayImpl?(contextController)
}, expandUsers: { }, expandUsers: {
expandedPromise.set(true) expandedPromise.set(true)

View File

@ -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) presentInGlobalOverlayImpl?(contextController, nil)
}) })
}, colorContextAction: { isCurrent, reference, accentColor, node, gesture in }, 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) presentInGlobalOverlayImpl?(contextController, nil)
}) })
}) })

View File

@ -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) presentInGlobalOverlayImpl?(contextController, nil)
}) })
}, colorContextAction: { isCurrent, reference, accentColor, node, gesture in }, 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) presentInGlobalOverlayImpl?(contextController, nil)
}) })
}) })

View File

@ -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) controller.presentInGlobalOverlay(contextController)
} }
return controller return controller

View File

@ -555,7 +555,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
return return
} }
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuSpeedItems() 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) self.presentInGlobalOverlay?(contextController)
} }

View File

@ -1791,7 +1791,7 @@ public final class VoiceChatController: ViewController {
dismissPromise.set(true) 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 contextController.useComplexItemsTransitionAnimation = true
strongSelf.controller?.presentInGlobalOverlay(contextController) strongSelf.controller?.presentInGlobalOverlay(contextController)
}, getPeerVideo: { [weak self] endpointId, position in }, getPeerVideo: { [weak self] endpointId, position in
@ -2463,7 +2463,7 @@ public final class VoiceChatController: ViewController {
private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
if let controller = self.controller { 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) controller.presentInGlobalOverlay(contextController)
} }
} }
@ -2492,7 +2492,7 @@ public final class VoiceChatController: ViewController {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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) items.append(.separator)
break break
@ -2525,7 +2525,7 @@ public final class VoiceChatController: ViewController {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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 { guard let strongSelf = self else {
return 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 { guard let strongSelf = self else {
return 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) return .single(items)
} }
@ -2927,7 +2927,7 @@ public final class VoiceChatController: ViewController {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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 return items
} }
@ -2973,7 +2973,7 @@ public final class VoiceChatController: ViewController {
guard let strongSelf = self else { guard let strongSelf = self else {
return 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) return .single(items)

View File

@ -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? { public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsMessageAttribute? {
var current: ReactionsMessageAttribute? var current: ReactionsMessageAttribute?
var pending: PendingReactionsMessageAttribute? var pending: PendingReactionsMessageAttribute?
@ -59,7 +78,7 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM
if let pending = pending { if let pending = pending {
var reactions = current?.reactions ?? [] var reactions = current?.reactions ?? []
let recentPeers = current?.recentPeers ?? [] var recentPeers = current?.recentPeers ?? []
if let value = pending.value { if let value = pending.value {
var found = false var found = false
for i in 0 ..< reactions.count { 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)) 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() { for i in (0 ..< reactions.count).reversed() {
if reactions[i].isSelected, pending.value != reactions[i].value { if reactions[i].isSelected, pending.value != reactions[i].value {
if reactions[i].count == 1 { if reactions[i].count == 1 {

View File

@ -3240,7 +3240,7 @@ func replayFinalState(
added = true added = true
updatedReactions = attribute.withUpdatedResults(reactions) updatedReactions = attribute.withUpdatedResults(reactions)
if updatedReactions.reactions == attribute.reactions { if updatedReactions == attribute {
return .skip return .skip
} }
attributes[j] = updatedReactions attributes[j] = updatedReactions

View File

@ -5,8 +5,8 @@ import TelegramApi
import MtProtoKit import MtProtoKit
public func updateMessageReactionsInteractively(postbox: Postbox, messageId: MessageId, reaction: String?) -> Signal<Never, NoError> { public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reaction: String?) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Void in return account.postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction()) transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction())
transaction.updateMessage(messageId, update: { currentMessage in transaction.updateMessage(messageId, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo? var storeForwardInfo: StoreMessageForwardInfo?
@ -20,7 +20,7 @@ public func updateMessageReactionsInteractively(postbox: Postbox, messageId: Mes
break loop 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)) 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))
}) })
} }

View File

@ -54,6 +54,10 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute {
public let reactions: [MessageReaction] public let reactions: [MessageReaction]
public let recentPeers: [RecentPeer] public let recentPeers: [RecentPeer]
public var associatedPeerIds: [PeerId] {
return self.recentPeers.map(\.peerId)
}
public init(reactions: [MessageReaction], recentPeers: [RecentPeer]) { public init(reactions: [MessageReaction], recentPeers: [RecentPeer]) {
self.reactions = reactions self.reactions = reactions
self.recentPeers = recentPeers self.recentPeers = recentPeers
@ -81,17 +85,33 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute {
} }
public final class PendingReactionsMessageAttribute: MessageAttribute { public final class PendingReactionsMessageAttribute: MessageAttribute {
public let accountPeerId: PeerId?
public let value: String? 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 self.value = value
} }
required public init(decoder: PostboxDecoder) { required public init(decoder: PostboxDecoder) {
self.accountPeerId = decoder.decodeOptionalInt64ForKey("ap").flatMap(PeerId.init)
self.value = decoder.decodeOptionalStringForKey("v") self.value = decoder.decodeOptionalStringForKey("v")
} }
public func encode(_ encoder: PostboxEncoder) { 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 { if let value = self.value {
encoder.encodeString(value, forKey: "v") encoder.encodeString(value, forKey: "v")
} else { } else {

View File

@ -250,6 +250,7 @@ swift_library(
"//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent", "//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent",
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode", "//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
"//submodules/QrCodeUI:QrCodeUI", "//submodules/QrCodeUI:QrCodeUI",
"//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -956,9 +956,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
).start(next: { actions, availableReactions, chatTextSelectionTips in ).start(next: { actions, availableReactions, chatTextSelectionTips in
var actions = actions var actions = actions
guard let strongSelf = self, !actions.items.isEmpty else { guard let strongSelf = self else {
return return
} }
switch actions.content {
case let .list(itemList):
if itemList.isEmpty {
return
}
case .custom:
break
}
var tip: ContextController.Tip? 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 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 }, activateMessagePinch: { [weak self] sourceNode in
guard let strongSelf = self else { 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.currentContextController = controller
strongSelf.forEachController({ controller in strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen {
@ -2460,7 +2468,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
f(.dismissWithoutContent) 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.currentContextController = controller
strongSelf.forEachController({ controller in strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen {
@ -2891,7 +2899,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return items 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) strongSelf.presentInGlobalOverlay(contextController)
}) })
}, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in }, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in
@ -3197,7 +3205,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return items 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) strongSelf.presentInGlobalOverlay(contextController)
} }
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
@ -6179,7 +6187,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return items 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 contextController.dismissedForCancel = { [weak chatController] in
if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds { if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds {
var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] 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 return
} else { } 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 return
} else { } 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)) 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) 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) strongSelf.presentInGlobalOverlay(contextController)
}, joinGroupCall: { [weak self] activeCall in }, joinGroupCall: { [weak self] activeCall in
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { 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(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false))
items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId), 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 contextController.dismissed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(interactive: true, { 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)) 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) 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) strongSelf.presentInGlobalOverlay(contextController)
} }
) )
@ -13439,7 +13447,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
if canDisplayContextMenu, let contextController = contextController { 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 { } else {
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in

View File

@ -23,6 +23,7 @@ import AnimatedAvatarSetNode
import AvatarNode import AvatarNode
import AdUI import AdUI
import TelegramNotices import TelegramNotices
import ReactionListContextMenuContent
private struct MessageContextMenuData { private struct MessageContextMenuData {
let starStatus: Bool? 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<ContextController.Items, NoError> { func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil) -> Signal<ContextController.Items, NoError> {
guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { 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 { 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? var loadStickerSaveStatus: MediaId?
@ -1170,13 +1171,21 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
} }
if let peer = message.peers[message.id.peerId], canViewReadStats(message: message, isMessageRead: isMessageRead, appConfig: appConfig) { 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], (canViewStats || reactionCount != 0) {
var hasReadReports = false var hasReadReports = false
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
if case .group = channel.info { if case .group = channel.info {
if let cachedData = cachedData as? CachedChannelData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 50 { if let cachedData = cachedData as? CachedChannelData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 50 {
hasReadReports = true hasReadReports = true
} }
} else {
reactionCount = 0
} }
} else if let group = peer as? TelegramGroup { } else if let group = peer as? TelegramGroup {
if group.participantCount <= 50 { if group.participantCount <= 50 {
@ -1184,17 +1193,26 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
} }
if hasReadReports { var readStats = readStats
if !canViewStats {
readStats = MessageReadStats(peers: [])
}
if hasReadReports || reactionCount != 0 {
if !actions.isEmpty { if !actions.isEmpty {
actions.insert(.separator, at: 0) actions.insert(.separator, at: 0)
} }
actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats in 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: { c.dismiss(completion: {
controllerInteraction.openPeer(stats.peers[0].id, .default, nil) controllerInteraction.openPeer(stats.peers[0].id, .default, nil)
}) })
} else if !stats.peers.isEmpty { } 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] = [] var subActions: [ContextMenuItem] = []
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -1224,7 +1242,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
let minHeight = c.getActionsMinHeight() let minHeight = c.getActionsMinHeight()
c.setItems(.single(ContextController.Items(items: subActions, tip: tip)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) c.setItems(.single(ContextController.Items(content: .list(subActions), tip: tip)), minHeight: minHeight, previousActionsTransition: .slide(forward: true))
}
} else { } else {
f(.default) f(.default)
} }
@ -1232,7 +1251,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
} }
return ContextController.Items(items: actions, tip: nil) return ContextController.Items(content: .list(actions), tip: nil)
} }
} }
@ -1850,10 +1869,15 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
} }
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) 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 { if let currentStats = self.currentStats {
self.buttonNode.isUserInteractionEnabled = !currentStats.peers.isEmpty self.buttonNode.isUserInteractionEnabled = !currentStats.peers.isEmpty || reactionCount != 0
} else { } else {
self.buttonNode.isUserInteractionEnabled = false self.buttonNode.isUserInteractionEnabled = reactionCount != 0
self.disposable = (item.context.engine.messages.messageReadStats(id: item.message.id) self.disposable = (item.context.engine.messages.messageReadStats(id: item.message.id)
|> deliverOnMainQueue).start(next: { [weak self] value in |> deliverOnMainQueue).start(next: { [weak self] value in
@ -1912,8 +1936,23 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize) 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 let currentStats = self.currentStats {
if currentStats.peers.isEmpty { if currentStats.peers.isEmpty {
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 var text = self.presentationData.strings.Conversation_ContextMenuNoViews
for media in self.item.message.media { for media in self.item.message.media {
if let file = media as? TelegramMediaFile { if let file = media as? TelegramMediaFile {
@ -1926,8 +1965,34 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
} }
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 { } else if currentStats.peers.count == 1 {
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) 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 {
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 { } else {
var text = self.presentationData.strings.Conversation_ContextMenuSeen(Int32(currentStats.peers.count)) var text = self.presentationData.strings.Conversation_ContextMenuSeen(Int32(currentStats.peers.count))
for media in self.item.message.media { for media in self.item.message.media {
@ -1942,6 +2007,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus
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 { } else {
self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
} }

View File

@ -1465,7 +1465,7 @@ final class ChatMediaInputNode: ChatInputNode {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } 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) strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil)
}) })
} }

View File

@ -706,6 +706,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if let backgroundNode = self.backgroundNode { 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) 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 { if let backgroundNode = self.backgroundNode {
backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) 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) { override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
@ -926,7 +938,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
var edited = false var edited = false
var viewCount: Int? = nil var viewCount: Int? = nil
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let _ = attribute as? EditedMessageAttribute, isEmoji { if let _ = attribute as? EditedMessageAttribute, isEmoji {
edited = true edited = true
@ -956,7 +968,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring
@ -1098,14 +1111,16 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))?
if !reactions.reactions.isEmpty { 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 maxReactionsWidth = params.width - totalInset
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
context: item.context, context: item.context,
presentationData: item.presentationData, presentationData: item.presentationData,
presentationContext: item.controllerInteraction.presentationContext,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: reactions, reactions: reactions,
message: item.message,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth constrainedWidth: maxReactionsWidth
)) ))
@ -1400,12 +1415,32 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
} }
reactionButtonsNode.frame = reactionButtonsFrame 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) strongSelf.addSubnode(reactionButtonsNode)
if animation.isAnimated { if animation.isAnimated {
reactionButtonsNode.animateIn(animation: animation) reactionButtonsNode.animateIn(animation: animation)
} }
} else { } else {
animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) 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 { } else if let reactionButtonsNode = strongSelf.reactionButtonsNode {
strongSelf.reactionButtonsNode = nil strongSelf.reactionButtonsNode = nil

View File

@ -322,7 +322,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: message)
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -498,7 +498,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
type: statusType, type: statusType,
edited: edited, edited: edited,
viewCount: viewCount, viewCount: viewCount,
dateReactions: dateReactions, dateReactions: dateReactionsAndPeers.reactions,
dateReactionPeers: dateReactionsAndPeers.peers,
dateReplies: dateReplies, dateReplies: dateReplies,
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
dateText: dateText 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), layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: associatedData.availableReactions, availableReactions: associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring hasAutoremove: message.isSelfExpiring

View File

@ -1543,7 +1543,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: message)
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -1592,7 +1592,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring hasAutoremove: message.isSelfExpiring
@ -1777,8 +1778,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
context: item.context, context: item.context,
presentationData: item.presentationData, presentationData: item.presentationData,
presentationContext: item.controllerInteraction.presentationContext,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: bubbleReactions, reactions: bubbleReactions,
message: item.message,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maximumNodeWidth constrainedWidth: maximumNodeWidth
)) ))
@ -2826,8 +2829,30 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if animation.isAnimated { if animation.isAnimated {
reactionButtonsNode.animateIn(animation: animation) 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 { } else {
animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) 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 { } else if let reactionButtonsNode = strongSelf.reactionButtonsNode {
strongSelf.reactionButtonsNode = nil strongSelf.reactionButtonsNode = nil
@ -3759,6 +3784,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
for contentNode in self.contentNodes { 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) 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) { override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
@ -3773,6 +3806,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
contentNode.applyAbsoluteOffset(value: value, animationCurve: animationCurve, duration: duration) 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) { private func applyAbsoluteOffsetSpringInternal(value: CGFloat, duration: Double, damping: CGFloat) {
@ -3781,6 +3818,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
contentNode.applyAbsoluteOffsetSpring(value: value, duration: duration, damping: damping) 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? { override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? {

View File

@ -163,7 +163,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -213,7 +213,8 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: nil), layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: nil),
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude), constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring

View File

@ -144,6 +144,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
var constrainedSize: CGSize var constrainedSize: CGSize
var availableReactions: AvailableReactions? var availableReactions: AvailableReactions?
var reactions: [MessageReaction] var reactions: [MessageReaction]
var reactionPeers: [(String, EnginePeer)]
var replyCount: Int var replyCount: Int
var isPinned: Bool var isPinned: Bool
var hasAutoremove: Bool var hasAutoremove: Bool
@ -159,6 +160,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
constrainedSize: CGSize, constrainedSize: CGSize,
availableReactions: AvailableReactions?, availableReactions: AvailableReactions?,
reactions: [MessageReaction], reactions: [MessageReaction],
reactionPeers: [(String, EnginePeer)],
replyCount: Int, replyCount: Int,
isPinned: Bool, isPinned: Bool,
hasAutoremove: Bool hasAutoremove: Bool
@ -173,6 +175,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
self.availableReactions = availableReactions self.availableReactions = availableReactions
self.constrainedSize = constrainedSize self.constrainedSize = constrainedSize
self.reactions = reactions self.reactions = reactions
self.reactionPeers = reactionPeers
self.replyCount = replyCount self.replyCount = replyCount
self.isPinned = isPinned self.isPinned = isPinned
self.hasAutoremove = hasAutoremove self.hasAutoremove = hasAutoremove
@ -188,7 +191,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
private let dateNode: TextNode private let dateNode: TextNode
private var impressionIcon: ASImageNode? private var impressionIcon: ASImageNode?
private var reactionNodes: [String: StatusReactionNode] = [:] private var reactionNodes: [String: StatusReactionNode] = [:]
private let reactionButtonsContainer = ReactionButtonsLayoutContainer() private let reactionButtonsContainer = ReactionButtonsAsyncLayoutContainer()
private var reactionCountNode: TextNode? private var reactionCountNode: TextNode?
private var reactionButtonNode: HighlightTrackingButtonNode? private var reactionButtonNode: HighlightTrackingButtonNode?
private var repliesIcon: ASImageNode? private var repliesIcon: ASImageNode?
@ -617,14 +620,14 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
let resultingWidth: CGFloat let resultingWidth: CGFloat
let resultingHeight: CGFloat let resultingHeight: CGFloat
let reactionButtons: ReactionButtonsLayoutContainer.Result let reactionButtonsResult: ReactionButtonsAsyncLayoutContainer.Result
switch arguments.layoutInput { switch arguments.layoutInput {
case .standalone: case .standalone:
verticalReactionsInset = 0.0 verticalReactionsInset = 0.0
verticalInset = 0.0 verticalInset = 0.0
resultingWidth = layoutSize.width resultingWidth = layoutSize.width
resultingHeight = layoutSize.height resultingHeight = layoutSize.height
reactionButtons = reactionButtonsContainer.update( reactionButtonsResult = reactionButtonsContainer.update(
context: arguments.context, context: arguments.context,
action: { value in action: { value in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -634,12 +637,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
}, },
reactions: [], reactions: [],
colors: reactionColors, colors: reactionColors,
constrainedWidth: arguments.constrainedSize.width, constrainedWidth: arguments.constrainedSize.width
transition: .immediate
) )
case let .trailingContent(contentWidth, reactionSettings): case let .trailingContent(contentWidth, reactionSettings):
if let reactionSettings = reactionSettings, !reactionSettings.displayInline { if let reactionSettings = reactionSettings, !reactionSettings.displayInline {
reactionButtons = reactionButtonsContainer.update( reactionButtonsResult = reactionButtonsContainer.update(
context: arguments.context, context: arguments.context,
action: { value in action: { value in
guard let strongSelf = self else { 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( return ReactionButtonsLayoutContainer.Reaction(
reaction: ReactionButtonComponent.Reaction( reaction: ReactionButtonComponent.Reaction(
value: reaction.value, value: reaction.value,
iconFile: iconFile iconFile: iconFile
), ),
count: Int(reaction.count), count: Int(reaction.count),
peers: peers,
isSelected: reaction.isSelected isSelected: reaction.isSelected
) )
}, },
colors: reactionColors, colors: reactionColors,
constrainedWidth: arguments.constrainedSize.width, constrainedWidth: arguments.constrainedSize.width
transition: .immediate
) )
} else { } else {
reactionButtons = reactionButtonsContainer.update( reactionButtonsResult = reactionButtonsContainer.update(
context: arguments.context, context: arguments.context,
action: { value in action: { value in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -683,14 +695,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
}, },
reactions: [], reactions: [],
colors: reactionColors, colors: reactionColors,
constrainedWidth: arguments.constrainedSize.width, constrainedWidth: arguments.constrainedSize.width
transition: .immediate
) )
} }
var reactionButtonsSize = CGSize() var reactionButtonsSize = CGSize()
var currentRowWidth: CGFloat = 0.0 var currentRowWidth: CGFloat = 0.0
for item in reactionButtons.items { for item in reactionButtonsResult.items {
if currentRowWidth + item.size.width > arguments.constrainedSize.width { if currentRowWidth + item.size.width > arguments.constrainedSize.width {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero { if !reactionButtonsSize.height.isZero {
@ -705,12 +716,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
} }
currentRowWidth += item.size.width currentRowWidth += item.size.width
} }
if !currentRowWidth.isZero && !reactionButtons.items.isEmpty { if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero { if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0 reactionButtonsSize.height += 6.0
} }
reactionButtonsSize.height += reactionButtons.items[0].size.height reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
} }
if reactionButtonsSize.width.isZero { if reactionButtonsSize.width.isZero {
@ -758,6 +769,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
strongSelf.type = arguments.type strongSelf.type = arguments.type
strongSelf.layoutSize = layoutSize strongSelf.layoutSize = layoutSize
let reactionButtons = reactionButtonsResult.apply(animation)
var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset) var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset)
for item in reactionButtons.items { for item in reactionButtons.items {
if reactionButtonPosition.x + item.size.width > boundingWidth { if reactionButtonPosition.x + item.size.width > boundingWidth {
@ -1136,9 +1149,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
return node.iconView return node.iconView
} }
} }
for (_, button) in self.reactionButtonsContainer.buttons { for (key, button) in self.reactionButtonsContainer.buttons {
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View { if key == value {
return result.iconView return button.iconView
} }
} }
return nil return nil

View File

@ -556,8 +556,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
context: item.context, context: item.context,
presentationData: item.presentationData, presentationData: item.presentationData,
presentationContext: item.controllerInteraction.presentationContext,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: reactions, reactions: reactions,
message: item.message,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth constrainedWidth: maxReactionsWidth
)) ))

View File

@ -430,7 +430,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: topMessage.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: topMessage)
for attribute in message.attributes { for attribute in message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden 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))), layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: !shouldDisplayInlineDateReactions(message: message))),
constrainedSize: constrainedSize, constrainedSize: constrainedSize,
availableReactions: associatedData.availableReactions, availableReactions: associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: isPinned && !associatedData.isInPinnedListMode, isPinned: isPinned && !associatedData.isInPinnedListMode,
hasAutoremove: message.isSelfExpiring hasAutoremove: message.isSelfExpiring

View File

@ -262,7 +262,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
let sentViaBot = false let sentViaBot = false
var viewCount: Int? = nil var viewCount: Int? = nil
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -299,7 +299,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring

View File

@ -69,6 +69,7 @@ struct ChatMessageDateAndStatus {
var edited: Bool var edited: Bool
var viewCount: Int? var viewCount: Int?
var dateReactions: [MessageReaction] var dateReactions: [MessageReaction]
var dateReactionPeers: [(String, EnginePeer)]
var dateReplies: Int var dateReplies: Int
var isPinned: Bool var isPinned: Bool
var dateText: String var dateText: String
@ -518,6 +519,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude),
availableReactions: associatedData.availableReactions, availableReactions: associatedData.availableReactions,
reactions: dateAndStatus.dateReactions, reactions: dateAndStatus.dateReactions,
reactionPeers: dateAndStatus.dateReactionPeers,
replyCount: dateAndStatus.dateReplies, replyCount: dateAndStatus.dateReplies,
isPinned: dateAndStatus.isPinned, isPinned: dateAndStatus.isPinned,
hasAutoremove: message.isSelfExpiring hasAutoremove: message.isSelfExpiring

View File

@ -183,7 +183,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -255,7 +255,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring

View File

@ -159,7 +159,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
if case .mosaic = preparePosition { if case .mosaic = preparePosition {
@ -207,7 +207,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
type: statusType, type: statusType,
edited: edited, edited: edited,
viewCount: viewCount, viewCount: viewCount,
dateReactions: dateReactions, dateReactions: dateReactionsAndPeers.reactions,
dateReactionPeers: dateReactionsAndPeers.peers,
dateReplies: dateReplies, dateReplies: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
dateText: dateText dateText: dateText

View File

@ -1024,7 +1024,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -1075,7 +1075,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring

View File

@ -11,6 +11,7 @@ import AnimatedCountLabelNode
import AnimatedAvatarSetNode import AnimatedAvatarSetNode
import ReactionButtonListComponent import ReactionButtonListComponent
import AccountContext import AccountContext
import WallpaperBackgroundNode
final class MessageReactionButtonsNode: ASDisplayNode { final class MessageReactionButtonsNode: ASDisplayNode {
enum DisplayType { enum DisplayType {
@ -24,20 +25,29 @@ final class MessageReactionButtonsNode: ASDisplayNode {
case right 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)? var reactionSelected: ((String) -> Void)?
override init() { override init() {
self.container = ReactionButtonsLayoutContainer() self.container = ReactionButtonsAsyncLayoutContainer()
self.backgroundMaskView = UIView()
super.init() super.init()
} }
func update() {
}
func prepareUpdate( func prepareUpdate(
context: AccountContext, context: AccountContext,
presentationData: ChatPresentationData, presentationData: ChatPresentationData,
presentationContext: ChatPresentationContext,
availableReactions: AvailableReactions?, availableReactions: AvailableReactions?,
reactions: ReactionsMessageAttribute, reactions: ReactionsMessageAttribute,
message: Message,
alignment: DisplayAlignment, alignment: DisplayAlignment,
constrainedWidth: CGFloat, constrainedWidth: CGFloat,
type: DisplayType type: DisplayType
@ -61,13 +71,13 @@ final class MessageReactionButtonsNode: ASDisplayNode {
case .freeform: case .freeform:
reactionColors = ReactionButtonComponent.Colors( reactionColors = ReactionButtonComponent.Colors(
deselectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, deselectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb,
selectedBackground: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, selectedBackground: UIColor(white: 1.0, alpha: 0.8).argb,
deselectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb, deselectedForeground: UIColor(white: 1.0, alpha: 1.0).argb,
selectedForeground: bubbleVariableColor(variableColor: presentationData.theme.theme.chat.message.incoming.actionButtonsTextColor, wallpaper: presentationData.theme.wallpaper).argb selectedForeground: UIColor(white: 0.0, alpha: 0.1).argb
) )
} }
let reactionButtons = self.container.update( let reactionButtonsResult = self.container.update(
context: context, context: context,
action: { [weak self] value in action: { [weak self] value in
guard let strongSelf = self else { 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( return ReactionButtonsLayoutContainer.Reaction(
reaction: ReactionButtonComponent.Reaction( reaction: ReactionButtonComponent.Reaction(
value: reaction.value, value: reaction.value,
iconFile: iconFile iconFile: iconFile
), ),
count: Int(reaction.count), count: Int(reaction.count),
peers: peers,
isSelected: reaction.isSelected isSelected: reaction.isSelected
) )
}, },
colors: reactionColors, colors: reactionColors,
constrainedWidth: constrainedWidth, constrainedWidth: constrainedWidth
transition: .immediate
) )
var reactionButtonsSize = CGSize() var reactionButtonsSize = CGSize()
var currentRowWidth: CGFloat = 0.0 var currentRowWidth: CGFloat = 0.0
for item in reactionButtons.items { for item in reactionButtonsResult.items {
if currentRowWidth + item.size.width > constrainedWidth { if currentRowWidth + item.size.width > constrainedWidth {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero { if !reactionButtonsSize.height.isZero {
@ -118,12 +144,12 @@ final class MessageReactionButtonsNode: ASDisplayNode {
} }
currentRowWidth += item.size.width currentRowWidth += item.size.width
} }
if !currentRowWidth.isZero && !reactionButtons.items.isEmpty { if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero { if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0 reactionButtonsSize.height += 6.0
} }
reactionButtonsSize.height += reactionButtons.items[0].size.height reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
} }
let topInset: CGFloat = 0.0 let topInset: CGFloat = 0.0
@ -136,6 +162,38 @@ final class MessageReactionButtonsNode: ASDisplayNode {
return 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 var reactionButtonPosition: CGPoint
switch alignment { switch alignment {
case .left: case .left:
@ -143,7 +201,13 @@ final class MessageReactionButtonsNode: ASDisplayNode {
case .right: case .right:
reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset) reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset)
} }
let reactionButtons = reactionButtonsResult.apply(animation)
var validIds = Set<String>()
for item in reactionButtons.items { for item in reactionButtons.items {
validIds.insert(item.value)
switch alignment { switch alignment {
case .left: case .left:
if reactionButtonPosition.x + item.size.width > boundingWidth { if reactionButtonPosition.x + item.size.width > boundingWidth {
@ -167,6 +231,19 @@ final class MessageReactionButtonsNode: ASDisplayNode {
reactionButtonPosition.x -= item.size.width + 6.0 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 { if item.view.superview == nil {
strongSelf.view.addSubview(item.view) strongSelf.view.addSubview(item.view)
if animation.isAnimated { if animation.isAnimated {
@ -177,6 +254,35 @@ final class MessageReactionButtonsNode: ASDisplayNode {
} else { } else {
animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil) 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 { for view in reactionButtons.removedViews {
@ -193,10 +299,40 @@ final class MessageReactionButtonsNode: ASDisplayNode {
}) })
} }
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? { func reactionTargetView(value: String) -> UIView? {
for (_, button) in self.container.buttons { for (key, button) in self.container.buttons {
if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View { if key == value {
return result.iconView return button.iconView
} }
} }
return nil return nil
@ -258,7 +394,8 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode
let buttonsUpdate = buttonsNode.prepareUpdate( let buttonsUpdate = buttonsNode.prepareUpdate(
context: item.context, context: item.context,
presentationData: item.presentationData, presentationData: item.presentationData,
availableReactions: item.associatedData.availableReactions, reactions: reactionsAttribute, 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 return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in
var boundingSize = CGSize() var boundingSize = CGSize()
@ -334,23 +471,29 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode {
final class Arguments { final class Arguments {
let context: AccountContext let context: AccountContext
let presentationData: ChatPresentationData let presentationData: ChatPresentationData
let presentationContext: ChatPresentationContext
let availableReactions: AvailableReactions? let availableReactions: AvailableReactions?
let reactions: ReactionsMessageAttribute let reactions: ReactionsMessageAttribute
let message: Message
let isIncoming: Bool let isIncoming: Bool
let constrainedWidth: CGFloat let constrainedWidth: CGFloat
init( init(
context: AccountContext, context: AccountContext,
presentationData: ChatPresentationData, presentationData: ChatPresentationData,
presentationContext: ChatPresentationContext,
availableReactions: AvailableReactions?, availableReactions: AvailableReactions?,
reactions: ReactionsMessageAttribute, reactions: ReactionsMessageAttribute,
message: Message,
isIncoming: Bool, isIncoming: Bool,
constrainedWidth: CGFloat constrainedWidth: CGFloat
) { ) {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.presentationContext = presentationContext
self.availableReactions = availableReactions self.availableReactions = availableReactions
self.reactions = reactions self.reactions = reactions
self.message = message
self.isIncoming = isIncoming self.isIncoming = isIncoming
self.constrainedWidth = constrainedWidth self.constrainedWidth = constrainedWidth
} }
@ -378,8 +521,10 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode {
let buttonsUpdate = node.buttonsNode.prepareUpdate( let buttonsUpdate = node.buttonsNode.prepareUpdate(
context: arguments.context, context: arguments.context,
presentationData: arguments.presentationData, presentationData: arguments.presentationData,
presentationContext: arguments.presentationContext,
availableReactions: arguments.availableReactions, availableReactions: arguments.availableReactions,
reactions: arguments.reactions, reactions: arguments.reactions,
message: arguments.message,
alignment: arguments.isIncoming ? .left : .right, alignment: arguments.isIncoming ? .left : .right,
constrainedWidth: arguments.constrainedWidth, constrainedWidth: arguments.constrainedWidth,
type: .freeform type: .freeform
@ -421,4 +566,20 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode {
} }
return nil 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)
}
} }

View File

@ -53,7 +53,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
var viewCount: Int? var viewCount: Int?
var rawText = "" var rawText = ""
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden edited = !attribute.isHidden
@ -123,7 +123,8 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: false)), layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: false)),
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring

View File

@ -253,6 +253,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
if let backgroundNode = self.backgroundNode { 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) 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 { if let backgroundNode = self.backgroundNode {
backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) 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) { override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
@ -465,7 +477,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
var edited = false var edited = false
var viewCount: Int? = nil var viewCount: Int? = nil
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let _ = attribute as? EditedMessageAttribute, isEmoji { if let _ = attribute as? EditedMessageAttribute, isEmoji {
edited = true edited = true
@ -495,7 +507,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring
@ -642,14 +655,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))?
if !reactions.reactions.isEmpty { 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 maxReactionsWidth = params.width - totalInset
let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments(
context: item.context, context: item.context,
presentationData: item.presentationData, presentationData: item.presentationData,
presentationContext: item.controllerInteraction.presentationContext,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: reactions, reactions: reactions,
message: item.message,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth constrainedWidth: maxReactionsWidth
)) ))
@ -978,8 +993,30 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
if animation.isAnimated { if animation.isAnimated {
reactionButtonsNode.animateIn(animation: animation) 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 { } else {
animation.animator.updateFrame(layer: reactionButtonsNode.layer, frame: reactionButtonsFrame, completion: nil) 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 { } else if let reactionButtonsNode = strongSelf.reactionButtonsNode {
strongSelf.reactionButtonsNode = nil strongSelf.reactionButtonsNode = nil

View File

@ -119,7 +119,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
var viewCount: Int? var viewCount: Int?
var dateReplies = 0 var dateReplies = 0
let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: item.message.attributes)?.reactions ?? [] let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message)
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute { if let attribute = attribute as? EditedMessageAttribute {
@ -305,7 +305,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
layoutInput: dateLayoutInput, layoutInput: dateLayoutInput,
constrainedSize: textConstrainedSize, constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions, availableReactions: item.associatedData.availableReactions,
reactions: dateReactions, reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring hasAutoremove: item.message.isSelfExpiring

View File

@ -234,7 +234,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
if let message = messages.first { 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)) 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) 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) presentInGlobalOverlay(contextController)
} else { } else {
gesture?.cancel() gesture?.cancel()

View File

@ -1826,7 +1826,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
} }
return ContextController.Items(items: items) return ContextController.Items(content: .list(items))
}, minHeight: nil) }, 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) strongSelf.controller?.window?.presentInGlobalOverlay(controller)
}) })
}, updateMessageReaction: { _, _ in }, updateMessageReaction: { _, _ in
@ -1963,7 +1963,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
} }
return ContextController.Items(items: items) return ContextController.Items(content: .list(items))
}, minHeight: nil) }, minHeight: nil)
}))) })))
} }
@ -1986,7 +1986,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
switch previewData { switch previewData {
case let .gallery(gallery): case let .gallery(gallery):
gallery.setHintWillBePresentedInPreviewingContext(true) 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) strongSelf.controller?.presentInGlobalOverlay(contextController)
case .instantPage: case .instantPage:
break break
@ -2213,7 +2213,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self?.chatInterfaceInteraction.openPeer(peer.id, .default, nil) 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) controller.presentInGlobalOverlay(contextController)
} }
@ -2845,7 +2845,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, synchronousLoad: true) }, synchronousLoad: true)
galleryController.setHintWillBePresentedInPreviewingContext(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) strongSelf.controller?.presentInGlobalOverlay(contextController)
} }
@ -3491,7 +3491,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.view.endEditing(true) self.view.endEditing(true)
if let sourceNode = self.headerNode.buttonNodes[.mute]?.referenceNode { 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 contextController.dismissed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.state = strongSelf.state.withHighlightedButton(nil) strongSelf.state = strongSelf.state.withHighlightedButton(nil)
@ -3717,7 +3717,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, action: { [weak self] c, f in }, action: { [weak self] c, f in
self?.openReport(user: false, contextController: c, backAction: { c in self?.openReport(user: false, contextController: c, backAction: { c in
if let mainItemsImpl = mainItemsImpl { 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 { if let sourceNode = self.headerNode.buttonNodes[.more]?.referenceNode {
let items = mainItemsImpl?() ?? .single([]) 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 contextController.dismissed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.state = strongSelf.state.withHighlightedButton(nil) strongSelf.state = strongSelf.state.withHighlightedButton(nil)
@ -4434,7 +4434,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}))) })))
if let contextController = contextController { if let contextController = contextController {
contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil)
} else { } else {
strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat)
if let (layout, navigationHeight) = strongSelf.validLayout { 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 { 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 contextController.dismissed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.state = strongSelf.state.withHighlightedButton(nil) strongSelf.state = strongSelf.state.withHighlightedButton(nil)
@ -4530,7 +4530,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
if let contextController = contextController { if let contextController = contextController {
contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil)
} else { } else {
strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat)
if let (layout, navigationHeight) = strongSelf.validLayout { 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 { 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 contextController.dismissed = { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.state = strongSelf.state.withHighlightedButton(nil) 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 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) self?.logoutAccount(id: id)
}) |> map { ContextController.Items(items: $0) }, gesture: gesture) }) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
self.controller?.presentInGlobalOverlay(contextController) self.controller?.presentInGlobalOverlay(contextController)
} else { } else {
gesture?.cancel() 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 contextController.passthroughTouchEvent = { sourceView, point in
guard let strongSelf = self else { guard let strongSelf = self else {
return .ignore 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)) 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) 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) 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) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
} }
} }