diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 9680abb33b..dee5e42818 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -13,22 +13,6 @@ import DirectMediaImageCache import TelegramStringFormatting import TooltipUI -private final class NullActionClass: NSObject, CAAction { - @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { - } -} - -private let nullAction = NullActionClass() - -private class SimpleLayer: CALayer { - override func action(forKey event: String) -> CAAction? { - return nullAction - } - - func update(size: CGSize) { - } -} - private enum SelectionTransition { case begin case change diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index f8750eeecb..0941143254 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -491,6 +491,7 @@ private final class CallListTabBarContextExtractedContentSource: ContextExtracte let keepInPlace: Bool = true let ignoreContentTouches: Bool = true let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true private let controller: ViewController private let sourceNode: ContextExtractedContentContainingNode diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 00fe2f8e1a..ffdbc52d92 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2900,6 +2900,7 @@ private final class ChatListTabBarContextExtractedContentSource: ContextExtracte let keepInPlace: Bool = true let ignoreContentTouches: Bool = true let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true private let controller: ChatListController private let sourceNode: ContextExtractedContentContainingNode diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 5b4b00a598..245220e46c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -866,7 +866,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> let currentRemotePeersValue: ([FoundPeer], [FoundPeer]) = currentRemotePeers.with { $0 } ?? ([], []) - if let query = query { + if let query = query, tagMask == nil { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 846ee21db2..437824c587 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -11,32 +11,7 @@ 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 CounterLayer: SimpleLayer { fileprivate final class Layout { struct Spec: Equatable { let clippingHeight: CGFloat @@ -106,10 +81,6 @@ fileprivate final class CounterLayer: CALayer { 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) @@ -121,7 +92,7 @@ fileprivate final class CounterLayer: CALayer { } } -public final class ReactionButtonAsyncView: UIButton { +public final class ReactionButtonAsyncNode: ContextControllerSourceNode { fileprivate final class Layout { struct Spec: Equatable { var component: ReactionButtonComponent @@ -139,6 +110,7 @@ public final class ReactionButtonAsyncView: UIButton { let counterFrame: CGRect? let backgroundImage: UIImage + let extractedBackgroundImage: UIImage let size: CGSize @@ -151,6 +123,7 @@ public final class ReactionButtonAsyncView: UIButton { counter: CounterLayer.Layout?, counterFrame: CGRect?, backgroundImage: UIImage, + extractedBackgroundImage: UIImage, size: CGSize ) { self.spec = spec @@ -161,6 +134,7 @@ public final class ReactionButtonAsyncView: UIButton { self.counter = counter self.counterFrame = counterFrame self.backgroundImage = backgroundImage + self.extractedBackgroundImage = extractedBackgroundImage self.size = size } @@ -199,8 +173,10 @@ public final class ReactionButtonAsyncView: UIButton { } let backgroundImage: UIImage + let extractedBackgroundImage: UIImage if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter { backgroundImage = currentLayout.backgroundImage + extractedBackgroundImage = currentLayout.extractedBackgroundImage } else { backgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in UIGraphicsPushContext(context) @@ -225,6 +201,31 @@ public final class ReactionButtonAsyncView: UIButton { 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)) + extractedBackgroundImage = 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: spec.component.colors.extractedBackground).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.colors.extractedForeground) + 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)) } @@ -270,6 +271,7 @@ public final class ReactionButtonAsyncView: UIButton { counter: counter, counterFrame: counterFrame, backgroundImage: backgroundImage, + extractedBackgroundImage: extractedBackgroundImage, size: size ) } @@ -277,21 +279,64 @@ public final class ReactionButtonAsyncView: UIButton { private var layout: Layout? + public let containerNode: ContextExtractedContentContainingNode + private let buttonNode: HighlightTrackingButtonNode public let iconView: UIImageView private var counterLayer: CounterLayer? private var avatarsView: AnimatedAvatarSetView? private let iconImageDisposable = MetaDisposable() - override init(frame: CGRect) { + override init() { + self.containerNode = ContextExtractedContentContainingNode() + self.buttonNode = HighlightTrackingButtonNode() + self.iconView = UIImageView() self.iconView.isUserInteractionEnabled = false - super.init(frame: CGRect()) + super.init() - self.addSubview(self.iconView) + self.targetNodeForActivationProgress = self.containerNode.contentNode - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.addSubnode(self.containerNode) + self.containerNode.contentNode.addSubnode(self.buttonNode) + self.buttonNode.view.addSubview(self.iconView) + + self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + let _ = strongSelf + if highlighted { + } else { + } + } + + self.isGestureEnabled = true + + self.containerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, _ in + guard let strongSelf = self, let layout = strongSelf.layout else { + return + } + + let backgroundImage = isExtracted ? layout.extractedBackgroundImage : layout.backgroundImage + + let previousContents = strongSelf.buttonNode.layer.contents + + let backgroundCapInsets = backgroundImage.capInsets + if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { + strongSelf.buttonNode.layer.contentsScale = backgroundImage.scale + strongSelf.buttonNode.layer.contents = backgroundImage.cgImage + } else { + ASDisplayNodeSetResizableContents(strongSelf.buttonNode.layer, backgroundImage) + } + + if let previousContents = previousContents { + strongSelf.buttonNode.layer.animate(from: previousContents as! CGImage, to: backgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) + } + } } required init?(coder aDecoder: NSCoder) { @@ -310,12 +355,17 @@ public final class ReactionButtonAsyncView: UIButton { } fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { + self.containerNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.containerNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.containerNode.contentRect = CGRect(origin: CGPoint(), size: layout.size) + animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) + let backgroundCapInsets = layout.backgroundImage.capInsets if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { - self.layer.contentsScale = layout.backgroundImage.scale - self.layer.contents = layout.backgroundImage.cgImage + self.buttonNode.layer.contentsScale = layout.backgroundImage.scale + self.buttonNode.layer.contents = layout.backgroundImage.cgImage } else { - ASDisplayNodeSetResizableContents(self.layer, layout.backgroundImage) + ASDisplayNodeSetResizableContents(self.buttonNode.layer, layout.backgroundImage) } animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil) @@ -372,7 +422,7 @@ public final class ReactionButtonAsyncView: UIButton { avatarsView = AnimatedAvatarSetView() avatarsView.isUserInteractionEnabled = false self.avatarsView = avatarsView - self.addSubview(avatarsView) + self.buttonNode.view.addSubview(avatarsView) } let content = AnimatedAvatarSetContext().update(peers: layout.spec.component.avatarPeers, animated: false) let avatarsSize = avatarsView.update( @@ -399,7 +449,7 @@ public final class ReactionButtonAsyncView: UIButton { self.layout = layout } - public static func asyncLayout(_ view: ReactionButtonAsyncView?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView) { + public static func asyncLayout(_ view: ReactionButtonAsyncNode?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncNode) { let currentLayout = view?.layout return { component in @@ -414,11 +464,11 @@ public final class ReactionButtonAsyncView: UIButton { return (size: layout.size, apply: { animation in var animation = animation - let updatedView: ReactionButtonAsyncView + let updatedView: ReactionButtonAsyncNode if let view = view { updatedView = view } else { - updatedView = ReactionButtonAsyncView() + updatedView = ReactionButtonAsyncNode() animation = .None } @@ -464,17 +514,23 @@ public final class ReactionButtonComponent: Component { public var selectedBackground: UInt32 public var deselectedForeground: UInt32 public var selectedForeground: UInt32 + public var extractedBackground: UInt32 + public var extractedForeground: UInt32 public init( deselectedBackground: UInt32, selectedBackground: UInt32, deselectedForeground: UInt32, - selectedForeground: UInt32 + selectedForeground: UInt32, + extractedBackground: UInt32, + extractedForeground: UInt32 ) { self.deselectedBackground = deselectedBackground self.selectedBackground = selectedBackground self.deselectedForeground = deselectedForeground self.selectedForeground = selectedForeground + self.extractedBackground = extractedBackground + self.extractedForeground = extractedForeground } } @@ -675,6 +731,25 @@ public final class ReactionButtonComponent: Component { } public final class ReactionButtonsAsyncLayoutContainer { + 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 + } + } + public struct Result { public struct Item { public var size: CGSize @@ -687,15 +762,15 @@ public final class ReactionButtonsAsyncLayoutContainer { public struct ApplyResult { public struct Item { public var value: String - public var view: ReactionButtonAsyncView + public var node: ReactionButtonAsyncNode public var size: CGSize } public var items: [Item] - public var removedViews: [ReactionButtonAsyncView] + public var removedNodes: [ReactionButtonAsyncNode] } - public private(set) var buttons: [String: ReactionButtonAsyncView] = [:] + public private(set) var buttons: [String: ReactionButtonAsyncNode] = [:] public init() { } @@ -703,12 +778,12 @@ public final class ReactionButtonsAsyncLayoutContainer { public func update( context: AccountContext, action: @escaping (String) -> Void, - reactions: [ReactionButtonsLayoutContainer.Reaction], + reactions: [ReactionButtonsAsyncLayoutContainer.Reaction], colors: ReactionButtonComponent.Colors, constrainedWidth: CGFloat ) -> Result { var items: [Result.Item] = [] - var applyItems: [(key: String, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView)] = [] + var applyItems: [(key: String, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncNode)] = [] var validIds = Set() for reaction in reactions.sorted(by: { lhs, rhs in @@ -737,7 +812,7 @@ public final class ReactionButtonsAsyncLayoutContainer { } } - let viewLayout = ReactionButtonAsyncView.asyncLayout(self.buttons[reaction.reaction.value]) + let viewLayout = ReactionButtonAsyncNode.asyncLayout(self.buttons[reaction.reaction.value]) let (size, apply) = viewLayout(ReactionButtonComponent( context: context, colors: colors, @@ -760,10 +835,10 @@ public final class ReactionButtonsAsyncLayoutContainer { removeIds.append(id) } } - var removedViews: [ReactionButtonAsyncView] = [] + var removedNodes: [ReactionButtonAsyncNode] = [] for id in removeIds { - if let view = self.buttons.removeValue(forKey: id) { - removedViews.append(view) + if let node = self.buttons.removeValue(forKey: id) { + removedNodes.append(node) } } @@ -772,128 +847,18 @@ public final class ReactionButtonsAsyncLayoutContainer { 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)) + let node = apply(animation) + items.append(ApplyResult.Item(value: key, node: node, size: size)) if let current = self.buttons[key] { - assert(current === view) + assert(current === node) } else { - self.buttons[key] = view + self.buttons[key] = node } } - return ApplyResult(items: items, removedViews: removedViews) + return ApplyResult(items: items, removedNodes: removedNodes) } ) } } - -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 - } - } - - public struct Result { - public struct Item { - public var view: ComponentHostView - public var size: CGSize - } - - public var items: [Item] - public var removedViews: [ComponentHostView] - } - - public private(set) var buttons: [String: ComponentHostView] = [:] - - public init() { - } - - public func update( - context: AccountContext, - action: @escaping (String) -> Void, - reactions: [Reaction], - colors: ReactionButtonComponent.Colors, - constrainedWidth: CGFloat, - transition: Transition - ) -> Result { - var items: [Result.Item] = [] - var removedViews: [ComponentHostView] = [] - - var validIds = Set() - for reaction in reactions.sorted(by: { lhs, rhs in - var lhsCount = lhs.count - if lhs.isSelected { - lhsCount -= 1 - } - var rhsCount = rhs.count - if rhs.isSelected { - rhsCount -= 1 - } - if lhsCount != rhsCount { - return lhsCount > rhsCount - } - return lhs.reaction.value < rhs.reaction.value - }) { - validIds.insert(reaction.reaction.value) - - let view: ComponentHostView - var itemTransition = transition - if let current = self.buttons[reaction.reaction.value] { - itemTransition = .immediate - view = current - } else { - view = ComponentHostView() - self.buttons[reaction.reaction.value] = view - } - let itemSize = view.update( - transition: itemTransition, - component: AnyComponent(ReactionButtonComponent( - context: context, - colors: colors, - reaction: reaction.reaction, - avatarPeers: reaction.peers, - count: reaction.count, - isSelected: reaction.isSelected, - action: action - )), - environment: {}, - containerSize: CGSize(width: constrainedWidth, height: 1000.0) - ) - items.append(Result.Item( - view: view, - size: itemSize - )) - } - - var removeIds: [String] = [] - for (id, view) in self.buttons { - if !validIds.contains(id) { - removeIds.append(id) - removedViews.append(view) - } - } - for id in removeIds { - self.buttons.removeValue(forKey: id) - } - - return Result( - items: items, - removedViews: removedViews - ) - } -} diff --git a/submodules/Components/ReactionImageComponent/BUILD b/submodules/Components/ReactionImageComponent/BUILD new file mode 100644 index 0000000000..603bc1d792 --- /dev/null +++ b/submodules/Components/ReactionImageComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ReactionImageComponent", + module_name = "ReactionImageComponent", + 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/WebPBinding:WebPBinding", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift new file mode 100644 index 0000000000..8de5bcd575 --- /dev/null +++ b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift @@ -0,0 +1,96 @@ +import Foundation +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData +import UIKit +import WebPBinding + +public final class ReactionImageNode: ASImageNode { + private var disposable: Disposable? + public let size: CGSize + + public init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) { + var file: TelegramMediaFile? + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction { + file = availableReaction.staticIcon + break + } + } + } + if let file = file { + self.size = file.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) + + super.init() + + self.disposable = (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.image = image + } + } + }) + } else { + self.size = CGSize(width: 18.0, height: 18.0) + super.init() + } + } + + deinit { + self.disposable?.dispose() + } +} + +public final class ReactionFileImageNode: ASImageNode { + private let disposable = MetaDisposable() + + private var currentFile: TelegramMediaFile? + + override public init() { + } + + deinit { + self.disposable.dispose() + } + + public func asyncLayout() -> (_ context: AccountContext, _ file: TelegramMediaFile?) -> (size: CGSize, apply: () -> Void) { + return { [weak self] context, file in + let size = file?.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) + + return (size, { + guard let strongSelf = self else { + return + } + if strongSelf.currentFile != file { + strongSelf.currentFile = file + + if let file = file { + strongSelf.disposable.set((context.account.postbox.mediaBox.resourceData(file.resource) + |> deliverOnMainQueue).start(next: { 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.image = image + } + } + })) + } + } + }) + } + } +} diff --git a/submodules/Components/ReactionListContextMenuContent/BUILD b/submodules/Components/ReactionListContextMenuContent/BUILD index 70072765c9..feaf8c08f7 100644 --- a/submodules/Components/ReactionListContextMenuContent/BUILD +++ b/submodules/Components/ReactionListContextMenuContent/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/ContextUI:ContextUI", "//submodules/AvatarNode:AvatarNode", + "//submodules/Components/ReactionImageComponent:ReactionImageComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index d5df070e9d..c4bf2af919 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -12,48 +12,7 @@ import WebPBinding import AnimatedAvatarSetNode import ContextUI import AvatarNode - -private final class ReactionImageNode: ASImageNode { - private var disposable: Disposable? - let size: CGSize - - init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) { - var file: TelegramMediaFile? - if let availableReactions = availableReactions { - for availableReaction in availableReactions.reactions { - if availableReaction.value == reaction { - file = availableReaction.staticIcon - break - } - } - } - if let file = file { - self.size = file.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) - - super.init() - - self.disposable = (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.image = image - } - } - }) - } else { - self.size = CGSize(width: 18.0, height: 18.0) - super.init() - } - } - - deinit { - self.disposable?.dispose() - } -} +import ReactionImageComponent private let avatarFont = avatarPlaceholderFont(size: 16.0) @@ -212,12 +171,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let contentSize = CGSize(width: sideInset * 2.0 + titleSize.width + iconSize.width + iconSpacing, height: titleSize.height) - self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floor((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize) + self.titleLabelNode.frame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: floorToScreenPixels((constrainedSize.height - titleSize.height) / 2.0)), size: titleSize) if let reactionIconNode = self.reactionIconNode { - reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) + reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) } else if let iconNode = self.iconNode { - iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) + iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) } return CGSize(width: contentSize.width, height: constrainedSize.height) @@ -264,9 +223,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } func update(size: CGSize, presentationData: PresentationData, selectedReaction: String?, transition: ContainedViewLayoutTransition) { - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = 11.0 let spacing: CGFloat = 0.0 - let verticalInset: CGFloat = 6.0 + let verticalInset: CGFloat = 7.0 self.selectionHighlightNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor let highlightHeight: CGFloat = size.height - verticalInset * 2.0 @@ -354,9 +313,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } func update(size: CGSize, presentationData: PresentationData, item: EngineMessageReactionListContext.Item, isLast: Bool, syncronousLoad: Bool) { - let avatarInset: CGFloat = 10.0 + let avatarInset: CGFloat = 12.0 let avatarSpacing: CGFloat = 8.0 let avatarSize: CGFloat = 28.0 + let sideInset: CGFloat = 16.0 let reaction: String? = item.reaction if let reaction = reaction { @@ -377,7 +337,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.avatarNode.frame = CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: item.peer, synchronousLoad: true) - let sideInset: CGFloat = 16.0 self.titleLabelNode.attributedText = NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor) var maxTextWidth: CGFloat = size.width - avatarInset - avatarSize - avatarSpacing - sideInset if reactionIconNode != nil { @@ -418,6 +377,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private var itemNodes: [Int: ItemNode] = [:] + private var placeholderItemImage: UIImage? + private var placeholderLayers: [Int: SimpleLayer] = [:] + init( context: AccountContext, availableReactions: AvailableReactions?, @@ -434,7 +396,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.requestUpdateApparentHeight = requestUpdateApparentHeight self.openPeer = openPeer - self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) self.listContext = context.engine.messages.messageReactionList(message: message, reaction: reaction) self.state = EngineMessageReactionListContext.State(message: message, reaction: reaction) @@ -464,9 +425,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent animateIn = true } strongSelf.state = state - strongSelf.requestUpdate(strongSelf, .immediate) + strongSelf.requestUpdate(strongSelf, animateIn ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) if animateIn { - strongSelf.scrollNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + for (_, itemNode) in strongSelf.itemNodes { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } }) } @@ -479,7 +442,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.ignoreScrolling { return } - self.updateVisibleItems(syncronousLoad: false) + self.updateVisibleItems(animated: false, syncronousLoad: false) if let size = self.currentSize { var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height @@ -493,7 +456,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } - private func updateVisibleItems(syncronousLoad: Bool) { + private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) { guard let size = self.currentSize else { return } @@ -504,34 +467,50 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0) var validIds = Set() + var validPlaceholderIds = Set() let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight))) let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight)) if minVisibleIndex <= maxVisibleIndex { for index in minVisibleIndex ... maxVisibleIndex { - if index >= self.state.items.count { - break - } - - validIds.insert(index) - - let itemNode: ItemNode - if let current = self.itemNodes[index] { - itemNode = current - } else { - let openPeer = self.openPeer - let peerId = self.state.items[index].peer.id - itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { - openPeer(peerId) - }) - self.itemNodes[index] = itemNode - self.scrollNode.addSubnode(itemNode) - } - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight)) - itemNode.update(size: itemFrame.size, presentationData: presentationData, item: self.state.items[index], isLast: index == self.state.items.count - 1, syncronousLoad: syncronousLoad) - itemNode.frame = itemFrame + + if index < self.state.items.count { + validIds.insert(index) + + let itemNode: ItemNode + if let current = self.itemNodes[index] { + itemNode = current + } else { + let openPeer = self.openPeer + let peerId = self.state.items[index].peer.id + itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { + openPeer(peerId) + }) + self.itemNodes[index] = itemNode + self.scrollNode.addSubnode(itemNode) + } + + itemNode.update(size: itemFrame.size, presentationData: presentationData, item: self.state.items[index], isLast: index == self.state.items.count - 1, syncronousLoad: syncronousLoad) + itemNode.frame = itemFrame + } else { + validPlaceholderIds.insert(index) + + let placeholderLayer: SimpleLayer + if let current = self.placeholderLayers[index] { + placeholderLayer = current + } else { + placeholderLayer = SimpleLayer() + if let placeholderItemImage = self.placeholderItemImage { + ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage) + } + self.placeholderLayers[index] = placeholderLayer + self.scrollNode.layer.addSublayer(placeholderLayer) + } + + placeholderLayer.frame = itemFrame + } } } @@ -542,18 +521,71 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent itemNode.removeFromSupernode() } } - for id in removeIds { self.itemNodes.removeValue(forKey: id) } + var removePlaceholderIds: [Int] = [] + for (id, placeholderLayer) in self.placeholderLayers { + if !validPlaceholderIds.contains(id) { + removePlaceholderIds.append(id) + if animated { + placeholderLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderLayer] _ in + placeholderLayer?.removeFromSuperlayer() + }) + } else { + placeholderLayer.removeFromSuperlayer() + } + } + } + for id in removePlaceholderIds { + self.placeholderLayers.removeValue(forKey: id) + } + if self.state.canLoadMore && maxVisibleIndex >= self.state.items.count - 16 { self.listContext.loadMore() } } - func update(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) { + func update(presentationData: PresentationData, constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) { let itemHeight: CGFloat = 44.0 + + if self.presentationData?.theme !== presentationData.theme { + let sideInset: CGFloat = 40.0 + let avatarInset: CGFloat = 12.0 + let avatarSpacing: CGFloat = 8.0 + let avatarSize: CGFloat = 28.0 + let lineHeight: CGFloat = 8.0 + + let shimmeringForegroundColor: UIColor + let shimmeringColor: UIColor + if presentationData.theme.overallDarkAppearance { + let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0) + + shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1) + shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } else { + shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07) + shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } + let _ = shimmeringColor + + self.placeholderItemImage = generateImage(CGSize(width: avatarInset + avatarSize + avatarSpacing + lineHeight + 2.0 + sideInset, height: itemHeight), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(shimmeringForegroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - lineHeight) / 2.0)), size: CGSize(width: lineHeight + 2.0, height: lineHeight))) + })?.stretchableImage(withLeftCapWidth: Int(avatarInset + avatarSize + avatarSpacing + lineHeight / 2.0 + 1.0), topCapHeight: 0) + + if let placeholderItemImage = self.placeholderItemImage { + for (_, placeholderLayer) in self.placeholderLayers { + ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage) + } + } + } + self.presentationData = presentationData + let size = CGSize(width: constrainedSize.width, height: CGFloat(self.state.totalCount) * itemHeight) let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height)) @@ -569,7 +601,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } self.ignoreScrolling = false - self.updateVisibleItems(syncronousLoad: !transition.isAnimated) + self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated) var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height apparentHeight = max(apparentHeight, 44.0) @@ -604,9 +636,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, + reaction: String?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, - back: @escaping () -> Void, + back: (() -> Void)?, openPeer: @escaping (PeerId) -> Void ) { self.context = context @@ -620,20 +653,26 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? - self.backButtonNode = BackButtonNode() - self.backButtonNode?.action = { - back() + if let back = back { + self.backButtonNode = BackButtonNode() + self.backButtonNode?.action = { + back() + } } var reactions: [(String?, Int)] = [] var totalCount: Int = 0 if let reactionsAttribute = message._asMessage().reactionsAttribute { - for reaction in reactionsAttribute.reactions { - totalCount += Int(reaction.count) - reactions.append((reaction.value, Int(reaction.count))) + for listReaction in reactionsAttribute.reactions { + if reaction == nil || listReaction.value == reaction { + totalCount += Int(listReaction.count) + reactions.append((listReaction.value, Int(listReaction.count))) + } } } - reactions.insert((nil, totalCount), at: 0) + if reaction == nil { + reactions.insert((nil, totalCount), at: 0) + } if reactions.count > 2 { self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message) @@ -641,13 +680,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.reactions = reactions - self.separatorNode = ASDisplayNode() - self.currentTabNode = ReactionsTabNode( context: context, availableReactions: availableReactions, message: message, - reaction: nil, + reaction: reaction, requestUpdate: { tab, transition in requestUpdateTab?(tab, transition) }, @@ -661,6 +698,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent super.init() + if self.backButtonNode != nil || self.tabListNode != nil { + self.separatorNode = ASDisplayNode() + } + if let backButtonNode = self.backButtonNode { self.addSubnode(backButtonNode) } @@ -718,7 +759,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } - func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { let constrainedSize = CGSize(width: min(260.0, constrainedWidth), height: maxHeight) var topContentHeight: CGFloat = 0.0 @@ -745,7 +786,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.currentTabNode.bounds.isEmpty { currentTabTransition = .immediate } - let currentTabLayout = self.currentTabNode.update(constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition) + let currentTabLayout = self.currentTabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition) currentTabTransition.updateFrame(node: self.currentTabNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: currentTabLayout.size.width, height: currentTabLayout.size.height + 100.0))) if let dismissedTabNode = self.dismissedTabNode { @@ -775,13 +816,15 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let context: AccountContext let availableReactions: AvailableReactions? let message: EngineMessage - let back: () -> Void + let reaction: String? + let back: (() -> Void)? let openPeer: (PeerId) -> Void - public init(context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, back: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) { + public init(context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, reaction: String?, back: (() -> Void)?, openPeer: @escaping (PeerId) -> Void) { self.context = context self.availableReactions = availableReactions self.message = message + self.reaction = reaction self.back = back self.openPeer = openPeer } @@ -794,6 +837,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent context: self.context, availableReactions: self.availableReactions, message: self.message, + reaction: self.reaction, requestUpdate: requestUpdate, requestUpdateApparentHeight: requestUpdateApparentHeight, back: self.back, diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 8a9a1ab98a..481ae15f1e 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2033,6 +2033,7 @@ public protocol ContextExtractedContentSource: AnyObject { var keepInPlace: Bool { get } var ignoreContentTouches: Bool { get } var blurBackground: Bool { get } + var centerActionsHorizontally: Bool { get } var shouldBeDismissed: Signal { get } func takeView() -> ContextControllerTakeViewInfo? @@ -2043,6 +2044,10 @@ public extension ContextExtractedContentSource { var centerVertically: Bool { return false } + + var centerActionsHorizontally: Bool { + return false + } var shouldBeDismissed: Signal { return .single(false) @@ -2076,7 +2081,7 @@ public enum ContextContentSource { } public protocol ContextControllerItemsNode: ASDisplayNode { - func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) var apparentHeight: CGFloat { get } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 2cc4332b79..13d67fbeba 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -503,6 +503,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { let contentLayout = self.contentNode.update( + presentationData: presentationData, constrainedWidth: constrainedSize.width, maxHeight: constrainedSize.height, bottomInset: 0.0, @@ -555,18 +556,65 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> Co final class ContextControllerActionsStackNode: ASDisplayNode { final class NavigationContainer: ASDisplayNode { + var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? + var requestPop: (() -> Void)? + var transitionFraction: CGFloat = 0.0 + + private var panRecognizer: UIPanGestureRecognizer? + + var isNavigationEnabled: Bool = false { + didSet { + self.panRecognizer?.isEnabled = self.isNavigationEnabled + } + } + override init() { super.init() self.clipsToBounds = true self.cornerRadius = 14.0 + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.panRecognizer = panRecognizer + self.view.addGestureRecognizer(panRecognizer) + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.transitionFraction = 0.0 + case .changed: + let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if self.transitionFraction != transitionFraction { + self.transitionFraction = transitionFraction + self.requestUpdate?(.immediate) + } + case .ended, .cancelled: + let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if transitionFraction > 0.2 { + self.transitionFraction = 0.0 + self.requestPop?() + } else { + self.transitionFraction = 0.0 + self.requestUpdate?(.animated(duration: 0.45, curve: .spring)) + } + default: + break + } + } + + func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) { } } final class ItemContainer: ASDisplayNode { let requestUpdate: (ContainedViewLayoutTransition) -> Void let node: ContextControllerActionsStackItemNode + let dimNode: ASDisplayNode let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? + var storedScrollingState: CGFloat? let positionLock: CGFloat? init( @@ -586,18 +634,24 @@ final class ContextControllerActionsStackNode: ASDisplayNode { requestUpdateApparentHeight: requestUpdateApparentHeight ) + self.dimNode = ASDisplayNode() + self.dimNode.isUserInteractionEnabled = false + self.dimNode.alpha = 0.0 + self.reactionItems = reactionItems self.positionLock = positionLock super.init() self.addSubnode(self.node) + self.addSubnode(self.dimNode) } func update( presentationData: PresentationData, constrainedSize: CGSize, standardWidth: CGFloat, + transitionFraction: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { let (size, apparentHeight) = self.node.update( @@ -606,10 +660,24 @@ final class ContextControllerActionsStackNode: ASDisplayNode { standardWidth: standardWidth, transition: transition ) - transition.updateFrame(node: self.node, frame: CGRect(origin: CGPoint(), size: size)) + + let maxScaleOffset: CGFloat = 10.0 + let scaleOffset: CGFloat = 0.0 * transitionFraction + maxScaleOffset * (1.0 - transitionFraction) + let scale: CGFloat = (size.width - scaleOffset) / size.width + let yOffset: CGFloat = size.height * (1.0 - scale) + transition.updatePosition(node: self.node, position: CGPoint(x: size.width / 2.0 + scaleOffset / 2.0, y: size.height / 2.0 - yOffset / 2.0)) + transition.updateBounds(node: self.node, bounds: CGRect(origin: CGPoint(), size: size)) + transition.updateTransformScale(node: self.node, scale: scale) return (size, apparentHeight) } + + func updateDimNode(presentationData: PresentationData, size: CGSize, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition) { + self.dimNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateAlpha(node: self.dimNode, alpha: 1.0 - transitionFraction) + } } private let getController: () -> ContextControllerProtocol? @@ -628,6 +696,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { return self.itemContainers.last?.positionLock } + var storedScrollingState: CGFloat? { + return self.itemContainers.last?.storedScrollingState + } + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -642,6 +714,20 @@ final class ContextControllerActionsStackNode: ASDisplayNode { super.init() self.addSubnode(self.navigationContainer) + + self.navigationContainer.requestUpdate = { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.requestUpdate(transition) + } + + self.navigationContainer.requestPop = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.pop() + } } func replace(item: ContextControllerActionsStackItem, animated: Bool) { @@ -653,11 +739,15 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } } self.itemContainers.removeAll() + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 - self.push(item: item, positionLock: nil, animated: animated) + self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: animated) } - func push(item: ContextControllerActionsStackItem, positionLock: CGFloat?, animated: Bool) { + func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) { + if let itemContainer = self.itemContainers.last { + itemContainer.storedScrollingState = currentScrollingState + } let itemContainer = ItemContainer( getController: self.getController, requestDismiss: self.requestDismiss, @@ -674,6 +764,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { ) self.itemContainers.append(itemContainer) self.navigationContainer.addSubnode(itemContainer) + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 let transition: ContainedViewLayoutTransition if animated { @@ -684,6 +775,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.requestUpdate(transition) } + func clearStoredScrollingState() { + self.itemContainers.last?.storedScrollingState = nil + } + func pop() { if self.itemContainers.count == 1 { //dismiss @@ -693,6 +788,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.dismissingItemContainers.append((itemContainer, true)) } + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 + let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) self.requestUpdate(transition) } @@ -706,8 +803,17 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty + struct ItemLayout { + var size: CGSize + var apparentHeight: CGFloat + var transitionFraction: CGFloat + var alphaTransitionFraction: CGFloat + var itemTransition: ContainedViewLayoutTransition + var animateAppearingContainer: Bool + } + var topItemSize = CGSize() - var topItemApparentHeight: CGFloat = 0.0 + var itemLayouts: [ItemLayout] = [] for i in 0 ..< self.itemContainers.count { let itemContainer = self.itemContainers[i] @@ -720,31 +826,77 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let itemConstrainedHeight: CGFloat = constrainedSize.height + let transitionFraction: CGFloat + let alphaTransitionFraction: CGFloat + if i == self.itemContainers.count - 1 { + transitionFraction = self.navigationContainer.transitionFraction + alphaTransitionFraction = 1.0 + } else if i == self.itemContainers.count - 2 { + transitionFraction = self.navigationContainer.transitionFraction - 1.0 + alphaTransitionFraction = self.navigationContainer.transitionFraction + } else { + transitionFraction = 0.0 + alphaTransitionFraction = 0.0 + } + let itemSize = itemContainer.update( presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: itemConstrainedHeight), - standardWidth: 260.0, + standardWidth: 250.0, + transitionFraction: alphaTransitionFraction, transition: itemContainerTransition ) if i == self.itemContainers.count - 1 { topItemSize = itemSize.size - topItemApparentHeight = itemSize.apparentHeight } - let itemFrame: CGRect - if i == self.itemContainers.count - 1 { - itemFrame = CGRect(origin: CGPoint(), size: itemSize.size) - } else { - itemFrame = CGRect(origin: CGPoint(x: -itemSize.size.width, y: 0.0), size: itemSize.size) - } - - itemContainerTransition.updateFrame(node: itemContainer, frame: itemFrame) - if animateAppearingContainer { - transition.animatePositionAdditive(node: itemContainer, offset: CGPoint(x: itemContainer.bounds.width, y: 0.0)) - } + itemLayouts.append(ItemLayout( + size: itemSize.size, + apparentHeight: itemSize.apparentHeight, + transitionFraction: transitionFraction, + alphaTransitionFraction: alphaTransitionFraction, + itemTransition: itemContainerTransition, + animateAppearingContainer: animateAppearingContainer + )) } - transition.updateFrame(node: self.navigationContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: topItemSize.width, height: max(44.0, topItemApparentHeight)))) + let topItemApparentHeight: CGFloat + let topItemWidth: CGFloat + if itemLayouts.isEmpty { + topItemApparentHeight = 0.0 + topItemWidth = 0.0 + } else if itemLayouts.count == 1 { + topItemApparentHeight = itemLayouts[0].apparentHeight + topItemWidth = itemLayouts[0].size.width + } else { + let lastItemLayout = itemLayouts[itemLayouts.count - 1] + let previousItemLayout = itemLayouts[itemLayouts.count - 2] + let transitionFraction = self.navigationContainer.transitionFraction + + topItemApparentHeight = lastItemLayout.apparentHeight * (1.0 - transitionFraction) + previousItemLayout.apparentHeight * transitionFraction + topItemWidth = lastItemLayout.size.width * (1.0 - transitionFraction) + previousItemLayout.size.width * transitionFraction + } + + let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight))) + transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame) + self.navigationContainer.update(presentationData: presentationData, size: navigationContainerFrame.size, transition: transition) + + for i in 0 ..< self.itemContainers.count { + let xOffset: CGFloat + if itemLayouts[i].transitionFraction < 0.0 { + xOffset = itemLayouts[i].transitionFraction * itemLayouts[i].size.width + } else { + xOffset = itemLayouts[i].transitionFraction * topItemWidth + } + let itemFrame = CGRect(origin: CGPoint(x: xOffset, y: 0.0), size: itemLayouts[i].size) + + itemLayouts[i].itemTransition.updateFrame(node: self.itemContainers[i], frame: itemFrame) + if itemLayouts[i].animateAppearingContainer { + transition.animatePositionAdditive(node: self.itemContainers[i], offset: CGPoint(x: itemFrame.width, y: 0.0)) + } + + self.itemContainers[i].updateDimNode(presentationData: presentationData, size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.size.height), transitionFraction: itemLayouts[i].alphaTransitionFraction, transition: transition) + } for (itemContainer, isPopped) in self.dismissingItemContainers { transition.updatePosition(node: itemContainer, position: CGPoint(x: isPopped ? itemContainer.bounds.width * 3.0 / 2.0 : -itemContainer.bounds.width / 2.0, y: itemContainer.position.y), completion: { [weak itemContainer] _ in @@ -753,6 +905,6 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } self.dismissingItemContainers.removeAll() - return CGSize(width: topItemSize.width, height: topItemSize.height) + return CGSize(width: topItemWidth, height: topItemSize.height) } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 65dd811954..efa227ca95 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -137,6 +137,20 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo return result } } + + if !self.source.ignoreContentTouches, let contentNode = self.contentNode { + let contentPoint = self.view.convert(point, to: contentNode.containingNode.contentNode.view) + if let result = contentNode.containingNode.contentNode.customHitTest?(contentPoint) { + return result + } else if let result = contentNode.containingNode.contentNode.hitTest(contentPoint, with: event) { + if result is TextSelectionNodeView { + return result + } else if contentNode.containingNode.contentRect.contains(contentPoint) { + return contentNode.containingNode.contentNode.view + } + } + } + return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) } else { return nil @@ -148,16 +162,21 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } func pushItems(items: ContextController.Items) { + let currentScrollingState = self.getCurrentScrollingState() let positionLock = self.getActionsStackPositionLock() - self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), positionLock: positionLock, animated: true) + self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), currentScrollingState: currentScrollingState, positionLock: positionLock, animated: true) } func popItems() { self.actionsStackNode.pop() } + private func getCurrentScrollingState() -> CGFloat { + return self.scrollNode.view.contentOffset.y + } + private func getActionsStackPositionLock() -> CGFloat? { - return self.actionsStackNode.frame.minY + return self.actionsStackNode.view.convert(CGPoint(), to: self.view).y } func update( @@ -166,7 +185,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition: ContainedViewLayoutTransition, stateTransition: ContextControllerPresentationNodeStateTransition? ) { - let contentActionsSpacing: CGFloat = 8.0 + let contentActionsSpacing: CGFloat = 7.0 + let actionsEdgeInset: CGFloat = 12.0 let actionsSideInset: CGFloat = 6.0 let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0 let bottomInset: CGFloat = 10.0 @@ -236,7 +256,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view) } } - //let contentRectGlobalFrame = contentNode.storedGlobalFrame ?? convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view) + + let contentParentGlobalFrame = convertFrame(contentNode.containingNode.bounds, from: contentNode.containingNode.view, to: self.view) + let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingNode.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingNode.contentRect.height), size: contentNode.containingNode.contentRect.size) var contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingNode.contentRect.size.height), size: contentNode.containingNode.contentRect.size) if case .animateOut = stateTransition { @@ -255,7 +277,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let actionsPositionLock = self.actionsStackNode.topPositionLock { actionsConstrainedHeight = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - actionsPositionLock } else { - actionsConstrainedHeight = layout.size.height + actionsConstrainedHeight = layout.size.height - contentTopInset - contentRect.height - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom } let actionsSize = self.actionsStackNode.update( @@ -266,18 +288,23 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .animateOut = stateTransition { } else { - if contentRect.minY < contentTopInset { - contentRect.origin.y = contentTopInset + if let topPositionLock = self.actionsStackNode.topPositionLock { + contentRect.origin.y = topPositionLock - contentActionsSpacing - contentRect.height + } else if self.source.keepInPlace { + } else { + if contentRect.minY < contentTopInset { + contentRect.origin.y = contentTopInset + } + var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height)) + if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom { + combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height + } + if combinedBounds.minY < contentTopInset { + combinedBounds.origin.y = contentTopInset + } + + contentRect.origin.y = combinedBounds.minY } - var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height)) - if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom { - combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height - } - if combinedBounds.minY < contentTopInset { - combinedBounds.origin.y = contentTopInset - } - - contentRect.origin.y = combinedBounds.minY } if let reactionContextNode = self.reactionContextNode { @@ -297,22 +324,51 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect) - var actionsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) - if contentRect.midX < layout.size.width / 2.0 { - actionsFrame.origin.x = contentRect.minX + actionsSideInset - 3.0 + var actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) + if self.source.keepInPlace { + actionsFrame.origin.y = contentRect.minY - contentActionsSpacing - actionsFrame.height + } + if self.source.centerActionsHorizontally { + actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) + if actionsFrame.maxX > layout.size.width - actionsEdgeInset { + actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width + } + if actionsFrame.minX < actionsEdgeInset { + actionsFrame.origin.x = actionsEdgeInset + } } else { - actionsFrame.origin.x = contentRect.maxX - actionsSideInset - actionsSize.width + if contentRect.midX < layout.size.width / 2.0 { + actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.minX + actionsSideInset - 4.0 + } else { + actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 + } + if actionsFrame.maxX > layout.size.width - actionsEdgeInset { + actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width + } + if actionsFrame.minX < actionsEdgeInset { + actionsFrame.origin.x = actionsEdgeInset + } } transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame) - contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size)) + contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size)) - let contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom + let contentHeight: CGFloat + if self.actionsStackNode.topPositionLock != nil { + contentHeight = layout.size.height + } else { + contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom + } let contentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != contentSize { let previousContentOffset = self.scrollNode.view.contentOffset self.scrollNode.view.contentSize = contentSize + if let storedScrollingState = self.actionsStackNode.storedScrollingState { + self.actionsStackNode.clearStoredScrollingState() + + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: storedScrollingState) + } if case .none = stateTransition, transition.isAnimated { let contentOffset = self.scrollNode.view.contentOffset transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: previousContentOffset.y - contentOffset.y) @@ -370,7 +426,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let actionsSize = self.actionsStackNode.bounds.size let actionsPositionDeltaXDistance: CGFloat = 0.0 - let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing + let actionsVerticalTransitionDirection: CGFloat + if contentNode.frame.minY < self.actionsStackNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } + let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animateSpring( from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), to: NSValue(cgPoint: CGPoint()), @@ -436,7 +498,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo public var updateDistractionFreeMode: ((Bool) -> Void)? public var requestDismiss: (() -> Void)*/ case let .animateOut(result, completion): - let duration: Double = 0.25 + let duration: Double = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2 let putBackInfo = self.source.putBack() @@ -463,11 +525,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) } - print("animationInContentDistance: \(animationInContentDistance)") + let actionsVerticalTransitionDirection: CGFloat + if contentNode.frame.minY < self.actionsStackNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } contentNode.containingNode.willUpdateIsExtractedToContextPreview?(false, transition) contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: 0.0, dy: -animationInContentDistance) + let reactionContextNodeIsAnimatingOut = self.reactionContextNodeIsAnimatingOut contentNode.offsetContainerNode.layer.animate( from: animationInContentDistance as NSNumber, to: 0.0 as NSNumber, @@ -477,7 +545,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo delay: 0.0, additive: true, completion: { [weak self] _ in - Queue.mainQueue().after(0.2 * UIView.animationDurationFactor(), { + Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, { contentNode.containingNode.isExtractedToContextPreview = false contentNode.containingNode.isExtractedToContextPreviewUpdated?(false) @@ -489,16 +557,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo }) } ) - /*Queue.mainQueue().after((duration + 0.2) * UIView.animationDurationFactor(), { [weak self] in - contentNode.containingNode.isExtractedToContextPreview = false - contentNode.containingNode.isExtractedToContextPreviewUpdated?(false) - - if let strongSelf = self, let contentNode = strongSelf.contentNode { - contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode) - } - - completion() - })*/ self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.actionsStackNode.layer.animate( @@ -514,7 +572,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let actionsSize = self.actionsStackNode.bounds.size let actionsPositionDeltaXDistance: CGFloat = 0.0 - let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing + let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animate( from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), diff --git a/submodules/Display/Source/ContextControllerSourceNode.swift b/submodules/Display/Source/ContextControllerSourceNode.swift index 08735ca8a9..1cadfe7ba7 100644 --- a/submodules/Display/Source/ContextControllerSourceNode.swift +++ b/submodules/Display/Source/ContextControllerSourceNode.swift @@ -1,7 +1,7 @@ import Foundation import AsyncDisplayKit -public final class ContextControllerSourceNode: ASDisplayNode { +open class ContextControllerSourceNode: ASDisplayNode { private var contextGesture: ContextGesture? public var isGestureEnabled: Bool = true { @@ -14,6 +14,7 @@ public final class ContextControllerSourceNode: ASDisplayNode { public var activated: ((ContextGesture, CGPoint) -> Void)? public var shouldBegin: ((CGPoint) -> Bool)? public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)? + public weak var additionalActivationProgressLayer: CALayer? public var targetNodeForActivationProgress: ASDisplayNode? public var targetNodeForActivationProgressContentRect: CGRect? @@ -23,7 +24,7 @@ public final class ContextControllerSourceNode: ASDisplayNode { self.contextGesture?.isEnabled = self.isGestureEnabled } - override public func didLoad() { + override open func didLoad() { super.didLoad() let contextGesture = ContextGesture(target: self, action: nil) @@ -75,15 +76,27 @@ public final class ContextControllerSourceNode: ASDisplayNode { case .update: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) targetNode.layer.sublayerTransform = sublayerTransform + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + additionalActivationProgressLayer.transform = sublayerTransform + } case .begin: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) targetNode.layer.sublayerTransform = sublayerTransform + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + additionalActivationProgressLayer.transform = sublayerTransform + } case .ended: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) let previousTransform = targetNode.layer.sublayerTransform targetNode.layer.sublayerTransform = sublayerTransform targetNode.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) + + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { + additionalActivationProgressLayer.transform = sublayerTransform + }) + } } } } diff --git a/submodules/Display/Source/SimpleLayer.swift b/submodules/Display/Source/SimpleLayer.swift new file mode 100644 index 0000000000..43f6e3520c --- /dev/null +++ b/submodules/Display/Source/SimpleLayer.swift @@ -0,0 +1,26 @@ +import UIKit + +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +open class SimpleLayer: CALayer { + override open func action(forKey event: String) -> CAAction? { + return nullAction + } + + override public init() { + super.init() + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 31b9dc9796..a569b6ce2a 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -84,8 +84,7 @@ public final class HashtagSearchController: TelegramBaseController { let listInteraction = ListMessageItemInteraction(openMessage: { message, mode -> Bool in return true - }, openMessageContextMenu: { message, bool, node, rect, gesture in - + }, openMessageContextMenu: { message, bool, node, rect, gesture in }, toggleMessagesSelection: { messageId, selected in }, openUrl: { url, _, _, message in }, openInstantPage: { message, data in diff --git a/submodules/PeerInfoUI/BUILD b/submodules/PeerInfoUI/BUILD index 25accba0be..cfd857c73d 100644 --- a/submodules/PeerInfoUI/BUILD +++ b/submodules/PeerInfoUI/BUILD @@ -73,6 +73,7 @@ swift_library( "//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/Components/ReactionImageComponent:ReactionImageComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift new file mode 100644 index 0000000000..af0568660f --- /dev/null +++ b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift @@ -0,0 +1,467 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import SwitchNode +import TelegramCore +import ItemListUI +import ReactionImageComponent +import AccountContext + +public class ItemListReactionItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let file: TelegramMediaFile? + let title: String + let value: Bool + let enabled: Bool + public let sectionId: ItemListSectionId + let style: ItemListStyle + let updated: (Bool) -> Void + public let tag: ItemListItemTag? + + public init( + context: AccountContext, + presentationData: ItemListPresentationData, + file: TelegramMediaFile?, + title: String, + value: Bool, + enabled: Bool = true, + sectionId: ItemListSectionId, + style: ItemListStyle, + updated: @escaping (Bool) -> Void, + tag: ItemListItemTag? = nil + ) { + self.context = context + self.presentationData = presentationData + self.file = file + self.title = title + self.value = value + self.enabled = enabled + self.sectionId = sectionId + self.style = style + self.updated = updated + self.tag = tag + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ItemListReactionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(false) }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListReactionItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + var animated = true + if case .None = animation { + animated = false + } + apply(animated) + }) + } + } + } + } + } +} + +private protocol ItemListSwitchNodeImpl { + var frameColor: UIColor { get set } + var contentColor: UIColor { get set } + var handleColor: UIColor { get set } + var positiveContentColor: UIColor { get set } + var negativeContentColor: UIColor { get set } + + var isOn: Bool { get } + func setOn(_ value: Bool, animated: Bool) +} + +extension SwitchNode: ItemListSwitchNodeImpl { + var positiveContentColor: UIColor { + get { + return .white + } set(value) { + + } + } + var negativeContentColor: UIColor { + get { + return .white + } set(value) { + + } + } +} + +extension IconSwitchNode: ItemListSwitchNodeImpl { +} + +public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let imageNode: ReactionFileImageNode + private let titleNode: TextNode + private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl + private let switchGestureNode: ASDisplayNode + private var disabledOverlayNode: ASDisplayNode? + + private let activateArea: AccessibilityAreaNode + + private var item: ItemListReactionItem? + + public var tag: ItemListItemTag? { + return self.item?.tag + } + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.imageNode = ReactionFileImageNode() + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + + self.switchNode = SwitchNode() + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.switchGestureNode = ASDisplayNode() + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.imageNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.switchNode) + self.addSubnode(self.switchGestureNode) + self.addSubnode(self.activateArea) + + self.activateArea.activate = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item, item.enabled else { + return false + } + let value = !strongSelf.switchNode.isOn + strongSelf.switchNode.setOn(value, animated: true) + item.updated(value) + return true + } + } + + override public func didLoad() { + super.didLoad() + + (self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged) + self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func asyncLayout() -> (_ item: ItemListReactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeImageLayout = self.imageNode.asyncLayout() + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.item + var currentDisabledOverlayNode = self.disabledOverlayNode + + return { item, params, neighbors in + var contentSize: CGSize + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + let sideImageInset: CGFloat = 44.0 + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + + var updatedTheme: PresentationTheme? + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + let (imageSize, imageApply) = makeImageLayout(item.context, item.file) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0 - sideImageInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0) + + if !item.enabled { + if currentDisabledOverlayNode == nil { + currentDisabledOverlayNode = ASDisplayNode() + } + } else { + currentDisabledOverlayNode = nil + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + + strongSelf.activateArea.accessibilityLabel = item.title + strongSelf.activateArea.accessibilityValue = item.value ? item.presentationData.strings.VoiceOver_Common_On : item.presentationData.strings.VoiceOver_Common_Off + strongSelf.activateArea.accessibilityHint = item.presentationData.strings.VoiceOver_Common_SwitchHint + var accessibilityTraits = UIAccessibilityTraits() + if item.enabled { + } else { + accessibilityTraits.insert(.notEnabled) + } + strongSelf.activateArea.accessibilityTraits = accessibilityTraits + + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + if let currentDisabledOverlayNode = currentDisabledOverlayNode { + if currentDisabledOverlayNode != strongSelf.disabledOverlayNode { + strongSelf.disabledOverlayNode = currentDisabledOverlayNode + strongSelf.insertSubnode(currentDisabledOverlayNode, belowSubnode: strongSelf.switchGestureNode) + currentDisabledOverlayNode.alpha = 0.0 + transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0) + currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)) + } else { + transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))) + } + currentDisabledOverlayNode.backgroundColor = itemBackgroundColor.withAlphaComponent(0.6) + } else if let disabledOverlayNode = strongSelf.disabledOverlayNode { + transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in + disabledOverlayNode?.removeFromSupernode() + }) + strongSelf.disabledOverlayNode = nil + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + + strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor + strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor + strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor + strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor + strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor + + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + let _ = titleApply() + + let leftInset = 16.0 + params.leftInset + sideImageInset + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 16.0 + params.leftInset + sideImageInset + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + + let imageFitSize = imageSize.aspectFitted(CGSize(width: 30.0, height: 30.0)) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor(sideImageInset - imageFitSize.width), y: floor((contentSize.height - imageFitSize.height) / 2.0)), size: imageFitSize) + imageApply() + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + if let switchView = strongSelf.switchNode.view as? UISwitch { + if strongSelf.switchNode.bounds.size.width.isZero { + switchView.sizeToFit() + } + let switchSize = switchView.bounds.size + + strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize) + strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame + if switchView.isOn != item.value { + switchView.setOn(item.value, animated: animated) + } + switchView.isUserInteractionEnabled = true + } + strongSelf.switchGestureNode.isHidden = item.enabled + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + } + }) + } + } + + override public func accessibilityActivate() -> Bool { + guard let item = self.item else { + return false + } + if !item.enabled { + return false + } + if let switchNode = self.switchNode as? IconSwitchNode { + switchNode.isOn = !switchNode.isOn + item.updated(switchNode.isOn) + } else if let switchNode = self.switchNode as? SwitchNode { + switchNode.isOn = !switchNode.isOn + item.updated(switchNode.isOn) + } + return true + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc private func switchValueChanged(_ switchView: UISwitch) { + if let item = self.item { + let value = switchView.isOn + item.updated(value) + } + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state { + if item.enabled { + let value = switchView.isOn + item.updated(!value) + } + } + } +} diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift new file mode 100644 index 0000000000..93807265cb --- /dev/null +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -0,0 +1,317 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import PresentationDataUtils + +private final class PeerAllowedReactionListControllerArguments { + let context: AccountContext + let toggleAll: () -> Void + let toggleItem: (String) -> Void + + init( + context: AccountContext, + toggleAll: @escaping () -> Void, + toggleItem: @escaping (String) -> Void + ) { + self.context = context + self.toggleAll = toggleAll + self.toggleItem = toggleItem + } +} + +private enum PeerAllowedReactionListControllerSection: Int32 { + case all + case items +} + +private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { + enum StableId: Hashable { + case allowAll + case allowAllInfo + case itemsHeader + case item(String) + } + + case allowAll(text: String, isEnabled: Bool) + case allowAllInfo(String) + + case itemsHeader(String) + case item(index: Int, value: String, file: TelegramMediaFile?, text: String, isEnabled: Bool) + + var section: ItemListSectionId { + switch self { + case .allowAll, .allowAllInfo: + return PeerAllowedReactionListControllerSection.all.rawValue + case .itemsHeader, .item: + return PeerAllowedReactionListControllerSection.items.rawValue + } + } + + var stableId: StableId { + switch self { + case .allowAll: + return .allowAll + case .allowAllInfo: + return .allowAllInfo + case .itemsHeader: + return .itemsHeader + case let .item(_, value, _, _, _): + return .item(value) + } + } + + var sortId: Int { + switch self { + case .allowAll: + return 0 + case .allowAllInfo: + return 1 + case .itemsHeader: + return 2 + case let .item(index, _, _, _, _): + return 100 + index + } + } + + static func ==(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool { + switch lhs { + case let .allowAll(text, isEnabled): + if case .allowAll(text, isEnabled) = rhs { + return true + } else { + return false + } + case let .allowAllInfo(text): + if case .allowAllInfo(text) = rhs { + return true + } else { + return false + } + case let .itemsHeader(text): + if case .itemsHeader(text) = rhs { + return true + } else { + return false + } + case let .item(index, value, file, text, isEnabled): + if case .item(index, value, file, text, isEnabled) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! PeerAllowedReactionListControllerArguments + switch self { + case let .allowAll(text, isEnabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: isEnabled, sectionId: self.section, style: .blocks, updated: { _ in + arguments.toggleAll() + }) + case let .allowAllInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .itemsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .item(_, value, file, text, isEnabled): + return ItemListReactionItem( + context: arguments.context, + presentationData: presentationData, + file: file, + title: text, + value: isEnabled, + sectionId: self.section, + style: .blocks, + updated: { _ in + arguments.toggleItem(value) + } + ) + } + } +} + +private struct PeerAllowedReactionListControllerState: Equatable { + var updatedAllowedReactions: Set? = nil +} + +private func peerAllowedReactionListControllerEntries( + presentationData: PresentationData, + availableReactions: AvailableReactions?, + cachedData: CachedPeerData?, + state: PeerAllowedReactionListControllerState +) -> [PeerAllowedReactionListControllerEntry] { + var entries: [PeerAllowedReactionListControllerEntry] = [] + + if let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions { + entries.append(.allowAll(text: "Allow Reactions", isEnabled: !allowedReactions.isEmpty)) + entries.append(.allowAllInfo("Allow subscribers to reacts to channel posts.")) + + entries.append(.itemsHeader("AVAILABLE REACTIONS")) + var index = 0 + for availableReaction in availableReactions.reactions { + entries.append(.item(index: index, value: availableReaction.value, file: availableReaction.staticIcon, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) + index += 1 + } + } + + return entries +} + +public func peerAllowedReactionListController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peerId: PeerId +) -> ViewController { + let statePromise = ValuePromise(PeerAllowedReactionListControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: PeerAllowedReactionListControllerState()) + let updateState: ((PeerAllowedReactionListControllerState) -> PeerAllowedReactionListControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + let _ = dismissImpl + + let actionsDisposable = DisposableSet() + actionsDisposable.add((context.account.postbox.transaction { transaction -> Set? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap(Set.init) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap(Set.init) + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { allowedReactions in + updateState { state in + var state = state + state.updatedAllowedReactions = allowedReactions + return state + } + })) + + let arguments = PeerAllowedReactionListControllerArguments( + context: context, + toggleAll: { + let _ = (context.engine.stickers.availableReactions() + |> take(1) + |> deliverOnMainQueue).start(next: { availableReactions in + guard let availableReactions = availableReactions else { + return + } + updateState { state in + var state = state + if var updatedAllowedReactions = state.updatedAllowedReactions { + if updatedAllowedReactions.isEmpty { + for availableReaction in availableReactions.reactions { + updatedAllowedReactions.insert(availableReaction.value) + } + } else { + updatedAllowedReactions.removeAll() + } + state.updatedAllowedReactions = updatedAllowedReactions + } + return state + } + }) + }, + toggleItem: { reaction in + updateState { state in + var state = state + if var updatedAllowedReactions = state.updatedAllowedReactions { + if updatedAllowedReactions.contains(reaction) { + updatedAllowedReactions.remove(reaction) + } else { + updatedAllowedReactions.insert(reaction) + } + state.updatedAllowedReactions = updatedAllowedReactions + } + return state + } + } + ) + + let peerView = context.account.viewTracker.peerView(peerId) + |> deliverOnMainQueue + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get(), + context.engine.stickers.availableReactions(), + peerView + ) + |> deliverOnMainQueue + |> map { presentationData, state, availableReactions, peerView -> (ItemListControllerState, (ItemListNodeState, Any)) in + //TODO:localize + let title: String = "Reactions" + + let entries = peerAllowedReactionListControllerEntries( + presentationData: presentationData, + availableReactions: availableReactions, + cachedData: peerView.cachedData, + state: state + ) + + let controllerState = ItemListControllerState( + presentationData: ItemListPresentationData(presentationData), + title: .text(title), + leftNavigationButton: nil, + rightNavigationButton: nil, + backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), + animateChanges: false + ) + let listState = ItemListNodeState( + presentationData: ItemListPresentationData(presentationData), + entries: entries, + style: .blocks, + animateChanges: true + ) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.willDisappear = { _ in + let _ = (context.account.postbox.transaction { transaction -> Set? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap(Set.init) + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap(Set.init) + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { initialAllowedReactions in + let updatedAllowedReactions = stateValue.with({ $0 }).updatedAllowedReactions + if let updatedAllowedReactions = updatedAllowedReactions, initialAllowedReactions != updatedAllowedReactions { + let _ = context.engine.peers.updatePeerAllowedReactions(peerId: peerId, allowedReactions: Array(updatedAllowedReactions)).start() + } + }) + } + dismissImpl = { [weak controller] in + guard let controller = controller else { + return + } + controller.dismiss() + } + + return controller +} diff --git a/submodules/Postbox/Sources/Coding.swift b/submodules/Postbox/Sources/Coding.swift index 9128c80edd..2a2252e00a 100644 --- a/submodules/Postbox/Sources/Coding.swift +++ b/submodules/Postbox/Sources/Coding.swift @@ -1311,6 +1311,14 @@ public final class PostboxDecoder { return [] } } + + public func decodeOptionalStringArrayForKey(_ key: String) -> [String]? { + if PostboxDecoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .StringArray) { + return decodeStringArrayRaw() + } else { + return nil + } + } public func decodeStringArrayRaw() -> [String] { var length: Int32 = 0 diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 37bf1907ee..7168e62eb0 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -460,3 +460,40 @@ public final class EngineMessageReactionListContext { } } } + +public enum UpdatePeerAllowedReactionsError { + case generic +} + +func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allowedReactions: [String]) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(UpdatePeerAllowedReactionsError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + return account.network.request(Api.functions.messages.setChatAvailableReactions(peer: inputPeer, availableReactions: allowedReactions)) + |> mapError { _ -> UpdatePeerAllowedReactionsError in + return .generic + } + |> mapToSignal { result -> Signal in + account.stateManager.addUpdates(result) + + return account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in + if let current = current as? CachedChannelData { + return current.withUpdatedAllowedReactions(allowedReactions) + } else if let current = current as? CachedGroupData { + return current.withUpdatedAllowedReactions(allowedReactions) + } else { + return current + } + }) + } + |> ignoreValues + |> castError(UpdatePeerAllowedReactionsError.self) + } + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index afd4930def..3422a04b51 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -228,6 +228,7 @@ public final class CachedChannelData: CachedPeerData { public let themeEmoticon: String? public let inviteRequestsPending: Int32? public let sendAsPeerId: PeerId? + public let allowedReactions: [String]? public let peerIds: Set public let messageIds: Set @@ -265,9 +266,39 @@ public final class CachedChannelData: CachedPeerData { self.themeEmoticon = nil self.inviteRequestsPending = nil self.sendAsPeerId = nil + self.allowedReactions = nil } - public init(isNotAccessible: Bool, flags: CachedChannelFlags, about: String?, participantsSummary: CachedChannelParticipantsSummary, exportedInvitation: ExportedInvitation?, botInfos: [CachedPeerBotInfo], peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, stickerPack: StickerPackCollectionInfo?, minAvailableMessageId: MessageId?, migrationReference: ChannelMigrationReference?, linkedDiscussionPeerId: LinkedDiscussionPeerId, peerGeoLocation: PeerGeoLocation?, slowModeTimeout: Int32?, slowModeValidUntilTimestamp: Int32?, hasScheduledMessages: Bool, statsDatacenterId: Int32, invitedBy: PeerId?, invitedOn: Int32?, photo: TelegramMediaImage?, activeCall: ActiveCall?, callJoinPeerId: PeerId?, autoremoveTimeout: CachedPeerAutoremoveTimeout, pendingSuggestions: [String], themeEmoticon: String?, inviteRequestsPending: Int32?, sendAsPeerId: PeerId?) { + public init( + isNotAccessible: Bool, + flags: CachedChannelFlags, + about: String?, + participantsSummary: CachedChannelParticipantsSummary, + exportedInvitation: ExportedInvitation?, + botInfos: [CachedPeerBotInfo], + peerStatusSettings: PeerStatusSettings?, + pinnedMessageId: MessageId?, + stickerPack: StickerPackCollectionInfo?, + minAvailableMessageId: MessageId?, + migrationReference: ChannelMigrationReference?, + linkedDiscussionPeerId: LinkedDiscussionPeerId, + peerGeoLocation: PeerGeoLocation?, + slowModeTimeout: Int32?, + slowModeValidUntilTimestamp: Int32?, + hasScheduledMessages: Bool, + statsDatacenterId: Int32, + invitedBy: PeerId?, + invitedOn: Int32?, + photo: TelegramMediaImage?, + activeCall: ActiveCall?, + callJoinPeerId: PeerId?, + autoremoveTimeout: CachedPeerAutoremoveTimeout, + pendingSuggestions: [String], + themeEmoticon: String?, + inviteRequestsPending: Int32?, + sendAsPeerId: PeerId?, + allowedReactions: [String]? + ) { self.isNotAccessible = isNotAccessible self.flags = flags self.about = about @@ -295,6 +326,7 @@ public final class CachedChannelData: CachedPeerData { self.themeEmoticon = themeEmoticon self.inviteRequestsPending = inviteRequestsPending self.sendAsPeerId = sendAsPeerId + self.allowedReactions = allowedReactions var peerIds = Set() for botInfo in botInfos { @@ -322,111 +354,115 @@ public final class CachedChannelData: CachedPeerData { } public func withUpdatedIsNotAccessible(_ isNotAccessible: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedFlags(_ flags: CachedChannelFlags) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedAbout(_ about: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedParticipantsSummary(_ participantsSummary: CachedChannelParticipantsSummary) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedStickerPack(_ stickerPack: StickerPackCollectionInfo?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedMinAvailableMessageId(_ minAvailableMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedMigrationReference(_ migrationReference: ChannelMigrationReference?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedLinkedDiscussionPeerId(_ linkedDiscussionPeerId: LinkedDiscussionPeerId) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedSlowModeTimeout(_ slowModeTimeout: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedSlowModeValidUntilTimestamp(_ slowModeValidUntilTimestamp: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedStatsDatacenterId(_ statsDatacenterId: Int32) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedInvitedBy(_ invitedBy: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedInvitedOn(_ invitedOn: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedActiveCall(_ activeCall: ActiveCall?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedPendingSuggestions(_ pendingSuggestions: [String]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedInviteRequestsPending(_ inviteRequestsPending: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) } public func withUpdatedSendAsPeerId(_ sendAsPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions) + } + + public func withUpdatedAllowedReactions(_ allowedReactions: [String]?) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions) } public init(decoder: PostboxDecoder) { @@ -516,6 +552,8 @@ public final class CachedChannelData: CachedPeerData { self.sendAsPeerId = decoder.decodeOptionalInt64ForKey("sendAsPeerId").flatMap(PeerId.init) + self.allowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") + if case let .known(linkedDiscussionPeerIdValue) = self.linkedDiscussionPeerId { if let linkedDiscussionPeerIdValue = linkedDiscussionPeerIdValue { peerIds.insert(linkedDiscussionPeerIdValue) @@ -660,6 +698,12 @@ public final class CachedChannelData: CachedPeerData { } else { encoder.encodeNil(forKey: "sendAsPeerId") } + + if let allowedReactions = self.allowedReactions { + encoder.encodeStringArray(allowedReactions, forKey: "allowedReactions") + } else { + encoder.encodeNil(forKey: "allowedReactions") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -775,6 +819,10 @@ public final class CachedChannelData: CachedPeerData { return false } + if other.allowedReactions != self.allowedReactions { + return false + } + return true } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift index 96b1cfbee2..5f556539a6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift @@ -55,6 +55,7 @@ public final class CachedGroupData: CachedPeerData { public let callJoinPeerId: PeerId? public let themeEmoticon: String? public let inviteRequestsPending: Int32? + public let allowedReactions: [String]? public let peerIds: Set public let messageIds: Set @@ -78,9 +79,27 @@ public final class CachedGroupData: CachedPeerData { self.callJoinPeerId = nil self.themeEmoticon = nil self.inviteRequestsPending = nil + self.allowedReactions = nil } - public init(participants: CachedGroupParticipants?, exportedInvitation: ExportedInvitation?, botInfos: [CachedPeerBotInfo], peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, about: String?, flags: CachedGroupFlags, hasScheduledMessages: Bool, invitedBy: PeerId?, photo: TelegramMediaImage?, activeCall: CachedChannelData.ActiveCall?, autoremoveTimeout: CachedPeerAutoremoveTimeout, callJoinPeerId: PeerId?, themeEmoticon: String?, inviteRequestsPending: Int32?) { + public init( + participants: CachedGroupParticipants?, + exportedInvitation: ExportedInvitation?, + botInfos: [CachedPeerBotInfo], + peerStatusSettings: PeerStatusSettings?, + pinnedMessageId: MessageId?, + about: String?, + flags: CachedGroupFlags, + hasScheduledMessages: Bool, + invitedBy: PeerId?, + photo: TelegramMediaImage?, + activeCall: CachedChannelData.ActiveCall?, + autoremoveTimeout: CachedPeerAutoremoveTimeout, + callJoinPeerId: PeerId?, + themeEmoticon: String?, + inviteRequestsPending: Int32?, + allowedReactions: [String]? + ) { self.participants = participants self.exportedInvitation = exportedInvitation self.botInfos = botInfos @@ -96,6 +115,7 @@ public final class CachedGroupData: CachedPeerData { self.callJoinPeerId = callJoinPeerId self.themeEmoticon = themeEmoticon self.inviteRequestsPending = inviteRequestsPending + self.allowedReactions = allowedReactions var messageIds = Set() if let pinnedMessageId = self.pinnedMessageId { @@ -160,6 +180,8 @@ public final class CachedGroupData: CachedPeerData { self.inviteRequestsPending = decoder.decodeOptionalInt32ForKey("irp") + self.allowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") + var messageIds = Set() if let pinnedMessageId = self.pinnedMessageId { messageIds.insert(pinnedMessageId) @@ -249,6 +271,12 @@ public final class CachedGroupData: CachedPeerData { } else { encoder.encodeNil(forKey: "irp") } + + if let allowedReactions = self.allowedReactions { + encoder.encodeStringArray(allowedReactions, forKey: "allowedReactions") + } else { + encoder.encodeNil(forKey: "allowedReactions") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -264,66 +292,74 @@ public final class CachedGroupData: CachedPeerData { return false } + if self.allowedReactions != other.allowedReactions { + return false + } + return self.participants == other.participants && self.exportedInvitation == other.exportedInvitation && self.botInfos == other.botInfos && self.peerStatusSettings == other.peerStatusSettings && self.pinnedMessageId == other.pinnedMessageId && self.about == other.about && self.flags == other.flags && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.invitedBy == other.invitedBy && self.themeEmoticon == other.themeEmoticon && self.inviteRequestsPending == other.inviteRequestsPending } public func withUpdatedParticipants(_ participants: CachedGroupParticipants?) -> CachedGroupData { - return CachedGroupData(participants: participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedAbout(_ about: String?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedFlags(_ flags: CachedGroupFlags) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedInvitedBy(_ invitedBy: PeerId?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedActiveCall(_ activeCall: CachedChannelData.ActiveCall?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: self.allowedReactions) } public func withUpdatedInviteRequestsPending(_ inviteRequestsPending: Int32?) -> CachedGroupData { - return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending) + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, allowedReactions: self.allowedReactions) + } + + public func withUpdatedAllowedReactions(_ allowedReactions: [String]?) -> CachedGroupData { + return CachedGroupData(participants: self.participants, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, about: self.about, flags: self.flags, hasScheduledMessages: self.hasScheduledMessages, invitedBy: self.invitedBy, photo: self.photo, activeCall: self.activeCall, autoremoveTimeout: self.autoremoveTimeout, callJoinPeerId: self.callJoinPeerId, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, allowedReactions: allowedReactions) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index d62a62be31..80d83c7b0f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -668,6 +668,10 @@ public extension TelegramEngine { public func updatePeerSendAsPeer(peerId: PeerId, sendAs: PeerId) -> Signal { return _internal_updatePeerSendAsPeer(account: self.account, peerId: peerId, sendAs: sendAs) } + + public func updatePeerAllowedReactions(peerId: PeerId, allowedReactions: [String]) -> Signal { + return _internal_updatePeerAllowedReactions(account: account, peerId: peerId, allowedReactions: allowedReactions) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 2d63b65899..508e71d359 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -274,7 +274,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } switch fullChat { - case let .chatFull(chatFullFlags, _, chatFullAbout, chatFullParticipants, chatFullChatPhoto, _, chatFullExportedInvite, chatFullBotInfo, chatFullPinnedMsgId, _, chatFullCall, _, chatFullGroupcallDefaultJoinAs, chatFullThemeEmoticon, chatFullRequestsPending, _, _): + case let .chatFull(chatFullFlags, _, chatFullAbout, chatFullParticipants, chatFullChatPhoto, _, chatFullExportedInvite, chatFullBotInfo, chatFullPinnedMsgId, _, chatFullCall, _, chatFullGroupcallDefaultJoinAs, chatFullThemeEmoticon, chatFullRequestsPending, _, allowedReactions): var botInfos: [CachedPeerBotInfo] = [] for botInfo in chatFullBotInfo ?? [] { switch botInfo { @@ -366,6 +366,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedCallJoinPeerId(groupCallDefaultJoinAs?.peerId) .withUpdatedThemeEmoticon(chatFullThemeEmoticon) .withUpdatedInviteRequestsPending(chatFullRequestsPending) + .withUpdatedAllowedReactions(allowedReactions ?? []) }) case .channelFull: break @@ -403,7 +404,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } switch fullChat { - case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, _): + case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, allowedReactions): var channelFlags = CachedChannelFlags() if (flags & (1 << 3)) != 0 { channelFlags.insert(.canDisplayParticipants) @@ -596,6 +597,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedThemeEmoticon(themeEmoticon) .withUpdatedInviteRequestsPending(requestsPending) .withUpdatedSendAsPeerId(sendAsPeerId) + .withUpdatedAllowedReactions(allowedReactions ?? []) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 981e8f9f82..781c2a4f4d 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -173,6 +173,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit animateMessageColors: animateBubbleColors, message: chat.message.withUpdated( incoming: chat.message.incoming.withUpdated( + bubble: chat.message.outgoing.bubble.withUpdated( + withWallpaper: chat.message.incoming.bubble.withWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: chat.message.incoming.bubble.withoutWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), linkTextColor: accentColor, linkHighlightColor: accentColor?.withAlphaComponent(0.5), accentTextColor: accentColor, @@ -200,12 +214,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleFillColors?.first?.withMultipliedBrightnessBy(1.421), - stroke: .clear + stroke: .clear, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: UIColor(rgb: 0x000000, alpha: 0.0) ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleFillColors?.first?.withMultipliedBrightnessBy(1.421), - stroke: .clear + stroke: .clear, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: UIColor(rgb: 0x000000, alpha: 0.0) ) ), primaryTextColor: outgoingPrimaryTextColor, @@ -223,6 +245,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit fileDurationColor: outgoingSecondaryTextColor, polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPrimaryTextColor, radioProgress: outgoingPrimaryTextColor, highlight: outgoingPrimaryTextColor?.withAlphaComponent(0.12), separator: outgoingSecondaryTextColor, bar: outgoingPrimaryTextColor) ), + freeform: chat.message.freeform.withUpdated( + withWallpaper: chat.message.freeform.withWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: chat.message.freeform.withoutWallpaper.withUpdated( + reactionInactiveBackground: chat.message.incoming.bubble.withoutWallpaper.fill.last, + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), infoLinkTextColor: accentColor, outgoingCheckColor: outgoingCheckColor, selectionControlColors: chat.message.selectionControlColors.withUpdated(fillColor: accentColor, foregroundColor: badgeTextColor) @@ -441,9 +477,78 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati let incomingBubbleAlpha: CGFloat = 0.9 let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1f1f1f)], highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1f1f1f)], highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil)), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], + highlightedFill: UIColor(rgb: 0x353539), + stroke: UIColor(rgb: 0x262628), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], + highlightedFill: UIColor(rgb: 0x353539), + stroke: UIColor(rgb: 0x262628), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), + primaryTextColor: UIColor(rgb: 0xffffff), + secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) + ), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], + highlightedFill: UIColor(rgb: 0x61BCF9), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], + highlightedFill: UIColor(rgb: 0x61BCF9), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) + ), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1f1f1f)], + highlightedFill: UIColor(rgb: 0x2a2a2a), + stroke: UIColor(rgb: 0x1f1f1f), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x1f1f1f), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1f1f1f)], + highlightedFill: UIColor(rgb: 0x2a2a2a), + stroke: UIColor(rgb: 0x1f1f1f), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x1f1f1f), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), infoPrimaryTextColor: UIColor(rgb: 0xffffff), infoLinkTextColor: UIColor(rgb: 0xffffff), outgoingCheckColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index c8ea3de526..fe7bcaead4 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -300,7 +300,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme } let incomingFillColor = mainBackgroundColor?.withMultipliedAlpha(0.9) - + chat = chat.withUpdated( defaultWallpaper: defaultWallpaper, animateMessageColors: animateBubbleColors, @@ -310,12 +310,20 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: incomingFillColor.flatMap({ [$0] }), highlightedFill: highlightedIncomingBubbleColor, - stroke: mainBackgroundColor + stroke: mainBackgroundColor, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: incomingFillColor.flatMap({ [$0] }), highlightedFill: highlightedIncomingBubbleColor, - stroke: mainBackgroundColor + stroke: mainBackgroundColor, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) ) ), secondaryTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), @@ -699,9 +707,76 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres let incomingBubbleAlpha: CGFloat = 0.9 let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColors, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColors[0], shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColors, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColors[0], shadow: nil)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColors[0], pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [mainBackgroundColor], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [mainBackgroundColor], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0) + ) + ), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor + ), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: outgoingBubbleFillColors, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: outgoingBubbleFillColors[0], + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: outgoingBubbleFillColors, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: outgoingBubbleFillColors[0], + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColors[0], pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white + ), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [mainBackgroundColor], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: incomingFillColor.withAlphaComponent(incomingBubbleAlpha), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [mainBackgroundColor], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: incomingFillColor.withAlphaComponent(incomingBubbleAlpha), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), infoPrimaryTextColor: UIColor(rgb: 0xffffff), infoLinkTextColor: accentColor, outgoingCheckColor: outgoingCheckColor, diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 02a483e189..67edfdc8a3 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -15,6 +15,18 @@ public func selectDateFillStaticColor(theme: PresentationTheme, wallpaper: Teleg } } +public func selectReactionFillStaticColor(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIColor { + if case .color = wallpaper { + return theme.chat.message.freeform.withoutWallpaper.reactionInactiveBackground + } else if theme.overallDarkAppearance { + return theme.chat.message.freeform.withoutWallpaper.reactionInactiveBackground + } else if case .builtin = wallpaper { + return UIColor(rgb: 0x748391, alpha: 0.45) + } else { + return theme.chat.serviceMessage.components.withCustomWallpaper.dateFillStatic + } +} + public func dateFillNeedsBlur(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> Bool { if case .builtin = wallpaper { return false @@ -526,7 +538,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let message = PresentationThemeChatMessage( incoming: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), @@ -548,7 +581,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.2), textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe1ffc7)], + highlightedFill: UIColor(rgb: 0xc8ffa6), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x2DA32F).withMultipliedAlpha(0.12), + reactionInactiveForeground: UIColor(rgb: 0x2DA32F), + reactionActiveBackground: UIColor(rgb: 0x2DA32F), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe1ffc7)], + highlightedFill: UIColor(rgb: 0xc8ffa6), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x2DA32F).withMultipliedAlpha(0.12), + reactionInactiveForeground: UIColor(rgb: 0x2DA32F), + reactionActiveBackground: UIColor(rgb: 0x2DA32F), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), linkTextColor: UIColor(rgb: 0x004bad), @@ -572,7 +626,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xbbde9f), textSelectionKnobColor: UIColor(rgb: 0x3fc33b)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil)), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), + reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), + reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1) + ) + ), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), outgoingCheckColor: UIColor(rgb: 0x19c700), @@ -591,7 +666,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let messageDay = PresentationThemeChatMessage( incoming: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xffffff), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xf1f1f4)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xf1f1f4), shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xffffff), + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xf1f1f4)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xf1f1f4), + shadow: nil, + reactionInactiveBackground: .clear, + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), @@ -616,7 +712,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.3), textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], + highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], + highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65), linkTextColor: UIColor(rgb: 0xffffff), @@ -640,7 +757,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil)), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe5e5ea)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xe5e5ea), + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe5e5ea)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xe5e5ea), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xF1F0F5), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), outgoingCheckColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index c0d0a57389..d0ea881704 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -639,16 +639,50 @@ public final class PresentationThemeBubbleColorComponents { public let highlightedFill: UIColor public let stroke: UIColor public let shadow: PresentationThemeBubbleShadow? + public let reactionInactiveBackground: UIColor + public let reactionInactiveForeground: UIColor + public let reactionActiveBackground: UIColor + public let reactionActiveForeground: UIColor - public init(fill: [UIColor], highlightedFill: UIColor, stroke: UIColor, shadow: PresentationThemeBubbleShadow?) { + public init( + fill: [UIColor], + highlightedFill: UIColor, + stroke: UIColor, + shadow: PresentationThemeBubbleShadow?, + reactionInactiveBackground: UIColor, + reactionInactiveForeground: UIColor, + reactionActiveBackground: UIColor, + reactionActiveForeground: UIColor + ) { self.fill = fill self.highlightedFill = highlightedFill self.stroke = stroke self.shadow = shadow + self.reactionInactiveBackground = reactionInactiveBackground + self.reactionInactiveForeground = reactionInactiveForeground + self.reactionActiveBackground = reactionActiveBackground + self.reactionActiveForeground = reactionActiveForeground } - public func withUpdated(fill: [UIColor]? = nil, highlightedFill: UIColor? = nil, stroke: UIColor? = nil) -> PresentationThemeBubbleColorComponents { - return PresentationThemeBubbleColorComponents(fill: fill ?? self.fill, highlightedFill: highlightedFill ?? self.highlightedFill, stroke: stroke ?? self.stroke, shadow: self.shadow) + public func withUpdated( + fill: [UIColor]? = nil, + highlightedFill: UIColor? = nil, + stroke: UIColor? = nil, + reactionInactiveBackground: UIColor? = nil, + reactionInactiveForeground: UIColor? = nil, + reactionActiveBackground: UIColor? = nil, + reactionActiveForeground: UIColor? = nil + ) -> PresentationThemeBubbleColorComponents { + return PresentationThemeBubbleColorComponents( + fill: fill ?? self.fill, + highlightedFill: highlightedFill ?? self.highlightedFill, + stroke: stroke ?? self.stroke, + shadow: self.shadow, + reactionInactiveBackground: reactionInactiveBackground ?? self.reactionInactiveBackground, + reactionInactiveForeground: reactionInactiveForeground ?? self.reactionInactiveForeground, + reactionActiveBackground: reactionActiveBackground ?? self.reactionActiveBackground, + reactionActiveForeground: reactionActiveForeground ?? self.reactionActiveForeground + ) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index e5f27ae7d9..3105038c81 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -1097,6 +1097,10 @@ extension PresentationThemeBubbleColorComponents: Codable { case stroke case shadow case bgList + case reactionInactiveBg + case reactionInactiveFg + case reactionActiveBg + case reactionActiveFg } public convenience init(from decoder: Decoder) throws { @@ -1122,7 +1126,11 @@ extension PresentationThemeBubbleColorComponents: Codable { fill: fill, highlightedFill: try decodeColor(values, .highlightedBg), stroke: try decodeColor(values, .stroke), - shadow: try? values.decode(PresentationThemeBubbleShadow.self, forKey: .shadow) + shadow: try? values.decode(PresentationThemeBubbleShadow.self, forKey: .shadow), + reactionInactiveBackground: try decodeColor(values, .reactionInactiveBg), + reactionInactiveForeground: try decodeColor(values, .reactionInactiveFg), + reactionActiveBackground: try decodeColor(values, .reactionActiveBg), + reactionActiveForeground: try decodeColor(values, .reactionActiveFg) ) } @@ -1141,6 +1149,10 @@ extension PresentationThemeBubbleColorComponents: Codable { } try encodeColor(&values, self.highlightedFill, .highlightedBg) try encodeColor(&values, self.stroke, .stroke) + try encodeColor(&values, self.reactionInactiveBackground, .reactionInactiveBg) + try encodeColor(&values, self.reactionInactiveForeground, .reactionInactiveFg) + try encodeColor(&values, self.reactionActiveBackground, .reactionActiveBg) + try encodeColor(&values, self.reactionActiveForeground, .reactionActiveFg) } } diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json new file mode 100644 index 0000000000..d2272e6102 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "reactions_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf new file mode 100644 index 0000000000..1994256b47 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf @@ -0,0 +1,180 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 0.176471 0.333333 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.000000 5.523438 cm +1.000000 1.000000 1.000000 scn +7.132812 18.054688 m +7.234375 18.054688 7.312500 18.117188 7.335938 18.226562 c +7.523438 19.359375 7.523438 19.359375 8.664062 19.570312 c +8.781250 19.593750 8.851562 19.664062 8.851562 19.765625 c +8.851562 19.875000 8.781250 19.937500 8.671875 19.968750 c +7.515625 20.195312 7.539062 20.195312 7.335938 21.304688 c +7.312500 21.414062 7.242188 21.476562 7.132812 21.476562 c +7.031250 21.476562 6.953125 21.406250 6.937500 21.304688 c +6.726562 20.179688 6.750000 20.171875 5.601562 19.968750 c +5.484375 19.945312 5.414062 19.867188 5.414062 19.765625 c +5.414062 19.671875 5.484375 19.593750 5.593750 19.570312 c +6.750000 19.343750 6.742188 19.351562 6.937500 18.226562 c +6.953125 18.125000 7.031250 18.054688 7.132812 18.054688 c +h +12.695312 15.976562 m +12.843750 15.976562 12.960938 16.078125 12.968750 16.242188 c +13.187500 18.023438 13.265625 18.078125 15.085938 18.367188 c +15.265625 18.382812 15.359375 18.484375 15.359375 18.640625 c +15.359375 18.781250 15.265625 18.882812 15.125000 18.906250 c +13.273438 19.273438 13.187500 19.250000 12.968750 21.031250 c +12.960938 21.187500 12.843750 21.289062 12.695312 21.289062 c +12.554688 21.289062 12.445312 21.187500 12.429688 21.039062 c +12.195312 19.218750 12.148438 19.164062 10.281250 18.906250 c +10.140625 18.890625 10.039062 18.781250 10.039062 18.640625 c +10.039062 18.492188 10.140625 18.390625 10.281250 18.367188 c +12.148438 17.992188 12.187500 18.007812 12.429688 16.218750 c +12.445312 16.078125 12.554688 15.976562 12.695312 15.976562 c +h +6.531250 2.656250 m +9.039062 0.148438 12.257812 0.359375 14.531250 2.640625 c +16.179688 4.281250 16.617188 6.031250 16.078125 8.109375 c +15.789062 9.585938 14.906250 11.281250 14.250000 12.523438 c +13.867188 13.257812 13.398438 14.210938 13.117188 14.546875 c +12.804688 14.937500 12.320312 14.984375 11.945312 14.671875 c +11.507812 14.320312 11.476562 13.835938 11.726562 13.125000 c +12.640625 10.656250 l +12.726562 10.437500 12.710938 10.304688 12.625000 10.226562 c +12.531250 10.132812 12.414062 10.117188 12.234375 10.289062 c +6.367188 16.164062 l +5.992188 16.539062 5.406250 16.539062 5.031250 16.164062 c +4.664062 15.789062 4.664062 15.203125 5.039062 14.828125 c +9.351562 10.515625 l +9.171875 10.421875 8.976562 10.320312 8.789062 10.195312 c +3.820312 15.164062 l +3.445312 15.539062 2.859375 15.539062 2.484375 15.164062 c +2.109375 14.789062 2.109375 14.210938 2.484375 13.835938 c +7.398438 8.921875 l +7.257812 8.757812 7.125000 8.578125 7.000000 8.398438 c +2.484375 12.914062 l +2.109375 13.289062 1.523438 13.289062 1.148438 12.921875 c +0.773438 12.546875 0.781250 11.960938 1.148438 11.585938 c +6.046875 6.695312 l +5.960938 6.460938 5.898438 6.226562 5.843750 6.007812 c +2.429688 9.414062 l +2.054688 9.789062 1.476562 9.789062 1.101562 9.421875 c +0.726562 9.046875 0.726562 8.460938 1.101562 8.085938 c +6.531250 2.656250 l +h +10.773438 14.898438 m +9.507812 16.164062 l +9.125000 16.539062 8.539062 16.531250 8.171875 16.164062 c +8.148438 16.140625 8.132812 16.125000 8.117188 16.101562 c +10.585938 13.632812 l +10.570312 14.093750 10.625000 14.515625 10.773438 14.898438 c +h +17.664062 2.640625 m +19.312500 4.289062 19.750000 6.031250 19.218750 8.109375 c +18.921875 9.585938 18.046875 11.281250 17.382812 12.523438 c +17.007812 13.257812 16.531250 14.210938 16.250000 14.546875 c +15.937500 14.929688 15.460938 14.976562 15.078125 14.671875 c +14.906250 14.539062 14.789062 14.375000 14.734375 14.195312 c +14.953125 13.781250 15.171875 13.359375 15.390625 12.945312 c +16.023438 11.742188 16.953125 9.937500 17.242188 8.328125 c +17.867188 5.804688 17.257812 3.671875 15.375000 1.796875 c +15.031250 1.453125 14.671875 1.156250 14.304688 0.898438 c +15.507812 1.046875 16.687500 1.648438 17.664062 2.640625 c +h +2.304688 0.000000 m +2.437500 0.000000 2.523438 0.085938 2.539062 0.218750 c +2.804688 1.750000 2.796875 1.773438 4.390625 2.070312 c +4.531250 2.101562 4.617188 2.171875 4.617188 2.312500 c +4.617188 2.445312 4.531250 2.523438 4.398438 2.546875 c +2.796875 2.875000 2.820312 2.890625 2.539062 4.398438 c +2.523438 4.531250 2.437500 4.617188 2.304688 4.617188 c +2.171875 4.617188 2.093750 4.531250 2.062500 4.398438 c +1.781250 2.867188 1.820312 2.843750 0.218750 2.546875 c +0.085938 2.523438 0.000000 2.445312 0.000000 2.312500 c +0.000000 2.171875 0.078125 2.101562 0.210938 2.070312 c +1.820312 1.750000 1.796875 1.742188 2.062500 0.218750 c +2.093750 0.085938 2.171875 0.000000 2.304688 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 5442 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000005532 00000 n +0000005555 00000 n +0000005728 00000 n +0000005802 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5861 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d6da13ca51..bfa1038a69 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -67,6 +67,7 @@ import ChatListUI import CalendarMessageScreen import ReactionSelectionNode import LottieMeshSwift +import ReactionListContextMenuContent #if DEBUG import os.signpost @@ -953,8 +954,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.availableReactions(), + peerAllowedReactions(context: strongSelf.context, peerId: topMessage.id.peerId), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) - ).start(next: { actions, availableReactions, chatTextSelectionTips in + ).start(next: { actions, availableReactions, allowedReactions, chatTextSelectionTips in var actions = actions guard let strongSelf = self else { @@ -1004,8 +1006,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actions.context = strongSelf.context - if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions { - for reaction in availableReactions.reactions { + if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions { + filterReactions: for reaction in availableReactions.reactions { + switch allowedReactions { + case let .set(set): + if !set.contains(reaction.value) { + continue filterReactions + } + case .all: + break + } actions.reactionItems.append(ReactionContextItem( reaction: ReactionContextItem.Reaction(rawValue: reaction.value), stillAnimation: reaction.selectAnimation, @@ -1083,6 +1093,45 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(controller) }) } + }, openMessageReactionContextMenu: { [weak self] message, sourceNode, gesture, value in + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.context.engine.stickers.availableReactions() + |> deliverOnMainQueue).start(next: { availableReactions in + guard let strongSelf = self else { + return + } + + var dismissController: ((@escaping () -> Void) -> Void)? + + let items = ContextController.Items(content: .custom(ReactionListContextMenuContent(context: strongSelf.context, availableReactions: availableReactions, message: EngineMessage(message), reaction: value, back: nil, openPeer: { id in + dismissController?({ + guard let strongSelf = self else { + return + } + + strongSelf.openPeer(peerId: id, navigation: .default, fromMessage: message) + }) + }))) + + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, contentNode: sourceNode)), items: .single(items), recognizer: nil, gesture: gesture) + + dismissController = { [weak controller] completion in + controller?.dismiss(completion: { + completion() + }) + } + + strongSelf.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + strongSelf.window?.presentInGlobalOverlay(controller) + }) }, updateMessageReaction: { [weak self] initialMessage, reaction in guard let strongSelf = self else { return @@ -1093,115 +1142,140 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let message = messages.first else { return } - if !canAddMessageReactions(message: message) { - return - } - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { - return - } - guard item.message.id == message.id else { + let _ = (peerAllowedReactions(context: strongSelf.context, peerId: message.id.peerId) + |> deliverOnMainQueue).start(next: { allowedReactions in + guard let strongSelf = self else { return } - var updatedReaction: String? - switch reaction { - case .default: - updatedReaction = item.associatedData.defaultReaction - case let .reaction(value): - updatedReaction = value - } + let _ = allowedReactions - var removedReaction: String? - - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - for listReaction in attribute.reactions { - switch reaction { - case .default: - if listReaction.isSelected { - updatedReaction = nil - removedReaction = listReaction.value - } - case let .reaction(value): - if listReaction.value == value && listReaction.isSelected { - updatedReaction = nil - removedReaction = value - } - } - } - } else if let attribute = attribute as? PendingReactionsMessageAttribute { - if attribute.value != nil { - switch reaction { - case .default: - updatedReaction = nil - removedReaction = attribute.value - case let .reaction(value): - if attribute.value == value { - updatedReaction = nil - removedReaction = value - } - } - } + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { + return } - } - - if let updatedReaction = updatedReaction { - if strongSelf.selectPollOptionFeedback == nil { - strongSelf.selectPollOptionFeedback = HapticFeedback() + guard item.message.id == message.id else { + return } - strongSelf.selectPollOptionFeedback?.tap() - itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in - guard let strongSelf = self else { + if !canAddMessageReactions(message: message) { + itemNode.openMessageContextMenu() + return + } + + var updatedReaction: String? + switch reaction { + case .default: + updatedReaction = item.associatedData.defaultReaction + case let .reaction(value): + updatedReaction = value + } + + var removedReaction: String? + + for attribute in message.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + for listReaction in attribute.reactions { + switch reaction { + case .default: + if listReaction.isSelected { + updatedReaction = nil + removedReaction = listReaction.value + } + case let .reaction(value): + if listReaction.value == value && listReaction.isSelected { + updatedReaction = nil + removedReaction = value + } + } + } + } else if let attribute = attribute as? PendingReactionsMessageAttribute { + if attribute.value != nil { + switch reaction { + case .default: + updatedReaction = nil + removedReaction = attribute.value + case let .reaction(value): + if attribute.value == value { + updatedReaction = nil + removedReaction = value + } + } + } + } + } + + if let updatedReaction = updatedReaction { + guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() return } - if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { - for reaction in availableReactions.reactions { - if reaction.value == updatedReaction { - let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: reaction.value), - stillAnimation: reaction.selectAnimation, - listAnimation: reaction.activateAnimation, - applicationAnimation: reaction.effectAnimation - )) - - strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation - strongSelf.currentStandaloneReactionItemNode = itemNode - - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - }) - + switch allowedReactions { + case let .set(set): + if !set.contains(updatedReaction) { + itemNode.openMessageContextMenu() + return + } + case .all: + break + } + + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.tap() + + itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in + guard let strongSelf = self else { + return + } + if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { + for reaction in availableReactions.reactions { + if reaction.value == updatedReaction { + let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + )) + + strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation + strongSelf.currentStandaloneReactionItemNode = itemNode + + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + }) + + break + } + } + } + }) + } else if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { + var hideRemovedReaction: Bool = false + if let reactions = mergedMessageReactions(attributes: message.attributes) { + for reaction in reactions.reactions { + if reaction.value == removedReaction { + hideRemovedReaction = reaction.count == 1 break } } } - }) - } else if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { - var hideRemovedReaction: Bool = false - if let reactions = mergedMessageReactions(attributes: message.attributes) { - for reaction in reactions.reactions { - if reaction.value == removedReaction { - hideRemovedReaction = reaction.count == 1 - break - } - } + + let standaloneDismissAnimation = StandaloneDismissReactionAnimation() + standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) + standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in + standaloneDismissAnimation?.removeFromSupernode() + }) } - let standaloneDismissAnimation = StandaloneDismissReactionAnimation() - standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) - standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in - standaloneDismissAnimation?.removeFromSupernode() - }) + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, 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 { return @@ -14390,3 +14464,23 @@ func canAddMessageReactions(message: Message) -> Bool { } return true } + +enum AllowedReactions { + case set(Set) + case all +} + +func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.transaction { transaction -> AllowedReactions? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap { return AllowedReactions.set(Set($0)) } + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap { return AllowedReactions.set(Set($0)) } + } else if peerId.namespace == Namespaces.Peer.CloudUser { + return .all + } else { + return nil + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 087df11a59..bc03ba8ad5 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -57,6 +57,7 @@ public final class ChatControllerInteraction { let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void + let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingNode, ContextGesture?, String) -> Void let activateMessagePinch: (PinchSourceContainerNode) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void @@ -153,6 +154,7 @@ public final class ChatControllerInteraction { openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, + openMessageReactionContextMenu: @escaping (Message, ContextExtractedContentContainingNode, ContextGesture?, String) -> Void, updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void, activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, @@ -236,6 +238,7 @@ public final class ChatControllerInteraction { self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu + self.openMessageReactionContextMenu = openMessageReactionContextMenu self.updateMessageReaction = updateMessageReaction self.activateMessagePinch = activateMessagePinch self.openMessageContextActions = openMessageContextActions @@ -321,7 +324,8 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { return nil }, chatControllerNode: { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 3da3ea5a96..d9dc7d04ec 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1201,6 +1201,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if group.participantCount <= 50 { hasReadReports = true } + } else { + reactionCount = 0 } var readStats = readStats @@ -1220,7 +1222,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }) } else if !stats.peers.isEmpty || reactionCount != 0 { if reactionCount != 0 { - c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), back: { [weak c] in + c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), reaction: nil, back: { [weak c] in c?.popItems() }, openPeer: { [weak c] id in c?.dismiss(completion: { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index f497e2031c..d734e96750 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -367,6 +367,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) { return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } if false, strongSelf.telegramFile == nil { if let animationNode = strongSelf.animationNode, animationNode.frame.contains(point) { @@ -972,7 +977,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -1414,6 +1420,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame if let (rect, containerSize) = strongSelf.absoluteRect { var rect = rect @@ -2382,6 +2396,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 339ac262f2..27785eca94 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -642,7 +642,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) } let _ = statusSuggestedWidthAndContinue diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 6dcaa1dfe0..de9e6b6496 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -249,11 +249,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) needReactions = false } else if result.last?.1 == ChatMessageCommentFooterContentNode.self { - if result[result.count - 2].1 == ChatMessageTextBubbleContentNode.self { + /*if result[result.count - 2].1 == ChatMessageTextBubbleContentNode.self { } else { result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) needReactions = false - } + }*/ } } } @@ -805,6 +805,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } + if let avatarNode = strongSelf.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) { return .waitForSingleTap } @@ -1596,7 +1602,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) mosaicStatusSizeAndApply = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -2829,6 +2836,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in + guard let strongSelf = strongSelf, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -3848,6 +3863,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return self.mainContextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + let subFrame = self.backgroundNode.frame + item.controllerInteraction.openMessageContextMenu(item.message, true, self, subFrame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 8c6be2f6fe..1869f576fb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -48,6 +48,15 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } override func accessibilityActivate() -> Bool { @@ -217,7 +226,8 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index 79f72e066f..8a37064a6c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -81,3 +81,82 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou return result } } + +final class ChatMessageReactionContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true + + private weak var chatNode: ChatControllerNode? + private let postbox: Postbox + private let message: Message + private let contentNode: ContextExtractedContentContainingNode + + var shouldBeDismissed: Signal { + if self.message.adAttribute != nil { + return .single(false) + } + let viewKey = PostboxViewKey.messages(Set([self.message.id])) + return self.postbox.combinedView(keys: [viewKey]) + |> map { views -> Bool in + guard let view = views.views[viewKey] as? MessagesView else { + return false + } + if view.messages.isEmpty { + return true + } else { + return false + } + } + |> distinctUntilChanged + } + + init(chatNode: ChatControllerNode, postbox: Postbox, message: Message, contentNode: ContextExtractedContentContainingNode) { + self.chatNode = chatNode + self.postbox = postbox + self.message = message + self.contentNode = contentNode + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerTakeViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerTakeViewInfo(contentContainingNode: self.contentNode, contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } + + func putBack() -> ContextControllerPutBackViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerPutBackViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } +} + diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 68d66a57b3..016f17676e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -148,6 +148,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var replyCount: Int var isPinned: Bool var hasAutoremove: Bool + var canViewReactionList: Bool init( context: AccountContext, @@ -163,7 +164,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionPeers: [(String, EnginePeer)], replyCount: Int, isPinned: Bool, - hasAutoremove: Bool + hasAutoremove: Bool, + canViewReactionList: Bool ) { self.context = context self.presentationData = presentationData @@ -179,6 +181,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { self.replyCount = replyCount self.isPinned = isPinned self.hasAutoremove = hasAutoremove + self.canViewReactionList = canViewReactionList } } @@ -220,6 +223,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.dateNode = TextNode() @@ -284,18 +288,26 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let reactionColors: ReactionButtonComponent.Colors switch arguments.type { case .BubbleIncoming, .ImageIncoming, .FreeIncoming: + let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: true, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( - deselectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, - selectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb, - deselectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, - selectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb ) case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing: + let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: false, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( - deselectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, - selectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb, - deselectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, - selectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb ) } @@ -641,6 +653,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { ) case let .trailingContent(contentWidth, reactionSettings): if let reactionSettings = reactionSettings, !reactionSettings.displayInline { + var totalReactionCount: Int = 0 + for reaction in arguments.reactions { + totalReactionCount += Int(reaction.count) + } + reactionButtonsResult = reactionButtonsContainer.update( context: arguments.context, action: { value in @@ -667,11 +684,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { peers.append(peer) } } - if peers.count != Int(reaction.count) { + if peers.count != Int(reaction.count) || arguments.reactionPeers.count != totalReactionCount { peers.removeAll() } - return ReactionButtonsLayoutContainer.Reaction( + return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile @@ -778,28 +795,47 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionButtonPosition.y += item.size.height + 6.0 } - if item.view.superview == nil { - strongSelf.view.addSubview(item.view) - item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size) + if item.node.supernode == nil { + strongSelf.addSubnode(item.node) + item.node.frame = CGRect(origin: reactionButtonPosition, size: item.size) if animation.isAnimated { - item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + item.node.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + item.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + item.node.isGestureEnabled = true + let itemValue = item.value + let itemNode = item.node + item.node.isGestureEnabled = arguments.canViewReactionList + item.node.activated = { [weak itemNode] gesture, _ in + guard let strongSelf = self else { + return + } + guard let itemNode = itemNode else { + return + } + + if let openReactionPreview = strongSelf.openReactionPreview { + openReactionPreview(gesture, itemNode.containerNode, itemValue) + } else { + gesture.cancel() + } } } else { - animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil) + animation.animator.updateFrame(layer: item.node.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil) } reactionButtonPosition.x += item.size.width + 6.0 } - for view in reactionButtons.removedViews { + for node in reactionButtons.removedNodes { 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() + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() }) } else { - view.removeFromSuperview() + node.removeFromSupernode() } } @@ -1160,7 +1196,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for (_, button) in self.reactionButtonsContainer.buttons { if button.frame.contains(point) { - if let result = button.hitTest(self.view.convert(point, to: button), with: event) { + if let result = button.hitTest(self.view.convert(point, to: button.view), with: event) { return result } } diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 935f6aa1b1..827b30dc56 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -63,6 +63,15 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } override func accessibilityActivate() -> Bool { diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 9c9a19dd9c..d872cd0c39 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -184,6 +184,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return .fail } } + + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } } return .waitForSingleTap } @@ -797,6 +803,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -1312,6 +1326,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.interactiveVideoNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 87cbc47714..260c025ce0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -462,7 +462,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: isPinned && !associatedData.isInPinnedListMode, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index c033782e6a..e0ca23424b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -303,7 +303,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index ecac91cdca..08514b4d60 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -522,7 +522,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio reactionPeers: dateAndStatus.dateReactionPeers, replyCount: dateAndStatus.dateReplies, isPinned: dateAndStatus.isPinned, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) let (size, apply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 43d64f554f..212c3f51ca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -869,6 +869,9 @@ public class ChatMessageItemView: ListViewItemNode { } } + func openMessageContextMenu() { + } + func targetReactionView(value: String) -> UIView? { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 1e0f25f693..eb244603e7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -259,7 +259,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index 8363389824..bcff6c1fc7 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1079,7 +1079,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index d3f853a2b8..41b56929a6 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -13,6 +13,24 @@ import ReactionButtonListComponent import AccountContext import WallpaperBackgroundNode +func canViewMessageReactionList(message: Message) -> Bool { + if let peer = message.peers[message.id.peerId] { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + return false + } else { + return true + } + } else if let _ = peer as? TelegramGroup { + return true + } else { + return false + } + } else { + return false + } +} + final class MessageReactionButtonsNode: ASDisplayNode { enum DisplayType { case incoming @@ -29,7 +47,9 @@ final class MessageReactionButtonsNode: ASDisplayNode { private let container: ReactionButtonsAsyncLayoutContainer private let backgroundMaskView: UIView private var backgroundMaskButtons: [String: UIView] = [:] + var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.container = ReactionButtonsAsyncLayoutContainer() @@ -53,7 +73,46 @@ final class MessageReactionButtonsNode: ASDisplayNode { type: DisplayType ) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) { let reactionColors: ReactionButtonComponent.Colors + let themeColors: PresentationThemeBubbleColorComponents switch type { + case .incoming: + themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + case .outgoing: + themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + case .freeform: + if presentationData.theme.wallpaper.isEmpty { + themeColors = presentationData.theme.theme.chat.message.freeform.withoutWallpaper + } else { + themeColors = presentationData.theme.theme.chat.message.freeform.withWallpaper + } + + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: selectReactionFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + } + + /*switch type { case .incoming: reactionColors = ReactionButtonComponent.Colors( deselectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, @@ -75,6 +134,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { deselectedForeground: UIColor(white: 1.0, alpha: 1.0).argb, selectedForeground: UIColor(white: 0.0, alpha: 0.1).argb ) + }*/ + + var totalReactionCount: Int = 0 + for reaction in reactions.reactions { + totalReactionCount += Int(reaction.count) } let reactionButtonsResult = self.container.update( @@ -109,11 +173,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { } } - if peers.count != Int(reaction.count) { + if peers.count != Int(reaction.count) || totalReactionCount != reactions.recentPeers.count { peers.removeAll() } - return ReactionButtonsLayoutContainer.Reaction( + return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile @@ -244,15 +308,27 @@ final class MessageReactionButtonsNode: ASDisplayNode { strongSelf.backgroundMaskButtons[item.value] = itemMaskView } - if item.view.superview == nil { - strongSelf.view.addSubview(item.view) + if item.node.supernode == nil { + strongSelf.addSubnode(item.node) if animation.isAnimated { - item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + item.node.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + item.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - item.view.frame = itemFrame + item.node.frame = itemFrame + + let itemValue = item.value + let itemNode = item.node + item.node.isGestureEnabled = canViewMessageReactionList(message: message) + item.node.activated = { [weak itemNode] gesture, _ in + guard let strongSelf = self, let itemNode = itemNode else { + gesture.cancel() + return + } + strongSelf.openReactionPreview?(gesture, itemNode.containerNode, itemValue) + } + item.node.additionalActivationProgressLayer = itemMaskView.layer } else { - animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil) + animation.animator.updateFrame(layer: item.node.layer, frame: itemFrame, completion: nil) } if itemMaskView.superview == nil { @@ -285,14 +361,14 @@ final class MessageReactionButtonsNode: ASDisplayNode { strongSelf.backgroundMaskButtons.removeValue(forKey: id) } - for view in reactionButtons.removedViews { + for node in reactionButtons.removedNodes { 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() + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() }) } else { - view.removeFromSuperview() + node.removeFromSupernode() } } }) @@ -349,6 +425,18 @@ final class MessageReactionButtonsNode: ASDisplayNode { animation.animator.updateScale(layer: button.layer, scale: 0.01, completion: nil) } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for (_, button) in self.container.buttons { + if button.frame.contains(point) { + if let result = button.hitTest(self.view.convert(point, to: button.view), with: event) { + return result + } + } + } + + return nil + } } final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode { @@ -367,6 +455,15 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } required init?(coder aDecoder: NSCoder) { @@ -502,6 +599,7 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { private let buttonsNode: MessageReactionButtonsNode var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.buttonsNode = MessageReactionButtonsNode() @@ -509,9 +607,14 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { super.init() self.addSubnode(self.buttonsNode) + self.buttonsNode.reactionSelected = { [weak self] value in self?.reactionSelected?(value) } + + self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + self?.openReactionPreview?(gesture, sourceNode, value) + } } class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) { diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 13df9e7b0c..c7e3cb838a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -127,7 +127,8 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index a35f84bc62..19a03cd5ed 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -173,6 +173,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } + if let item = strongSelf.item, item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { if strongSelf.imageNode.frame.contains(point) { return .waitForDoubleTap @@ -511,7 +517,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -988,6 +995,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -1599,6 +1614,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 2dd980e9bd..5cb4c3be32 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -76,6 +76,15 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } } required init?(coder aDecoder: NSCoder) { @@ -309,7 +318,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index fc705ac0ad..c0f14cb7a1 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -255,6 +255,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 93cac14f9d..e498f2a6d4 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -108,7 +108,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 80644ecb5a..ad27f89381 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -69,6 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f77ed82440..7d6789dbe6 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -464,6 +464,7 @@ private final class PeerInfoInteraction { let openFaq: (String?) -> Void let openAddMember: () -> Void let openQrCode: () -> Void + let editingOpenReactionsSetup: () -> Void init( openUsername: @escaping (String) -> Void, @@ -504,7 +505,8 @@ private final class PeerInfoInteraction { openDeletePeer: @escaping () -> Void, openFaq: @escaping (String?) -> Void, openAddMember: @escaping () -> Void, - openQrCode: @escaping () -> Void + openQrCode: @escaping () -> Void, + editingOpenReactionsSetup: @escaping () -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -545,6 +547,7 @@ private final class PeerInfoInteraction { self.openFaq = openFaq self.openAddMember = openAddMember self.openQrCode = openQrCode + self.editingOpenReactionsSetup = editingOpenReactionsSetup } } @@ -1127,7 +1130,8 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr let ItemDiscussionGroup = 3 let ItemSignMessages = 4 let ItemSignMessagesHelp = 5 - let ItemDeleteChannel = 5 + let ItemDeleteChannel = 6 + let ItemReactions = 7 let isCreator = channel.flags.contains(.isCreator) @@ -1176,6 +1180,23 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr })) } + if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { + //TODO:localize + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { + interaction.editingOpenReactionsSetup() + })) + } + if isCreator || (channel.adminRights != nil && channel.hasPermission(.sendMessages)) { let messagesShouldHaveSignatures: Bool switch channel.info { @@ -1210,6 +1231,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr let ItemLocation = 112 let ItemLocationSetup = 113 let ItemDeleteGroup = 114 + let ItemReactions = 115 let isCreator = channel.flags.contains(.isCreator) let isPublic = channel.username != nil @@ -1283,11 +1305,45 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr })) } + if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { + //TODO:localize + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { + interaction.editingOpenReactionsSetup() + })) + } + if !isPublic, case .known(nil) = cachedData.linkedDiscussionPeerId { items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPreHistory, label: .text(cachedData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden), text: presentationData.strings.GroupInfo_GroupHistoryShort, icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { interaction.editingOpenPreHistorySetup() })) } + } else { + if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { + //TODO:localize + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { + interaction.editingOpenReactionsSetup() + })) + } } if cachedData.flags.contains(.canSetStickerSet) && canEditPeerInfo(context: context, peer: channel) { @@ -1353,6 +1409,7 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr let ItemPermissions = 104 let ItemAdmins = 105 let ItemMemberRequests = 106 + let ItemReactions = 107 var canViewAdminsAndBanned = false @@ -1382,6 +1439,23 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr interaction.editingOpenPreHistorySetup() })) + do { + //TODO:localize + let label: String + if let cachedData = data.cachedData as? CachedGroupData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { + interaction.editingOpenReactionsSetup() + })) + } + canViewAdminsAndBanned = true } else if case let .admin(rights, _) = group.role { if rights.rights.contains(.canInviteUsers) { @@ -1677,6 +1751,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }, openQrCode: { [weak self] in self?.openQrCode() + }, + editingOpenReactionsSetup: { [weak self] in + self?.editingOpenReactionsSetup() } ) @@ -1846,6 +1923,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(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { [weak self] message, node, rect, gesture in @@ -4698,6 +4776,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) } + private func editingOpenReactionsSetup() { + guard let data = self.data, let peer = data.peer else { + return + } + self.controller?.push(peerAllowedReactionListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) + } + private func editingToggleMessageSignatures(value: Bool) { self.toggleShouldChannelMessagesSignaturesDisposable.set(self.context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: self.peerId, enabled: value).start()) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d36d7c4937..7fd0a6c3d5 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1239,7 +1239,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: { message in tapMessage?(message)