mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reaction improvements
This commit is contained in:
parent
b0d3aa2578
commit
95a492a25f
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<String>()
|
||||
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<Empty>
|
||||
public var size: CGSize
|
||||
}
|
||||
|
||||
public var items: [Item]
|
||||
public var removedViews: [ComponentHostView<Empty>]
|
||||
}
|
||||
|
||||
public private(set) var buttons: [String: ComponentHostView<Empty>] = [:]
|
||||
|
||||
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<Empty>] = []
|
||||
|
||||
var validIds = Set<String>()
|
||||
for reaction in reactions.sorted(by: { lhs, rhs in
|
||||
var lhsCount = lhs.count
|
||||
if lhs.isSelected {
|
||||
lhsCount -= 1
|
||||
}
|
||||
var rhsCount = rhs.count
|
||||
if rhs.isSelected {
|
||||
rhsCount -= 1
|
||||
}
|
||||
if lhsCount != rhsCount {
|
||||
return lhsCount > rhsCount
|
||||
}
|
||||
return lhs.reaction.value < rhs.reaction.value
|
||||
}) {
|
||||
validIds.insert(reaction.reaction.value)
|
||||
|
||||
let view: ComponentHostView<Empty>
|
||||
var itemTransition = transition
|
||||
if let current = self.buttons[reaction.reaction.value] {
|
||||
itemTransition = .immediate
|
||||
view = current
|
||||
} else {
|
||||
view = ComponentHostView<Empty>()
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -316,6 +316,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
||||
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 {
|
||||
@ -336,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 {
|
||||
@ -377,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?,
|
||||
@ -393,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)
|
||||
|
||||
@ -423,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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -438,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
|
||||
@ -452,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
|
||||
}
|
||||
@ -463,34 +467,50 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
||||
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0)
|
||||
|
||||
var validIds = Set<Int>()
|
||||
var validPlaceholderIds = Set<Int>()
|
||||
|
||||
let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight)))
|
||||
let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight))
|
||||
|
||||
if minVisibleIndex <= maxVisibleIndex {
|
||||
for index in minVisibleIndex ... maxVisibleIndex {
|
||||
if index >= self.state.items.count {
|
||||
break
|
||||
}
|
||||
|
||||
validIds.insert(index)
|
||||
|
||||
let itemNode: ItemNode
|
||||
if let current = self.itemNodes[index] {
|
||||
itemNode = current
|
||||
} else {
|
||||
let openPeer = self.openPeer
|
||||
let peerId = self.state.items[index].peer.id
|
||||
itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: {
|
||||
openPeer(peerId)
|
||||
})
|
||||
self.itemNodes[index] = itemNode
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight))
|
||||
itemNode.update(size: itemFrame.size, presentationData: presentationData, item: self.state.items[index], isLast: index == self.state.items.count - 1, syncronousLoad: syncronousLoad)
|
||||
itemNode.frame = itemFrame
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -501,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))
|
||||
@ -528,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)
|
||||
@ -563,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
|
||||
@ -579,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)
|
||||
@ -600,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)
|
||||
},
|
||||
@ -620,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)
|
||||
}
|
||||
@ -677,12 +759,12 @@ 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
|
||||
if let backButtonNode = self.backButtonNode {
|
||||
let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 45.0))
|
||||
let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0))
|
||||
backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: self.tabListNode == nil)
|
||||
transition.updateFrame(node: backButtonNode, frame: backButtonFrame)
|
||||
topContentHeight += backButtonFrame.height
|
||||
@ -704,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 {
|
||||
@ -734,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
|
||||
}
|
||||
@ -753,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,
|
||||
|
@ -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<Bool, NoError> { get }
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo?
|
||||
@ -2043,6 +2044,10 @@ public extension ContextExtractedContentSource {
|
||||
var centerVertically: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var centerActionsHorizontally: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var shouldBeDismissed: Signal<Bool, NoError> {
|
||||
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 }
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 - 4.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 - 1.0
|
||||
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)),
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
26
submodules/Display/Source/SimpleLayer.swift
Normal file
26
submodules/Display/Source/SimpleLayer.swift
Normal file
@ -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")
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -366,7 +366,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
|
||||
.withUpdatedCallJoinPeerId(groupCallDefaultJoinAs?.peerId)
|
||||
.withUpdatedThemeEmoticon(chatFullThemeEmoticon)
|
||||
.withUpdatedInviteRequestsPending(chatFullRequestsPending)
|
||||
.withUpdatedAllowedReactions(allowedReactions)
|
||||
.withUpdatedAllowedReactions(allowedReactions ?? [])
|
||||
})
|
||||
case .channelFull:
|
||||
break
|
||||
@ -597,7 +597,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee
|
||||
.withUpdatedThemeEmoticon(themeEmoticon)
|
||||
.withUpdatedInviteRequestsPending(requestsPending)
|
||||
.withUpdatedSendAsPeerId(sendAsPeerId)
|
||||
.withUpdatedAllowedReactions(allowedReactions)
|
||||
.withUpdatedAllowedReactions(allowedReactions ?? [])
|
||||
})
|
||||
|
||||
if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated {
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "reactions_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
180
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf
vendored
Normal file
180
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf
vendored
Normal file
@ -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
|
@ -66,6 +66,7 @@ import ChatListUI
|
||||
import CalendarMessageScreen
|
||||
import ReactionSelectionNode
|
||||
import LottieMeshSwift
|
||||
import ReactionListContextMenuContent
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
@ -952,16 +953,7 @@ 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(),
|
||||
strongSelf.context.account.postbox.transaction { transaction -> Set<String>? in
|
||||
let cachedData = transaction.getPeerCachedData(peerId: topMessage.id.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
|
||||
}
|
||||
},
|
||||
peerAllowedReactions(context: strongSelf.context, peerId: topMessage.id.peerId),
|
||||
ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
|
||||
).start(next: { actions, availableReactions, allowedReactions, chatTextSelectionTips in
|
||||
var actions = actions
|
||||
@ -1014,9 +1006,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
actions.context = strongSelf.context
|
||||
|
||||
if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions {
|
||||
for reaction in availableReactions.reactions {
|
||||
if !allowedReactions.contains(reaction.value) {
|
||||
continue
|
||||
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),
|
||||
@ -1095,6 +1092,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
|
||||
@ -1105,115 +1141,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
|
||||
@ -14400,3 +14461,23 @@ func canAddMessageReactions(message: Message) -> Bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
enum AllowedReactions {
|
||||
case set(Set<String>)
|
||||
case all
|
||||
}
|
||||
|
||||
func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal<AllowedReactions?, NoError> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -1192,6 +1192,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
if group.participantCount <= 50 {
|
||||
hasReadReports = true
|
||||
}
|
||||
} else {
|
||||
reactionCount = 0
|
||||
}
|
||||
|
||||
var readStats = readStats
|
||||
@ -1211,7 +1213,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: {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -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<Bool, NoError> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -869,6 +869,9 @@ public class ChatMessageItemView: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
func openMessageContextMenu() {
|
||||
}
|
||||
|
||||
func targetReactionView(value: String) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -69,6 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, openPeer: { _, _, _ in
|
||||
}, openPeerMention: { _ in
|
||||
}, openMessageContextMenu: { _, _, _, _, _ in
|
||||
}, openMessageReactionContextMenu: { _, _, _, _ in
|
||||
}, updateMessageReaction: { _, _ in
|
||||
}, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { _, _, _, _ in
|
||||
|
@ -1182,7 +1182,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
|
||||
if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) {
|
||||
//TODO:localize
|
||||
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
|
||||
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()
|
||||
}))
|
||||
}
|
||||
@ -1297,7 +1307,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
|
||||
if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) {
|
||||
//TODO:localize
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
|
||||
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()
|
||||
}))
|
||||
}
|
||||
@ -1310,7 +1330,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
} else {
|
||||
if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) {
|
||||
//TODO:localize
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
|
||||
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()
|
||||
}))
|
||||
}
|
||||
@ -1409,10 +1439,22 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
|
||||
interaction.editingOpenPreHistorySetup()
|
||||
}))
|
||||
|
||||
//TODO:localize
|
||||
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
|
||||
interaction.editingOpenReactionsSetup()
|
||||
}))
|
||||
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 {
|
||||
@ -1881,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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user