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