Reaction improvements

This commit is contained in:
Ali 2021-12-18 00:47:09 +04:00
parent b0d3aa2578
commit 95a492a25f
45 changed files with 1802 additions and 479 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View 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")
}
}

View File

@ -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

View File

@ -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 {

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "reactions_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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

View File

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

View File

@ -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: {

View File

@ -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: {

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -869,6 +869,9 @@ public class ChatMessageItemView: ListViewItemNode {
}
}
func openMessageContextMenu() {
}
func targetReactionView(value: String) -> UIView? {
return nil
}

View File

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

View File

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

View File

@ -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)) {

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -69,6 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openPeer: { _, _, _ in
}, openPeerMention: { _ in
}, openMessageContextMenu: { _, _, _, _, _ in
}, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in

View File

@ -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

View File

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