Reaction experiments

This commit is contained in:
Isaac 2024-06-28 19:36:15 +02:00
parent ad1ddf65c0
commit 8af8de7096
44 changed files with 2281 additions and 98 deletions

View File

@ -601,6 +601,7 @@ final class ChatSendMessageContextScreenComponent: Component {
id: AnyHashable("items"), id: AnyHashable("items"),
items: items, items: items,
reactionItems: nil, reactionItems: nil,
previewReaction: nil,
tip: nil, tip: nil,
tipSignal: .single(nil), tipSignal: .single(nil),
dismissed: nil dismissed: nil
@ -630,6 +631,7 @@ final class ChatSendMessageContextScreenComponent: Component {
id: AnyHashable("items"), id: AnyHashable("items"),
items: items, items: items,
reactionItems: nil, reactionItems: nil,
previewReaction: nil,
tip: nil, tip: nil,
tipSignal: .single(nil), tipSignal: .single(nil),
dismissed: nil dismissed: nil

View File

@ -24,7 +24,7 @@ private final class StarsButtonEffectLayer: SimpleLayer {
override init() { override init() {
super.init() super.init()
self.backgroundColor = UIColor.blue.withAlphaComponent(0.2).cgColor self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor
} }
override init(layer: Any) { override init(layer: Any) {

View File

@ -391,6 +391,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private final class ItemNode: HighlightTrackingButtonNode { private final class ItemNode: HighlightTrackingButtonNode {
let context: AccountContext let context: AccountContext
let displayReadTimestamps: Bool let displayReadTimestamps: Bool
let displayReactionIcon: Bool
let availableReactions: AvailableReactions? let availableReactions: AvailableReactions?
let animationCache: AnimationCache let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer let animationRenderer: MultiAnimationRenderer
@ -411,10 +412,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private var item: EngineMessageReactionListContext.Item? private var item: EngineMessageReactionListContext.Item?
init(context: AccountContext, displayReadTimestamps: Bool, availableReactions: AvailableReactions?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, action: @escaping () -> Void) { init(context: AccountContext, displayReadTimestamps: Bool, displayReactionIcon: Bool, availableReactions: AvailableReactions?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, action: @escaping () -> Void) {
self.action = action self.action = action
self.context = context self.context = context
self.displayReadTimestamps = displayReadTimestamps self.displayReadTimestamps = displayReadTimestamps
self.displayReactionIcon = displayReactionIcon
self.availableReactions = availableReactions self.availableReactions = availableReactions
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
@ -548,7 +550,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
let reaction: MessageReaction.Reaction? = item.reaction let reaction: MessageReaction.Reaction? = item.reaction
if reaction != self.item?.reaction { if self.displayReactionIcon, reaction != self.item?.reaction {
if let reaction = reaction { if let reaction = reaction {
switch reaction { switch reaction {
case .builtin: case .builtin:
@ -802,6 +804,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private let context: AccountContext private let context: AccountContext
private let displayReadTimestamps: Bool private let displayReadTimestamps: Bool
private let displayReactionIcons: Bool
private let availableReactions: AvailableReactions? private let availableReactions: AvailableReactions?
private let animationCache: AnimationCache private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer private let animationRenderer: MultiAnimationRenderer
@ -833,6 +836,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
init( init(
context: AccountContext, context: AccountContext,
displayReadTimestamps: Bool, displayReadTimestamps: Bool,
displayReactionIcons: Bool,
availableReactions: AvailableReactions?, availableReactions: AvailableReactions?,
animationCache: AnimationCache, animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer, animationRenderer: MultiAnimationRenderer,
@ -845,6 +849,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
) { ) {
self.context = context self.context = context
self.displayReadTimestamps = displayReadTimestamps self.displayReadTimestamps = displayReadTimestamps
self.displayReactionIcons = displayReactionIcons
self.availableReactions = availableReactions self.availableReactions = availableReactions
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
@ -955,7 +960,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
} else { } else {
let openPeer = self.openPeer let openPeer = self.openPeer
let peer = item.peer let peer = item.peer
itemNode = ItemNode(context: self.context, displayReadTimestamps: self.displayReadTimestamps, availableReactions: self.availableReactions, animationCache: self.animationCache, animationRenderer: self.animationRenderer, action: { itemNode = ItemNode(context: self.context, displayReadTimestamps: self.displayReadTimestamps, displayReactionIcon: self.displayReactionIcons, availableReactions: self.availableReactions, animationCache: self.animationCache, animationRenderer: self.animationRenderer, action: {
openPeer(peer, item.reaction != nil) openPeer(peer, item.reaction != nil)
}) })
self.itemNodes[index] = itemNode self.itemNodes[index] = itemNode
@ -1104,6 +1109,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode, ASGestureRecognizerDelegate { final class ItemsNode: ASDisplayNode, ContextControllerItemsNode, ASGestureRecognizerDelegate {
private let context: AccountContext private let context: AccountContext
private let displayReadTimestamps: Bool private let displayReadTimestamps: Bool
private let displayReactionIcons: Bool
private let availableReactions: AvailableReactions? private let availableReactions: AvailableReactions?
private let animationCache: AnimationCache private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer private let animationRenderer: MultiAnimationRenderer
@ -1148,6 +1154,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
) { ) {
self.context = context self.context = context
self.displayReadTimestamps = displayReadTimestamps self.displayReadTimestamps = displayReadTimestamps
self.displayReactionIcons = reaction == nil
self.availableReactions = availableReactions self.availableReactions = availableReactions
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
@ -1159,9 +1166,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.requestUpdate = requestUpdate self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight self.requestUpdateApparentHeight = requestUpdateApparentHeight
//var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
//var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
if let back = back { if let back = back {
self.backButtonNode = BackButtonNode() self.backButtonNode = BackButtonNode()
self.backButtonNode?.action = { self.backButtonNode?.action = {
@ -1218,45 +1222,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
strongSelf.tabListNode?.scrollToTabReaction = ReactionTabListNode.ScrollToTabReaction(value: reaction) strongSelf.tabListNode?.scrollToTabReaction = ReactionTabListNode.ScrollToTabReaction(value: reaction)
strongSelf.currentTabIndex = tabIndex strongSelf.currentTabIndex = tabIndex
/*let currentTabNode = ReactionsTabNode(
context: context,
availableReactions: availableReactions,
message: message,
reaction: reaction,
readStats: nil,
requestUpdate: { tab, transition in
requestUpdateTab?(tab, transition)
},
requestUpdateApparentHeight: { tab, transition in
requestUpdateTabApparentHeight?(tab, transition)
},
openPeer: { id in
openPeer(id)
}
)
strongSelf.currentTabNode = currentTabNode
strongSelf.addSubnode(currentTabNode)*/
strongSelf.requestUpdate(.animated(duration: 0.45, curve: .spring)) strongSelf.requestUpdate(.animated(duration: 0.45, curve: .spring))
} }
/*requestUpdateTab = { [weak self] tab, transition in
guard let strongSelf = self else {
return
}
if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
strongSelf.requestUpdate(transition)
}
}
requestUpdateTabApparentHeight = { [weak self] tab, transition in
guard let strongSelf = self else {
return
}
if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
strongSelf.requestUpdateApparentHeight(transition)
}
}*/
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let strongSelf = self else { guard let strongSelf = self else {
return [] return []
@ -1371,6 +1339,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
tabNode = ReactionsTabNode( tabNode = ReactionsTabNode(
context: self.context, context: self.context,
displayReadTimestamps: self.displayReadTimestamps, displayReadTimestamps: self.displayReadTimestamps,
displayReactionIcons: self.displayReactionIcons,
availableReactions: self.availableReactions, availableReactions: self.availableReactions,
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/UIKitRuntimeUtils", "//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -436,6 +436,12 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
self.targetSelectionIndex = nil self.targetSelectionIndex = nil
icon = nil icon = nil
isUserInteractionEnabled = action != nil isUserInteractionEnabled = action != nil
case let .starsReactions(topCount):
self.action = nil
self.text = "Send \(topCount) or more to highlight your profile"
self.targetSelectionIndex = nil
icon = nil
isUserInteractionEnabled = action != nil
} }
self.iconNode = ASImageNode() self.iconNode = ASImageNode()

View File

@ -2277,6 +2277,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var allPresetReactionsAreAvailable: Bool public var allPresetReactionsAreAvailable: Bool
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
public var disablePositionLock: Bool public var disablePositionLock: Bool
public var previewReaction: TelegramMediaFile?
public var tip: Tip? public var tip: Tip?
public var tipSignal: Signal<Tip?, NoError>? public var tipSignal: Signal<Tip?, NoError>?
public var dismissed: (() -> Void)? public var dismissed: (() -> Void)?
@ -2294,6 +2295,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
allPresetReactionsAreAvailable: Bool = false, allPresetReactionsAreAvailable: Bool = false,
getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil,
disablePositionLock: Bool = false, disablePositionLock: Bool = false,
previewReaction: TelegramMediaFile? = nil,
tip: Tip? = nil, tip: Tip? = nil,
tipSignal: Signal<Tip?, NoError>? = nil, tipSignal: Signal<Tip?, NoError>? = nil,
dismissed: (() -> Void)? = nil dismissed: (() -> Void)? = nil
@ -2310,6 +2312,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable
self.getEmojiContent = getEmojiContent self.getEmojiContent = getEmojiContent
self.disablePositionLock = disablePositionLock self.disablePositionLock = disablePositionLock
self.previewReaction = previewReaction
self.tip = tip self.tip = tip
self.tipSignal = tipSignal self.tipSignal = tipSignal
self.dismissed = dismissed self.dismissed = dismissed
@ -2327,6 +2330,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.allPresetReactionsAreAvailable = false self.allPresetReactionsAreAvailable = false
self.getEmojiContent = nil self.getEmojiContent = nil
self.disablePositionLock = false self.disablePositionLock = false
self.previewReaction = nil
self.tip = nil self.tip = nil
self.tipSignal = nil self.tipSignal = nil
self.dismissed = nil self.dismissed = nil
@ -2345,6 +2349,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
case messageCopyProtection(isChannel: Bool) case messageCopyProtection(isChannel: Bool)
case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?)
case notificationTopicExceptions(text: String, action: (() -> Void)?) case notificationTopicExceptions(text: String, action: (() -> Void)?)
case starsReactions(topCount: Int)
public static func ==(lhs: Tip, rhs: Tip) -> Bool { public static func ==(lhs: Tip, rhs: Tip) -> Bool {
switch lhs { switch lhs {
@ -2390,6 +2395,12 @@ public final class ContextController: ViewController, StandalonePresentableContr
} else { } else {
return false return false
} }
case let .starsReactions(topCount):
if case .starsReactions(topCount) = rhs {
return true
} else {
return false
}
} }
} }
} }

View File

@ -59,6 +59,16 @@ public struct ContextControllerReactionItems {
} }
} }
public final class ContextControllerPreviewReaction {
public let context: AccountContext
public let file: TelegramMediaFile
public init(context: AccountContext, file: TelegramMediaFile) {
self.context = context
self.file = file
}
}
public protocol ContextControllerActionsStackItem: AnyObject { public protocol ContextControllerActionsStackItem: AnyObject {
func node( func node(
getController: @escaping () -> ContextControllerProtocol?, getController: @escaping () -> ContextControllerProtocol?,
@ -71,6 +81,7 @@ public protocol ContextControllerActionsStackItem: AnyObject {
var tip: ContextController.Tip? { get } var tip: ContextController.Tip? { get }
var tipSignal: Signal<ContextController.Tip?, NoError>? { get } var tipSignal: Signal<ContextController.Tip?, NoError>? { get }
var reactionItems: ContextControllerReactionItems? { get } var reactionItems: ContextControllerReactionItems? { get }
var previewReaction: ContextControllerPreviewReaction? { get }
var dismissed: (() -> Void)? { get } var dismissed: (() -> Void)? { get }
} }
@ -936,6 +947,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
public let id: AnyHashable? public let id: AnyHashable?
public let items: [ContextMenuItem] public let items: [ContextMenuItem]
public let reactionItems: ContextControllerReactionItems? public let reactionItems: ContextControllerReactionItems?
public let previewReaction: ContextControllerPreviewReaction?
public let tip: ContextController.Tip? public let tip: ContextController.Tip?
public let tipSignal: Signal<ContextController.Tip?, NoError>? public let tipSignal: Signal<ContextController.Tip?, NoError>?
public let dismissed: (() -> Void)? public let dismissed: (() -> Void)?
@ -944,6 +956,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
id: AnyHashable?, id: AnyHashable?,
items: [ContextMenuItem], items: [ContextMenuItem],
reactionItems: ContextControllerReactionItems?, reactionItems: ContextControllerReactionItems?,
previewReaction: ContextControllerPreviewReaction?,
tip: ContextController.Tip?, tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?, tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)? dismissed: (() -> Void)?
@ -951,6 +964,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
self.id = id self.id = id
self.items = items self.items = items
self.reactionItems = reactionItems self.reactionItems = reactionItems
self.previewReaction = previewReaction
self.tip = tip self.tip = tip
self.tipSignal = tipSignal self.tipSignal = tipSignal
self.dismissed = dismissed self.dismissed = dismissed
@ -1034,6 +1048,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
let id: AnyHashable? let id: AnyHashable?
private let content: ContextControllerItemsContent private let content: ContextControllerItemsContent
let reactionItems: ContextControllerReactionItems? let reactionItems: ContextControllerReactionItems?
let previewReaction: ContextControllerPreviewReaction?
let tip: ContextController.Tip? let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>? let tipSignal: Signal<ContextController.Tip?, NoError>?
let dismissed: (() -> Void)? let dismissed: (() -> Void)?
@ -1042,6 +1057,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
id: AnyHashable?, id: AnyHashable?,
content: ContextControllerItemsContent, content: ContextControllerItemsContent,
reactionItems: ContextControllerReactionItems?, reactionItems: ContextControllerReactionItems?,
previewReaction: ContextControllerPreviewReaction?,
tip: ContextController.Tip?, tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?, tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)? dismissed: (() -> Void)?
@ -1049,6 +1065,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
self.id = id self.id = id
self.content = content self.content = content
self.reactionItems = reactionItems self.reactionItems = reactionItems
self.previewReaction = previewReaction
self.tip = tip self.tip = tip
self.tipSignal = tipSignal self.tipSignal = tipSignal
self.dismissed = dismissed self.dismissed = dismissed
@ -1084,13 +1101,17 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C
getEmojiContent: items.getEmojiContent getEmojiContent: items.getEmojiContent
) )
} }
var previewReaction: ContextControllerPreviewReaction?
if let context = items.context, let file = items.previewReaction {
previewReaction = ContextControllerPreviewReaction(context: context, file: file)
}
switch items.content { switch items.content {
case let .list(listItems): case let .list(listItems):
return [ContextControllerActionsListStackItem(id: items.id, items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] return [ContextControllerActionsListStackItem(id: items.id, items: listItems, reactionItems: reactionItems, previewReaction: previewReaction, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
case let .twoLists(listItems1, listItems2): case let .twoLists(listItems1, listItems2):
return [ContextControllerActionsListStackItem(id: items.id, items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(id: nil, items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)] return [ContextControllerActionsListStackItem(id: items.id, items: listItems1, reactionItems: nil, previewReaction: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(id: nil, items: listItems2, reactionItems: nil, previewReaction: nil, tip: nil, tipSignal: nil, dismissed: nil)]
case let .custom(customContent): case let .custom(customContent):
return [ContextControllerActionsCustomStackItem(id: items.id, content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)] return [ContextControllerActionsCustomStackItem(id: items.id, content: customContent, reactionItems: reactionItems, previewReaction: previewReaction, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
} }
} }
@ -1207,6 +1228,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
let tipSignal: Signal<ContextController.Tip?, NoError>? let tipSignal: Signal<ContextController.Tip?, NoError>?
var tipNode: InnerTextSelectionTipContainerNode? var tipNode: InnerTextSelectionTipContainerNode?
let reactionItems: ContextControllerReactionItems? let reactionItems: ContextControllerReactionItems?
let previewReaction: ContextControllerPreviewReaction?
let itemDismissed: (() -> Void)? let itemDismissed: (() -> Void)?
var storedScrollingState: CGFloat? var storedScrollingState: CGFloat?
let positionLock: CGFloat? let positionLock: CGFloat?
@ -1222,6 +1244,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
tip: ContextController.Tip?, tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?, tipSignal: Signal<ContextController.Tip?, NoError>?,
reactionItems: ContextControllerReactionItems?, reactionItems: ContextControllerReactionItems?,
previewReaction: ContextControllerPreviewReaction?,
itemDismissed: (() -> Void)?, itemDismissed: (() -> Void)?,
positionLock: CGFloat? positionLock: CGFloat?
) { ) {
@ -1240,6 +1263,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
self.dimNode.alpha = 0.0 self.dimNode.alpha = 0.0
self.reactionItems = reactionItems self.reactionItems = reactionItems
self.previewReaction = previewReaction
self.itemDismissed = itemDismissed self.itemDismissed = itemDismissed
self.positionLock = positionLock self.positionLock = positionLock
@ -1376,6 +1400,10 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
return self.itemContainers.last?.reactionItems return self.itemContainers.last?.reactionItems
} }
public var topPreviewReaction: ContextControllerPreviewReaction? {
return self.itemContainers.last?.previewReaction
}
public var topPositionLock: CGFloat? { public var topPositionLock: CGFloat? {
return self.itemContainers.last?.positionLock return self.itemContainers.last?.positionLock
} }
@ -1509,6 +1537,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
tip: item.tip, tip: item.tip,
tipSignal: item.tipSignal, tipSignal: item.tipSignal,
reactionItems: item.reactionItems, reactionItems: item.reactionItems,
previewReaction: item.previewReaction,
itemDismissed: item.dismissed, itemDismissed: item.dismissed,
positionLock: positionLock positionLock: positionLock
) )

View File

@ -241,6 +241,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
private let scrollNode: ASDisplayNode private let scrollNode: ASDisplayNode
private var reactionContextNode: ReactionContextNode? private var reactionContextNode: ReactionContextNode?
private var reactionPreviewView: ReactionPreviewView?
private var reactionContextNodeIsAnimatingOut: Bool = false private var reactionContextNodeIsAnimatingOut: Bool = false
private var itemContentNode: ItemContentNode? private var itemContentNode: ItemContentNode?
@ -637,6 +638,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
var animateReactionsIn = false var animateReactionsIn = false
var contentTopInset: CGFloat = topInset var contentTopInset: CGFloat = topInset
var removedReactionContextNode: ReactionContextNode? var removedReactionContextNode: ReactionContextNode?
if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty { if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty {
let reactionContextNode: ReactionContextNode let reactionContextNode: ReactionContextNode
if let current = self.reactionContextNode { if let current = self.reactionContextNode {
@ -733,6 +735,25 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
removedReactionContextNode = reactionContextNode removedReactionContextNode = reactionContextNode
} }
let reactionPreviewSize = CGSize(width: 100.0, height: 100.0)
let reactionPreviewInset: CGFloat = 7.0
var removedReactionPreviewView: ReactionPreviewView?
if self.reactionContextNode == nil, let previewReaction = self.actionsStackNode.topPreviewReaction {
let reactionPreviewView: ReactionPreviewView
if let current = self.reactionPreviewView {
reactionPreviewView = current
} else {
reactionPreviewView = ReactionPreviewView(context: previewReaction.context, file: previewReaction.file)
self.reactionPreviewView = reactionPreviewView
self.view.addSubview(reactionPreviewView)
}
contentTopInset += reactionPreviewSize.height + reactionPreviewInset
} else {
removedReactionPreviewView = self.reactionPreviewView
self.reactionPreviewView = nil
}
if let contentNode = itemContentNode { if let contentNode = itemContentNode {
switch stateTransition { switch stateTransition {
case .animateIn, .animateOut: case .animateIn, .animateOut:
@ -963,6 +984,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.proposedReactionsPositionLock = nil self.proposedReactionsPositionLock = nil
} }
if let reactionPreviewView = self.reactionPreviewView {
let anchorRect = contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0)
let reactionPreviewFrame = CGRect(origin: CGPoint(x: floor((anchorRect.midX - reactionPreviewSize.width * 0.5)), y: anchorRect.minY - reactionPreviewInset - reactionPreviewSize.height), size: reactionPreviewSize)
transition.updateFrame(view: reactionPreviewView, frame: reactionPreviewFrame)
reactionPreviewView.update(size: reactionPreviewFrame.size)
}
if let _ = self.currentReactionsPositionLock { if let _ = self.currentReactionsPositionLock {
transition.updateAlpha(node: self.actionsStackNode, alpha: 0.0) transition.updateAlpha(node: self.actionsStackNode, alpha: 0.0)
} else { } else {
@ -976,6 +1005,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}) })
} }
if let removedReactionPreviewView {
transition.updateAlpha(layer: removedReactionPreviewView.layer, alpha: 0.0, completion: { [weak removedReactionPreviewView] _ in
removedReactionPreviewView?.removeFromSuperview()
})
}
transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true)
var actionsFrame: CGRect var actionsFrame: CGRect
@ -1205,6 +1240,29 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
damping: springDamping, damping: springDamping,
additive: true additive: true
) )
if let reactionPreviewView = self.reactionPreviewView {
reactionPreviewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
reactionPreviewView.layer.animateSpring(
from: -animationInContentYDistance as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.y",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
reactionPreviewView.layer.animateSpring(
from: 0.01 as NSNumber,
to: 1.0 as NSNumber,
keyPath: "transform.scale",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: false
)
}
} else if let contentNode = controllerContentNode { } else if let contentNode = controllerContentNode {
if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() {
let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) let sourcePoint = sourceView.convert(sourceRect.center, to: self.view)
@ -1493,6 +1551,32 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}) })
} }
) )
if let reactionPreviewView = self.reactionPreviewView {
reactionPreviewView.layer.animate(
from: 0.0 as NSNumber,
to: -animationInContentYDistance as NSNumber,
keyPath: "position.y",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
removeOnCompletion: true,
additive: true,
completion: { _ in
}
)
reactionPreviewView.layer.animate(
from: 1.0 as NSNumber,
to: 0.01 as NSNumber,
keyPath: "transform.scale",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
removeOnCompletion: false,
additive: false
)
reactionPreviewView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in })
}
} }
if let contentNode = controllerContentNode { if let contentNode = controllerContentNode {
if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() {

View File

@ -0,0 +1,55 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import ComponentFlow
import TelegramCore
import AccountContext
import EmojiStatusComponent
final class ReactionPreviewView: UIView {
private let context: AccountContext
private let file: TelegramMediaFile
private let icon = ComponentView<Empty>()
init(context: AccountContext, file: TelegramMediaFile) {
self.context = context
self.file = file
super.init(frame: CGRect())
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(size: CGSize) {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
content: .animation(
content: .file(file: self.file),
size: size,
placeholderColor: .clear,
themeColor: .white,
loopMode: .count(0)
),
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
}
}

View File

@ -664,6 +664,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate {
} }
strongSelf.hideExpandedTopPanel = emojiContent.panelItemGroups.isEmpty strongSelf.hideExpandedTopPanel = emojiContent.panelItemGroups.isEmpty
if emojiContent.panelItemGroups.count == 1 && emojiContent.panelItemGroups[0].groupId == AnyHashable("recent") {
strongSelf.hideExpandedTopPanel = true
}
var emojiContent = emojiContent var emojiContent = emojiContent
if let emojiSearchResult = emojiSearchState.result { if let emojiSearchResult = emojiSearchState.result {

View File

@ -60,7 +60,7 @@ private final class StarsReactionEffectLayer: SimpleLayer {
override init() { override init() {
super.init() super.init()
self.backgroundColor = UIColor.blue.withAlphaComponent(0.2).cgColor self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor
} }
override init(layer: Any) { override init(layer: Any) {

View File

@ -3,7 +3,11 @@ import Postbox
import TelegramApi import TelegramApi
public struct MessageReaction: Equatable, PostboxCoding, Codable { public struct MessageReaction: Equatable, PostboxCoding, Codable {
#if DEBUG
public static let starsReactionId: Int64 = 5435957248314579621 public static let starsReactionId: Int64 = 5435957248314579621
#else
public static let starsReactionId: Int64 = 12340000
#endif
public enum Reaction: Hashable, Comparable, Codable, PostboxCoding { public enum Reaction: Hashable, Comparable, Codable, PostboxCoding {
case builtin(String) case builtin(String)

View File

@ -455,6 +455,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/FactCheckAlertController", "//submodules/TelegramUI/Components/Chat/FactCheckAlertController",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View File

@ -1082,7 +1082,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.message),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -737,7 +737,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring, hasAutoremove: message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: message, isInline: associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: message),
animationCache: controllerInteraction.presentationContext.animationCache, animationCache: controllerInteraction.presentationContext.animationCache,
animationRenderer: controllerInteraction.presentationContext.animationRenderer animationRenderer: controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -2277,7 +2277,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
replyCount: dateReplies, replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring, hasAutoremove: message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: message, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: message),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -302,7 +302,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -905,7 +905,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
let canViewReactionList = arguments.canViewReactionList let canViewReactionList = arguments.canViewReactionList
item.node.view.activateAfterCompletion = !canViewReactionList item.node.view.activateAfterCompletion = !canViewReactionList
item.node.view.activated = { [weak itemNode] gesture, _ in item.node.view.activated = { [weak itemNode] gesture, _ in
guard let strongSelf = self, canViewReactionList else { guard let strongSelf = self else {
return return
} }
guard let itemNode = itemNode else { guard let itemNode = itemNode else {

View File

@ -449,7 +449,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -532,7 +532,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode,
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -947,7 +947,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode,
hasAutoremove: arguments.message.isSelfExpiring, hasAutoremove: arguments.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: arguments.topMessage, isInline: arguments.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: arguments.topMessage),
animationCache: arguments.controllerInteraction.presentationContext.animationCache, animationCache: arguments.controllerInteraction.presentationContext.animationCache,
animationRenderer: arguments.controllerInteraction.presentationContext.animationRenderer animationRenderer: arguments.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -585,7 +585,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -967,7 +967,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
replyCount: dateAndStatus.dateReplies, replyCount: dateAndStatus.dateReplies,
isPinned: dateAndStatus.isPinned, isPinned: dateAndStatus.isPinned,
hasAutoremove: message.isSelfExpiring, hasAutoremove: message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: message, isInline: associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: message),
animationCache: presentationContext.animationCache, animationCache: presentationContext.animationCache,
animationRenderer: presentationContext.animationRenderer animationRenderer: presentationContext.animationRenderer
)) ))

View File

@ -158,7 +158,7 @@ public struct ChatMessageItemLayoutConstants {
} }
} }
public func canViewMessageReactionList(message: Message, isInline: Bool) -> Bool { public func canViewMessageReactionList(message: Message) -> Bool {
var found = false var found = false
var canViewList = false var canViewList = false
for attribute in message.attributes { for attribute in message.attributes {

View File

@ -286,7 +286,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -1127,7 +1127,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -362,16 +362,13 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
let itemValue = item.value let itemValue = item.value
let itemNode = item.node let itemNode = item.node
item.node.view.isGestureEnabled = true item.node.view.isGestureEnabled = true
let canViewReactionList = canViewMessageReactionList(message: message, isInline: associatedData.isInline) let canViewReactionList = canViewMessageReactionList(message: message)
item.node.view.activateAfterCompletion = !canViewReactionList item.node.view.activateAfterCompletion = !canViewReactionList
item.node.view.activated = { [weak itemNode] gesture, _ in item.node.view.activated = { [weak itemNode] gesture, _ in
guard let strongSelf = self, let itemNode = itemNode else { guard let strongSelf = self, let itemNode = itemNode else {
gesture.cancel() gesture.cancel()
return return
} }
if !canViewReactionList {
return
}
strongSelf.openReactionPreview?(gesture, itemNode.view.containerView, itemValue) strongSelf.openReactionPreview?(gesture, itemNode.view.containerView, itemValue)
} }
item.node.view.additionalActivationProgressLayer = itemMaskView.layer item.node.view.additionalActivationProgressLayer = itemMaskView.layer

View File

@ -142,7 +142,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -647,7 +647,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.message),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -649,7 +649,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies, replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread), isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread),
hasAutoremove: item.message.isSelfExpiring, hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline), canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache, animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
)) ))

View File

@ -0,0 +1,38 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatSendStarsScreen",
module_name = "ChatSendStarsScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ChatPresentationInterfaceState",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/ContextUI",
"//submodules/AppBundle",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/SliderComponent",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/AvatarNode",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,166 @@
import Foundation
import UIKit
import Display
import ComponentFlow
private let labelWidth: CGFloat = 16.0
private let labelHeight: CGFloat = 36.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
private let font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
final class BadgeLabelView: UIView {
private class StackView: UIView {
var labels: [UILabel] = []
var currentValue: Int32 = 0
var color: UIColor = .white {
didSet {
for view in self.labels {
view.textColor = self.color
}
}
}
init() {
super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight
for i in -1 ..< 10 {
let label = UILabel()
if i == -1 {
label.text = "9"
} else {
label.text = "\(i)"
}
label.textColor = self.color
label.font = font
label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
self.addSubview(label)
self.labels.append(label)
height += labelHeight
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue
self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
}
}
private var itemViews: [Int: StackView] = [:]
private var staticLabel = UILabel()
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var color: UIColor = .white {
didSet {
self.staticLabel.textColor = self.color
for (_, view) in self.itemViews {
view.color = self.color
}
}
}
func update(value: String, transition: ComponentTransition) -> CGSize {
if value.contains(" ") {
for (_, view) in self.itemViews {
view.isHidden = true
}
if self.staticLabel.superview == nil {
self.staticLabel.textColor = self.color
self.staticLabel.font = font
self.addSubview(self.staticLabel)
}
self.staticLabel.text = value
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
}
let string = value
let stringArray = Array(string.map { String($0) }.reversed())
let totalWidth = CGFloat(stringArray.count) * labelWidth
var validIds: [Int] = []
for i in 0 ..< stringArray.count {
validIds.append(i)
let itemView: StackView
var itemTransition = transition
if let current = self.itemViews[i] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = StackView()
itemView.color = self.color
self.itemViews[i] = itemView
self.addSubview(itemView)
}
let digit = Int32(stringArray[i]) ?? 0
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
itemTransition.setFrame(
view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
)
}
var removeIds: [Int] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return CGSize(width: totalWidth, height: labelHeight)
}
}

View File

@ -259,7 +259,7 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe
if case let .set(reactions) = allowedReactions { if case let .set(reactions) = allowedReactions {
#if DEBUG #if DEBUG
var reactions = reactions var reactions = reactions
if "".isEmpty { if context.sharedContext.applicationBindings.appBuildType == .internal {
reactions.insert(.custom(MessageReaction.starsReactionId)) reactions.insert(.custom(MessageReaction.starsReactionId))
} }
#endif #endif
@ -277,13 +277,14 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe
} }
} else { } else {
#if DEBUG #if DEBUG
return context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) if context.sharedContext.applicationBindings.appBuildType == .internal {
|> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in return context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
return (allowedReactions, files) |> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in
return (allowedReactions, files)
}
} }
#else
return .single((allowedReactions, [:]))
#endif #endif
return .single((allowedReactions, [:]))
} }
} }
@ -302,7 +303,7 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe
var existingIds = Set<MessageReaction.Reaction>() var existingIds = Set<MessageReaction.Reaction>()
#if DEBUG #if DEBUG
if "".isEmpty { if context.sharedContext.applicationBindings.appBuildType == .internal {
if let file = allowedReactionsAndFiles.files[MessageReaction.starsReactionId] { if let file = allowedReactionsAndFiles.files[MessageReaction.starsReactionId] {
existingIds.insert(.custom(MessageReaction.starsReactionId)) existingIds.insert(.custom(MessageReaction.starsReactionId))

View File

@ -60,6 +60,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
private var reactionsInfoText: ComponentView<Empty>? private var reactionsInfoText: ComponentView<Empty>?
private var reactionInput: ComponentView<Empty>? private var reactionInput: ComponentView<Empty>?
private var reactionCountSection: ComponentView<Empty>? private var reactionCountSection: ComponentView<Empty>?
private var paidReactionsSection: ComponentView<Empty>?
private let actionButton = ComponentView<Empty>() private let actionButton = ComponentView<Empty>()
private var reactionSelectionControl: ComponentView<Empty>? private var reactionSelectionControl: ComponentView<Empty>?
@ -79,6 +80,8 @@ final class PeerAllowedReactionsScreenComponent: Component {
private var allowedReactionCount: Int = 11 private var allowedReactionCount: Int = 11
private var appliedReactionSettings: PeerReactionSettings? private var appliedReactionSettings: PeerReactionSettings?
private var areStarsReactionsEnabled: Bool = true
private var emojiContent: EmojiPagerContentComponent? private var emojiContent: EmojiPagerContentComponent?
private var emojiContentDisposable: Disposable? private var emojiContentDisposable: Disposable?
private var caretPosition: Int? private var caretPosition: Int?
@ -93,6 +96,8 @@ final class PeerAllowedReactionsScreenComponent: Component {
private weak var currentUndoController: UndoOverlayController? private weak var currentUndoController: UndoOverlayController?
private var cachedChevronImage: (UIImage, PresentationTheme)?
override init(frame: CGRect) { override init(frame: CGRect) {
self.scrollView = UIScrollView() self.scrollView = UIScrollView()
self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsVerticalScrollIndicator = true
@ -862,7 +867,110 @@ final class PeerAllowedReactionsScreenComponent: Component {
} }
} }
contentHeight += reactionCountSectionSize.height contentHeight += reactionCountSectionSize.height
contentHeight += 12.0
if "".isEmpty {
contentHeight += 32.0
let paidReactionsSection: ComponentView<Empty>
if let current = self.paidReactionsSection {
paidReactionsSection = current
} else {
paidReactionsSection = ComponentView()
self.paidReactionsSection = paidReactionsSection
}
//TODO:localize
let parsedString = parseMarkdownIntoAttributedString("Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)", attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { url in
return ("URL", url)
}))
let paidReactionsFooterText = NSMutableAttributedString(attributedString: parsedString)
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
}
if let range = paidReactionsFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
paidReactionsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: paidReactionsFooterText.string))
}
//TODO:localize
let paidReactionsSectionSize = paidReactionsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(paidReactionsFooterText),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
}, tapAction: { [weak self] attributes, _ in
guard let self, let component = self.component else {
return
}
if let url = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
component.context.sharedContext.applicationBindings.openUrl(url)
}
}
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListSwitchItemComponent(
theme: environment.theme,
title: "Enable Paid Reactions",
value: areStarsReactionsEnabled,
valueUpdated: { [weak self] value in
guard let self else {
return
}
self.areStarsReactionsEnabled = value
}
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let paidReactionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: paidReactionsSectionSize)
if let paidReactionsSectionView = paidReactionsSection.view {
if paidReactionsSectionView.superview == nil {
self.scrollView.addSubview(paidReactionsSectionView)
}
if animateIn {
paidReactionsSectionView.frame = paidReactionsSectionFrame
if !transition.animation.isImmediate {
paidReactionsSectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
transition.setFrame(view: paidReactionsSectionView, frame: paidReactionsSectionFrame)
}
}
contentHeight += paidReactionsSectionSize.height
contentHeight += 12.0
} else {
contentHeight += 12.0
if let paidReactionsSection = self.paidReactionsSection {
self.paidReactionsSection = nil
if let paidReactionsSectionView = paidReactionsSection.view {
if !transition.animation.isImmediate {
paidReactionsSectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak paidReactionsSectionView] _ in
paidReactionsSectionView?.removeFromSuperview()
})
} else {
paidReactionsSectionView.removeFromSuperview()
}
}
}
}
} else { } else {
if let reactionsTitleText = self.reactionsTitleText { if let reactionsTitleText = self.reactionsTitleText {
self.reactionsTitleText = nil self.reactionsTitleText = nil
@ -915,6 +1023,19 @@ final class PeerAllowedReactionsScreenComponent: Component {
} }
} }
} }
if let paidReactionsSection = self.paidReactionsSection {
self.paidReactionsSection = nil
if let paidReactionsSectionView = paidReactionsSection.view {
if !transition.animation.isImmediate {
paidReactionsSectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak paidReactionsSectionView] _ in
paidReactionsSectionView?.removeFromSuperview()
})
} else {
paidReactionsSectionView.removeFromSuperview()
}
}
}
} }
var buttonContents: [AnyComponentWithIdentity<Empty>] = [] var buttonContents: [AnyComponentWithIdentity<Empty>] = []

View File

@ -12,6 +12,8 @@ public final class SliderComponent: Component {
public let markPositions: Bool public let markPositions: Bool
public let trackBackgroundColor: UIColor public let trackBackgroundColor: UIColor
public let trackForegroundColor: UIColor public let trackForegroundColor: UIColor
public let knobSize: CGFloat?
public let knobColor: UIColor?
public let valueUpdated: (Int) -> Void public let valueUpdated: (Int) -> Void
public let isTrackingUpdated: ((Bool) -> Void)? public let isTrackingUpdated: ((Bool) -> Void)?
@ -21,6 +23,8 @@ public final class SliderComponent: Component {
markPositions: Bool, markPositions: Bool,
trackBackgroundColor: UIColor, trackBackgroundColor: UIColor,
trackForegroundColor: UIColor, trackForegroundColor: UIColor,
knobSize: CGFloat? = nil,
knobColor: UIColor? = nil,
valueUpdated: @escaping (Int) -> Void, valueUpdated: @escaping (Int) -> Void,
isTrackingUpdated: ((Bool) -> Void)? = nil isTrackingUpdated: ((Bool) -> Void)? = nil
) { ) {
@ -29,6 +33,8 @@ public final class SliderComponent: Component {
self.markPositions = markPositions self.markPositions = markPositions
self.trackBackgroundColor = trackBackgroundColor self.trackBackgroundColor = trackBackgroundColor
self.trackForegroundColor = trackForegroundColor self.trackForegroundColor = trackForegroundColor
self.knobSize = knobSize
self.knobColor = knobColor
self.valueUpdated = valueUpdated self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated self.isTrackingUpdated = isTrackingUpdated
} }
@ -49,6 +55,12 @@ public final class SliderComponent: Component {
if lhs.trackForegroundColor != rhs.trackForegroundColor { if lhs.trackForegroundColor != rhs.trackForegroundColor {
return false return false
} }
if lhs.knobSize != rhs.knobSize {
return false
}
if lhs.knobColor != rhs.knobColor {
return false
}
return true return true
} }
@ -94,8 +106,12 @@ public final class SliderComponent: Component {
} else { } else {
sliderView = TGPhotoEditorSliderView() sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0 if let knobSize = component.knobSize {
sliderView.lineSize = 4.0 sliderView.lineSize = knobSize + 4.0
} else {
sliderView.lineSize = 4.0
}
sliderView.trackCornerRadius = sliderView.lineSize * 0.5
sliderView.dotSize = 5.0 sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0 sliderView.minimumValue = 0.0
sliderView.startValue = 0.0 sliderView.startValue = 0.0
@ -110,12 +126,25 @@ public final class SliderComponent: Component {
sliderView.backColor = component.trackBackgroundColor sliderView.backColor = component.trackBackgroundColor
sliderView.startColor = component.trackBackgroundColor sliderView.startColor = component.trackBackgroundColor
sliderView.trackColor = component.trackForegroundColor sliderView.trackColor = component.trackForegroundColor
sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in if let knobSize = component.knobSize {
context.clear(CGRect(origin: CGPoint(), size: size)) sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor) context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) if let knobColor = component.knobColor {
}) context.setFillColor(knobColor.cgColor)
} else {
context.setFillColor(UIColor.white.cgColor)
}
context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - knobSize) * 0.5), y: floor((size.width - knobSize) * 0.5)), size: CGSize(width: knobSize, height: knobSize)))
})
} else {
sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
})
}
sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)

View File

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

View File

@ -0,0 +1,62 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Filter /FlateDecode
/Length 3 0 R
>>
stream
xe”ÍŠG …÷ý½¤-•~k²NòÆcðóûSÏxÆØw¥{Z:ut¤ªw¼ÿòÿãýßþvþþÏñîíßãóñéÐKîß)ß__£ï¾ÙZ+“ £Jã||x«ü9øüøxØåé"ªiÒzNIkÉjx4\-Ï'°t8 L|¯øÒÞsšŠ‰ˆ<0E>U¾RÏu ->YµÅÈZ!žÄ£2ÖpGX¥Ýu±6…|Œ¨ûYÃÒ¼Úº!!€À·žë¢sÓ5<C393>¤Ãµ.k—Úp9 îuÚµ<Ë.0J£ N¿Žœ,kô$ˆôw/]¶ž¹|;ºì
ÛBvíŠÖ 0'¶hDºîç~ÃJÎÓÚVhÀAº¨ÁÔ+BéGª5F-©\•ºåf_Þ6ý`8~÷<1A>áÆ žŽY‡EÇ ¨m¶òÞwÁ:¿\Wö,íÛ&)2¿n6¯¬ÁëŒÏª÷<C2AA>¸‡g±ZÙÌ£¨Ãi&+
Ö¸…ú'0—%w^G<>-Û&k»%_:Ï Y[kä™ ´j ØvFs]ÒO`ÃæXHf- +‰@˜úlÄpu3Èl>öî°9( ² @z³C /çÍdË<1øU×lI¡`ûz‰ö xé<78>tç¼ÉyñáqpPóm&Ä~–Ï´ÝF< W¹rƒ ]Ø{-çâúeÂ4.0ÙMº³Úh-œwDµ••…Æ{bÁšPW©pÃ0“œ5xKÉ… êS)`,Éø {¡„C§qHf<48>xÄ.0®žÍm‡½ÜöXA”/=HI>ȬgÂ…†Ž¹,#Y Nd
ô²YèñCœ«D-ð|ÜsuÛ´ÿúRÍŽÔ*9g2Ùh‰šçéùñ…{ÿÿþr|<þ:¾/T'
endstream
endobj
3 0 obj
704
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 8.000000 8.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
0000000822 00000 n
0000000844 00000 n
0000001015 00000 n
0000001089 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1148
%%EOF

View File

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

View File

@ -1749,6 +1749,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: false, storeAsRecentlyUsed: false).startStandalone() let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: false, storeAsRecentlyUsed: false).startStandalone()
#if DEBUG
if strongSelf.context.sharedContext.applicationBindings.appBuildType == .internal {
if mappedUpdatedReactions.contains(where: {
if case let .custom(fileId, _) = $0, fileId == MessageReaction.starsReactionId {
return true
} else {
return false
}
}) {
let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
|> deliverOnMainQueue).start(next: { [weak strongSelf] files in
guard let strongSelf, let file = files[MessageReaction.starsReactionId] else {
return
}
//TODO:localize
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .starsSent(context: strongSelf.context, file: file, amount: 1, title: "Star Sent", text: "Long tap on {star} to select custom quantity of stars."), elevatedLayout: false, action: { _ in
return false
}), in: .current)
})
}
}
#endif
} }
}) })
}, activateMessagePinch: { [weak self] sourceNode in }, activateMessagePinch: { [weak self] sourceNode in

View File

@ -15,6 +15,8 @@ import TextNodeWithEntities
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import SavedTagNameAlertController import SavedTagNameAlertController
import PremiumUI import PremiumUI
import ChatSendStarsScreen
import ChatMessageItemCommon
extension ChatControllerImpl { extension ChatControllerImpl {
func presentTagPremiumPaywall() { func presentTagPremiumPaywall() {
@ -159,6 +161,41 @@ extension ChatControllerImpl {
self.window?.presentInGlobalOverlay(controller) self.window?.presentInGlobalOverlay(controller)
}) })
} else { } else {
if self.context.sharedContext.applicationBindings.appBuildType == .internal, case .custom(MessageReaction.starsReactionId) = value {
let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId)
|> deliverOnMainQueue).start(next: { [weak self] initialData in
guard let self, let initialData else {
return
}
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount in
guard let self else {
return
}
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
|> deliverOnMainQueue).start(next: { [weak self] files in
guard let self, let file = files[MessageReaction.starsReactionId] else {
return
}
//TODO:localize
let title: String
if amount == 1 {
title = "Star Sent"
} else {
title = "\(amount) Stars Sent"
}
self.present(UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, file: file, amount: amount, title: title, text: nil), elevatedLayout: false, action: { _ in
return false
}), in: .current)
})
}))
})
return
}
var customFileIds: [Int64] = [] var customFileIds: [Int64] = []
if case let .custom(fileId) = value { if case let .custom(fileId) = value {
customFileIds.append(fileId) customFileIds.append(fileId)
@ -175,21 +212,28 @@ extension ChatControllerImpl {
var dismissController: ((@escaping () -> Void) -> Void)? var dismissController: ((@escaping () -> Void) -> Void)?
var items = ContextController.Items(content: .custom(ReactionListContextMenuContent( var items: ContextController.Items
context: self.context, if canViewMessageReactionList(message: message) {
displayReadTimestamps: false, items = ContextController.Items(content: .custom(ReactionListContextMenuContent(
availableReactions: availableReactions, context: self.context,
animationCache: self.controllerInteraction!.presentationContext.animationCache, displayReadTimestamps: false,
animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer, availableReactions: availableReactions,
message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in animationCache: self.controllerInteraction!.presentationContext.animationCache,
dismissController?({ [weak self] in animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer,
guard let self else { message: EngineMessage(message),
return reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in
dismissController?({ [weak self] in
guard let self else {
return
}
self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil)
})
} }
)))
self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil) } else {
}) items = ContextController.Items(content: .list([]))
}))) }
var packReferences: [StickerPackReference] = [] var packReferences: [StickerPackReference] = []
var existingIds = Set<Int64>() var existingIds = Set<Int64>()
@ -315,6 +359,16 @@ extension ChatControllerImpl {
} }
} }
let reactionFile: TelegramMediaFile?
switch value {
case .builtin:
reactionFile = availableReactions?.reactions.first(where: { $0.value == value })?.selectAnimation
case let .custom(fileId):
reactionFile = customEmoji[fileId]
}
items.context = self.context
items.previewReaction = reactionFile
self.canReadHistory.set(false) self.canReadHistory.set(false)
let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture) let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture)

View File

@ -39,6 +39,7 @@ public enum UndoOverlayContent {
case copy(text: String) case copy(text: String)
case mediaSaved(text: String) case mediaSaved(text: String)
case paymentSent(currencyValue: String, itemTitle: String) case paymentSent(currencyValue: String, itemTitle: String)
case starsSent(context: AccountContext, file: TelegramMediaFile, amount: Int64, title: String, text: String?)
case inviteRequestSent(title: String, text: String) case inviteRequestSent(title: String, text: String)
case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?)
case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?)

View File

@ -380,6 +380,69 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = string self.textNode.attributedText = string
displayUndo = false displayUndo = false
self.originalRemainingSeconds = 5 self.originalRemainingSeconds = 5
case let .starsSent(context, file, _, title, text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
let imageBoundingSize = CGSize(width: 34.0, height: 34.0)
let emojiStatus = ComponentView<Empty>()
self.emojiStatus = emojiStatus
let _ = emojiStatus.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
content: .animation(
content: .file(file: file),
size: imageBoundingSize,
placeholderColor: UIColor(white: 1.0, alpha: 0.1),
themeColor: .white,
loopMode: .count(1)
),
isVisibleForAnimations: true,
useSharedAnimation: false,
action: nil
)),
environment: {},
containerSize: imageBoundingSize
)
self.stickerImageSize = imageBoundingSize
if let text {
let formattedString = text
let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString, font: Font.regular(14.0), textColor: .white))
let starRange = (string.string as NSString).range(of: "{star}")
if starRange.location != NSNotFound {
string.replaceCharacters(in: starRange, with: "")
string.insert(NSAttributedString(string: ".", attributes: [
.font: Font.regular(14.0),
ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: MessageReaction.starsReactionId, file: file, custom: nil)
]), at: starRange.location)
}
self.textNode.attributedText = string
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: UIColor(white: 1.0, alpha: 0.1),
attemptSynchronous: false
)
self.textNode.visibility = true
}
//TODO:localize
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
displayUndo = false
self.originalRemainingSeconds = 3
isUserInteractionEnabled = true
case let .messagesUnpinned(title, text, undo, isHidden): case let .messagesUnpinned(title, text, undo, isHidden):
self.avatarNode = nil self.avatarNode = nil
self.iconNode = nil self.iconNode = nil
@ -1232,7 +1295,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content { switch content {
case .removedChat: case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode) self.panelWrapperNode.addSubnode(self.timerTextNode)
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged: case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .starsSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged:
if self.textNode.tapAttributeAction != nil || displayUndo { if self.textNode.tapAttributeAction != nil || displayUndo {
self.isUserInteractionEnabled = true self.isUserInteractionEnabled = true
} else { } else {