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

View File

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

View File

@ -391,6 +391,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private final class ItemNode: HighlightTrackingButtonNode {
let context: AccountContext
let displayReadTimestamps: Bool
let displayReactionIcon: Bool
let availableReactions: AvailableReactions?
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
@ -411,10 +412,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
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.context = context
self.displayReadTimestamps = displayReadTimestamps
self.displayReactionIcon = displayReactionIcon
self.availableReactions = availableReactions
self.animationCache = animationCache
self.animationRenderer = animationRenderer
@ -548,7 +550,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
let reaction: MessageReaction.Reaction? = item.reaction
if reaction != self.item?.reaction {
if self.displayReactionIcon, reaction != self.item?.reaction {
if let reaction = reaction {
switch reaction {
case .builtin:
@ -802,6 +804,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
private let context: AccountContext
private let displayReadTimestamps: Bool
private let displayReactionIcons: Bool
private let availableReactions: AvailableReactions?
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
@ -833,6 +836,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
init(
context: AccountContext,
displayReadTimestamps: Bool,
displayReactionIcons: Bool,
availableReactions: AvailableReactions?,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
@ -845,6 +849,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
) {
self.context = context
self.displayReadTimestamps = displayReadTimestamps
self.displayReactionIcons = displayReactionIcons
self.availableReactions = availableReactions
self.animationCache = animationCache
self.animationRenderer = animationRenderer
@ -955,7 +960,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
} else {
let openPeer = self.openPeer
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)
})
self.itemNodes[index] = itemNode
@ -1104,6 +1109,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode, ASGestureRecognizerDelegate {
private let context: AccountContext
private let displayReadTimestamps: Bool
private let displayReactionIcons: Bool
private let availableReactions: AvailableReactions?
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
@ -1148,6 +1154,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
) {
self.context = context
self.displayReadTimestamps = displayReadTimestamps
self.displayReactionIcons = reaction == nil
self.availableReactions = availableReactions
self.animationCache = animationCache
self.animationRenderer = animationRenderer
@ -1159,9 +1166,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.requestUpdate = requestUpdate
self.requestUpdateApparentHeight = requestUpdateApparentHeight
//var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
//var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)?
if let back = back {
self.backButtonNode = BackButtonNode()
self.backButtonNode?.action = {
@ -1218,45 +1222,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
strongSelf.tabListNode?.scrollToTabReaction = ReactionTabListNode.ScrollToTabReaction(value: reaction)
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))
}
/*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
guard let strongSelf = self else {
return []
@ -1371,6 +1339,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
tabNode = ReactionsTabNode(
context: self.context,
displayReadTimestamps: self.displayReadTimestamps,
displayReactionIcons: self.displayReactionIcons,
availableReactions: self.availableReactions,
animationCache: self.animationCache,
animationRenderer: self.animationRenderer,

View File

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

View File

@ -436,6 +436,12 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
self.targetSelectionIndex = nil
icon = 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()

View File

@ -2277,6 +2277,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var allPresetReactionsAreAvailable: Bool
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
public var disablePositionLock: Bool
public var previewReaction: TelegramMediaFile?
public var tip: Tip?
public var tipSignal: Signal<Tip?, NoError>?
public var dismissed: (() -> Void)?
@ -2294,6 +2295,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
allPresetReactionsAreAvailable: Bool = false,
getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil,
disablePositionLock: Bool = false,
previewReaction: TelegramMediaFile? = nil,
tip: Tip? = nil,
tipSignal: Signal<Tip?, NoError>? = nil,
dismissed: (() -> Void)? = nil
@ -2310,6 +2312,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable
self.getEmojiContent = getEmojiContent
self.disablePositionLock = disablePositionLock
self.previewReaction = previewReaction
self.tip = tip
self.tipSignal = tipSignal
self.dismissed = dismissed
@ -2327,6 +2330,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.allPresetReactionsAreAvailable = false
self.getEmojiContent = nil
self.disablePositionLock = false
self.previewReaction = nil
self.tip = nil
self.tipSignal = nil
self.dismissed = nil
@ -2345,6 +2349,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
case messageCopyProtection(isChannel: Bool)
case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?)
case notificationTopicExceptions(text: String, action: (() -> Void)?)
case starsReactions(topCount: Int)
public static func ==(lhs: Tip, rhs: Tip) -> Bool {
switch lhs {
@ -2390,6 +2395,12 @@ public final class ContextController: ViewController, StandalonePresentableContr
} else {
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 {
func node(
getController: @escaping () -> ContextControllerProtocol?,
@ -71,6 +81,7 @@ public protocol ContextControllerActionsStackItem: AnyObject {
var tip: ContextController.Tip? { get }
var tipSignal: Signal<ContextController.Tip?, NoError>? { get }
var reactionItems: ContextControllerReactionItems? { get }
var previewReaction: ContextControllerPreviewReaction? { get }
var dismissed: (() -> Void)? { get }
}
@ -936,6 +947,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
public let id: AnyHashable?
public let items: [ContextMenuItem]
public let reactionItems: ContextControllerReactionItems?
public let previewReaction: ContextControllerPreviewReaction?
public let tip: ContextController.Tip?
public let tipSignal: Signal<ContextController.Tip?, NoError>?
public let dismissed: (() -> Void)?
@ -944,6 +956,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
id: AnyHashable?,
items: [ContextMenuItem],
reactionItems: ContextControllerReactionItems?,
previewReaction: ContextControllerPreviewReaction?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
@ -951,6 +964,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
self.id = id
self.items = items
self.reactionItems = reactionItems
self.previewReaction = previewReaction
self.tip = tip
self.tipSignal = tipSignal
self.dismissed = dismissed
@ -1034,6 +1048,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
let id: AnyHashable?
private let content: ContextControllerItemsContent
let reactionItems: ContextControllerReactionItems?
let previewReaction: ContextControllerPreviewReaction?
let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>?
let dismissed: (() -> Void)?
@ -1042,6 +1057,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
id: AnyHashable?,
content: ContextControllerItemsContent,
reactionItems: ContextControllerReactionItems?,
previewReaction: ContextControllerPreviewReaction?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
@ -1049,6 +1065,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
self.id = id
self.content = content
self.reactionItems = reactionItems
self.previewReaction = previewReaction
self.tip = tip
self.tipSignal = tipSignal
self.dismissed = dismissed
@ -1084,13 +1101,17 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C
getEmojiContent: items.getEmojiContent
)
}
var previewReaction: ContextControllerPreviewReaction?
if let context = items.context, let file = items.previewReaction {
previewReaction = ContextControllerPreviewReaction(context: context, file: file)
}
switch items.content {
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):
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):
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>?
var tipNode: InnerTextSelectionTipContainerNode?
let reactionItems: ContextControllerReactionItems?
let previewReaction: ContextControllerPreviewReaction?
let itemDismissed: (() -> Void)?
var storedScrollingState: CGFloat?
let positionLock: CGFloat?
@ -1222,6 +1244,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
reactionItems: ContextControllerReactionItems?,
previewReaction: ContextControllerPreviewReaction?,
itemDismissed: (() -> Void)?,
positionLock: CGFloat?
) {
@ -1240,6 +1263,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
self.dimNode.alpha = 0.0
self.reactionItems = reactionItems
self.previewReaction = previewReaction
self.itemDismissed = itemDismissed
self.positionLock = positionLock
@ -1376,6 +1400,10 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
return self.itemContainers.last?.reactionItems
}
public var topPreviewReaction: ContextControllerPreviewReaction? {
return self.itemContainers.last?.previewReaction
}
public var topPositionLock: CGFloat? {
return self.itemContainers.last?.positionLock
}
@ -1509,6 +1537,7 @@ public final class ContextControllerActionsStackNode: ASDisplayNode {
tip: item.tip,
tipSignal: item.tipSignal,
reactionItems: item.reactionItems,
previewReaction: item.previewReaction,
itemDismissed: item.dismissed,
positionLock: positionLock
)

View File

@ -241,6 +241,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
private let scrollNode: ASDisplayNode
private var reactionContextNode: ReactionContextNode?
private var reactionPreviewView: ReactionPreviewView?
private var reactionContextNodeIsAnimatingOut: Bool = false
private var itemContentNode: ItemContentNode?
@ -637,6 +638,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
var animateReactionsIn = false
var contentTopInset: CGFloat = topInset
var removedReactionContextNode: ReactionContextNode?
if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty {
let reactionContextNode: ReactionContextNode
if let current = self.reactionContextNode {
@ -733,6 +735,25 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
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 {
switch stateTransition {
case .animateIn, .animateOut:
@ -963,6 +984,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
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 {
transition.updateAlpha(node: self.actionsStackNode, alpha: 0.0)
} 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)
var actionsFrame: CGRect
@ -1205,6 +1240,29 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
damping: springDamping,
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 {
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)
@ -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 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
if emojiContent.panelItemGroups.count == 1 && emojiContent.panelItemGroups[0].groupId == AnyHashable("recent") {
strongSelf.hideExpandedTopPanel = true
}
var emojiContent = emojiContent
if let emojiSearchResult = emojiSearchState.result {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -967,7 +967,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
replyCount: dateAndStatus.dateReplies,
isPinned: dateAndStatus.isPinned,
hasAutoremove: message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: message, isInline: associatedData.isInline),
canViewReactionList: canViewMessageReactionList(message: message),
animationCache: presentationContext.animationCache,
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 canViewList = false
for attribute in message.attributes {

View File

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

View File

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

View File

@ -362,16 +362,13 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
let itemValue = item.value
let itemNode = item.node
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.activated = { [weak itemNode] gesture, _ in
guard let strongSelf = self, let itemNode = itemNode else {
gesture.cancel()
return
}
if !canViewReactionList {
return
}
strongSelf.openReactionPreview?(gesture, itemNode.view.containerView, itemValue)
}
item.node.view.additionalActivationProgressLayer = itemMaskView.layer

View File

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

View File

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

View File

@ -649,7 +649,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread),
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage, isInline: item.associatedData.isInline),
canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache,
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 DEBUG
var reactions = reactions
if "".isEmpty {
if context.sharedContext.applicationBindings.appBuildType == .internal {
reactions.insert(.custom(MessageReaction.starsReactionId))
}
#endif
@ -277,13 +277,14 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe
}
} else {
#if DEBUG
if context.sharedContext.applicationBindings.appBuildType == .internal {
return context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId])
|> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in
return (allowedReactions, files)
}
#else
return .single((allowedReactions, [:]))
}
#endif
return .single((allowedReactions, [:]))
}
}
@ -302,7 +303,7 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe
var existingIds = Set<MessageReaction.Reaction>()
#if DEBUG
if "".isEmpty {
if context.sharedContext.applicationBindings.appBuildType == .internal {
if let file = allowedReactionsAndFiles.files[MessageReaction.starsReactionId] {
existingIds.insert(.custom(MessageReaction.starsReactionId))

View File

@ -60,6 +60,7 @@ final class PeerAllowedReactionsScreenComponent: Component {
private var reactionsInfoText: ComponentView<Empty>?
private var reactionInput: ComponentView<Empty>?
private var reactionCountSection: ComponentView<Empty>?
private var paidReactionsSection: ComponentView<Empty>?
private let actionButton = ComponentView<Empty>()
private var reactionSelectionControl: ComponentView<Empty>?
@ -79,6 +80,8 @@ final class PeerAllowedReactionsScreenComponent: Component {
private var allowedReactionCount: Int = 11
private var appliedReactionSettings: PeerReactionSettings?
private var areStarsReactionsEnabled: Bool = true
private var emojiContent: EmojiPagerContentComponent?
private var emojiContentDisposable: Disposable?
private var caretPosition: Int?
@ -93,6 +96,8 @@ final class PeerAllowedReactionsScreenComponent: Component {
private weak var currentUndoController: UndoOverlayController?
private var cachedChevronImage: (UIImage, PresentationTheme)?
override init(frame: CGRect) {
self.scrollView = UIScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -862,7 +867,110 @@ final class PeerAllowedReactionsScreenComponent: Component {
}
}
contentHeight += reactionCountSectionSize.height
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 {
if let reactionsTitleText = self.reactionsTitleText {
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>] = []

View File

@ -12,6 +12,8 @@ public final class SliderComponent: Component {
public let markPositions: Bool
public let trackBackgroundColor: UIColor
public let trackForegroundColor: UIColor
public let knobSize: CGFloat?
public let knobColor: UIColor?
public let valueUpdated: (Int) -> Void
public let isTrackingUpdated: ((Bool) -> Void)?
@ -21,6 +23,8 @@ public final class SliderComponent: Component {
markPositions: Bool,
trackBackgroundColor: UIColor,
trackForegroundColor: UIColor,
knobSize: CGFloat? = nil,
knobColor: UIColor? = nil,
valueUpdated: @escaping (Int) -> Void,
isTrackingUpdated: ((Bool) -> Void)? = nil
) {
@ -29,6 +33,8 @@ public final class SliderComponent: Component {
self.markPositions = markPositions
self.trackBackgroundColor = trackBackgroundColor
self.trackForegroundColor = trackForegroundColor
self.knobSize = knobSize
self.knobColor = knobColor
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
@ -49,6 +55,12 @@ public final class SliderComponent: Component {
if lhs.trackForegroundColor != rhs.trackForegroundColor {
return false
}
if lhs.knobSize != rhs.knobSize {
return false
}
if lhs.knobColor != rhs.knobColor {
return false
}
return true
}
@ -94,8 +106,12 @@ public final class SliderComponent: Component {
} else {
sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
if let knobSize = component.knobSize {
sliderView.lineSize = knobSize + 4.0
} else {
sliderView.lineSize = 4.0
}
sliderView.trackCornerRadius = sliderView.lineSize * 0.5
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
@ -110,12 +126,25 @@ public final class SliderComponent: Component {
sliderView.backColor = component.trackBackgroundColor
sliderView.startColor = component.trackBackgroundColor
sliderView.trackColor = component.trackForegroundColor
if let knobSize = component.knobSize {
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)
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.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()
#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

View File

@ -15,6 +15,8 @@ import TextNodeWithEntities
import ChatPresentationInterfaceState
import SavedTagNameAlertController
import PremiumUI
import ChatSendStarsScreen
import ChatMessageItemCommon
extension ChatControllerImpl {
func presentTagPremiumPaywall() {
@ -159,6 +161,41 @@ extension ChatControllerImpl {
self.window?.presentInGlobalOverlay(controller)
})
} 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] = []
if case let .custom(fileId) = value {
customFileIds.append(fileId)
@ -175,13 +212,16 @@ extension ChatControllerImpl {
var dismissController: ((@escaping () -> Void) -> Void)?
var items = ContextController.Items(content: .custom(ReactionListContextMenuContent(
var items: ContextController.Items
if canViewMessageReactionList(message: message) {
items = ContextController.Items(content: .custom(ReactionListContextMenuContent(
context: self.context,
displayReadTimestamps: false,
availableReactions: availableReactions,
animationCache: self.controllerInteraction!.presentationContext.animationCache,
animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer,
message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in
message: EngineMessage(message),
reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in
dismissController?({ [weak self] in
guard let self else {
return
@ -189,7 +229,11 @@ extension ChatControllerImpl {
self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil)
})
})))
}
)))
} else {
items = ContextController.Items(content: .list([]))
}
var packReferences: [StickerPackReference] = []
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)
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 mediaSaved(text: 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 image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?)
case notificationSoundAdded(title: String, text: String, action: (() -> Void)?)

View File

@ -380,6 +380,69 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = string
displayUndo = false
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):
self.avatarNode = nil
self.iconNode = nil
@ -1232,7 +1295,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content {
case .removedChat:
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 {
self.isUserInteractionEnabled = true
} else {