diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 23cccaf8d9..145cbda01f 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -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 diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 011398bc0e..7d4154d910 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -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) { diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index d623afc580..259baf3c12 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -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, diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index ad064177f8..f9c89d04e8 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 592efe01ae..ee65de32c0 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -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() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index acf2dcbb5f..ea7827e4fd 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2277,6 +2277,7 @@ public final class ContextController: ViewController, StandalonePresentableContr public var allPresetReactionsAreAvailable: Bool public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? public var disablePositionLock: Bool + public var previewReaction: TelegramMediaFile? public var tip: Tip? public var tipSignal: Signal? public var dismissed: (() -> Void)? @@ -2294,6 +2295,7 @@ public final class ContextController: ViewController, StandalonePresentableContr allPresetReactionsAreAvailable: Bool = false, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal)? = nil, disablePositionLock: Bool = false, + previewReaction: TelegramMediaFile? = nil, tip: Tip? = nil, tipSignal: Signal? = 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 + } } } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 449b9e1f6e..c1feba5545 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -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? { 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? 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?, 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? let dismissed: (() -> Void)? @@ -1042,6 +1057,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta id: AnyHashable?, content: ContextControllerItemsContent, reactionItems: ContextControllerReactionItems?, + previewReaction: ContextControllerPreviewReaction?, tip: ContextController.Tip?, tipSignal: Signal?, 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? 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?, 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 ) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 7eb2349e23..5a1a05e815 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -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() { diff --git a/submodules/ContextUI/Sources/ReactionPreviewView.swift b/submodules/ContextUI/Sources/ReactionPreviewView.swift new file mode 100644 index 0000000000..753087581c --- /dev/null +++ b/submodules/ContextUI/Sources/ReactionPreviewView.swift @@ -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() + + 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 + } + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index e85083e41b..d007db0582 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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 { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 4bd8a3b79d..180f7524c9 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -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) { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index 6caeb1cb8e..6758c55120 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -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) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index bd18f2c52d..a061c44031 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 19f3a9c4fa..a84afc3bc9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 967bbbdc75..4c4c6237fe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 44d2791a2d..9cbc52cc4c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift index bde09fc878..e6156462fc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode/Sources/ChatMessageContactBubbleContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 051a246935..ce5c3d34b3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index 62c4f1e610..a2e2b5deef 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 466041cbeb..20bbc99a73 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 57e01782a4..208c51937c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index bc11d74c99..d119a50ed4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index c9366f7e85..eaacde7b1e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index 783004044f..690ab2d00b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 918afe822c..2c8c18860a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 4d49dca6b9..332531f153 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index c0c186521c..96b9be4346 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 2b47aa6ee7..8d5e633293 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index dd0b686155..9db9e5f4ba 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index ae805ac9af..2055bbcf12 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -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 )) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD new file mode 100644 index 0000000000..5d003e453b --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/BadgeLabelView.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/BadgeLabelView.swift new file mode 100644 index 0000000000..6e24d18396 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/BadgeLabelView.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift new file mode 100644 index 0000000000..d6556c800c --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -0,0 +1,1439 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState +import ComponentFlow +import AccountContext +import ViewControllerComponent +import TelegramCore +import SwiftSignalKit +import Display +import MultilineTextComponent +import ButtonComponent +import PlainButtonComponent +import Markdown +import EmojiStatusComponent +import SliderComponent +import RoundedRectWithTailPath +import AvatarNode +import BundleIconComponent + +private final class BalanceComponent: CombinedComponent { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let balance: Int64? + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + balance: Int64? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.balance = balance + } + + static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.balance != rhs.balance { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let balance = Child(MultilineTextComponent.self) + let icon = Child(EmojiStatusComponent.self) + + return { context in + var size = CGSize(width: 0.0, height: 0.0) + + //TODO:localize + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: "Balance", font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + ), + availableSize: context.availableSize, + transition: .immediate + ) + + size.width = max(size.width, title.size.width) + size.height += title.size.height + + let balanceText: String + if let value = context.component.balance { + balanceText = "\(value)" + } else { + balanceText = "..." + } + let balance = balance.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: balanceText, font: Font.medium(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let iconSize = CGSize(width: 18.0, height: 18.0) + let icon = icon.update( + component: EmojiStatusComponent( + context: context.component.context, + animationCache: context.component.context.animationCache, + animationRenderer: context.component.context.animationRenderer, + content: .animation( + content: .customEmoji(fileId: MessageReaction.starsReactionId), + size: iconSize, + placeholderColor: .gray, + themeColor: nil, + loopMode: .count(0) + ), + isVisibleForAnimations: true, + action: nil + ), + availableSize: iconSize, + transition: context.transition + ) + + let titleSpacing: CGFloat = 1.0 + let iconSpacing: CGFloat = 2.0 + + size.height += titleSpacing + + size.width = max(size.width, icon.size.width + iconSpacing + balance.size.width) + size.height += balance.size.height + + context.add( + title.position( + title.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: title.size)).center + ) + ) + context.add( + balance.position( + balance.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + iconSpacing, y: title.size.height + titleSpacing), size: balance.size)).center + ) + ) + context.add( + icon.position( + icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: title.size.height + titleSpacing), size: icon.size)).center + ) + ) + + return size + } + } +} + +private final class BadgeComponent: Component { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let badgeView: UIView + private let badgeMaskView: UIView + private let badgeShapeLayer = SimpleShapeLayer() + + private let badgeForeground: SimpleLayer + private let badgeIcon: UIImageView + private let badgeLabel: BadgeLabelView + private let badgeLabelMaskView = UIImageView() + + private var badgeTailPosition: CGFloat = 0.0 + private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? + + private var component: BadgeComponent? + + private var previousAvailableSize: CGSize? + + override init(frame: CGRect) { + self.badgeView = UIView() + self.badgeView.alpha = 0.0 + + self.badgeShapeLayer.fillColor = UIColor.white.cgColor + self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.badgeMaskView = UIView() + self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeView.mask = self.badgeMaskView + + self.badgeForeground = SimpleLayer() + + self.badgeIcon = UIImageView() + self.badgeIcon.contentMode = .center + + self.badgeLabel = BadgeLabelView() + let _ = self.badgeLabel.update(value: "0", transition: .immediate) + self.badgeLabel.mask = self.badgeLabelMaskView + + super.init(frame: frame) + + self.addSubview(self.badgeView) + self.badgeView.layer.addSublayer(self.badgeForeground) + self.badgeView.addSubview(self.badgeIcon) + self.badgeView.addSubview(self.badgeLabel) + + self.badgeLabelMaskView.contentMode = .scaleToFill + self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 36.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + ] + var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.isUserInteractionEnabled = false + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + + if self.component == nil { + self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate) + } + + self.component = component + self.badgeIcon.tintColor = .white + + self.badgeLabel.color = .white + + let countWidth: CGFloat + switch component.title.count { + case 1: + countWidth = 20.0 + case 2: + countWidth = 35.0 + case 3: + countWidth = 51.0 + case 4: + countWidth = 60.0 + case 5: + countWidth = 74.0 + case 6: + countWidth = 88.0 + default: + countWidth = 51.0 + } + let badgeWidth: CGFloat = countWidth + 54.0 + + let badgeSize = CGSize(width: badgeWidth, height: 48.0) + let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0) + self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) + self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) + + self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) + + transition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) + + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) + if self.badgeForeground.animation(forKey: "movement") == nil { + self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) + } + + self.badgeIcon.frame = CGRect(x: 10.0, y: 9.0, width: 30.0, height: 30.0) + self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0) + + self.badgeView.alpha = 1.0 + + let size = badgeSize + + let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12)) + transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize)) + + if self.previousAvailableSize != availableSize { + self.previousAvailableSize = availableSize + + let activeColors: [UIColor] = [ + UIColor(rgb: 0xFFAB03), + UIColor(rgb: 0xFFCB37) + ] + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(activeColors.count - 1) + for i in 0 ..< activeColors.count { + locations.append(delta * CGFloat(i)) + } + + let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal) + self.badgeForeground.contentsGravity = .resizeAspectFill + self.badgeForeground.contents = gradient?.cgImage + + self.setupGradientAnimations() + } + + return size + } + + func adjustTail(size: CGSize, overflowWidth: CGFloat) { + var tailPosition = size.width * 0.5 + tailPosition += overflowWidth + tailPosition = max(0.0, min(size.width, tailPosition)) + + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPosition / size.width).cgPath + } + + private func setupGradientAnimations() { + guard let _ = self.component else { + return + } + if let _ = self.badgeForeground.animation(forKey: "movement") { + } else { + CATransaction.begin() + + let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 + let badgePreviousValue = self.badgeForeground.position.x + var badgeNewValue: CGFloat = badgeOffset + if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { + badgeNewValue -= self.badgeForeground.frame.width * 0.35 + } + self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) + + let badgeAnimation = CABasicAnimation(keyPath: "position.x") + badgeAnimation.duration = 4.5 + badgeAnimation.fromValue = badgePreviousValue + badgeAnimation.toValue = badgeNewValue + badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations() + } + self.badgeForeground.add(badgeAnimation, forKey: "movement") + + CATransaction.commit() + } + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class PeerBadgeComponent: Component { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let backgroundMaskLayer = SimpleLayer() + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + private let icon = ComponentView() + + private var component: PeerBadgeComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundMaskLayer) + self.layer.addSublayer(self.backgroundLayer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Premium/SendStarsPeerBadgeStarIcon", + tintColor: .white) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let sideInset: CGFloat = 3.0 + let titleSpacing: CGFloat = 1.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.bold(9.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - titleSpacing - iconSize.width, height: 100.0) + ) + + let contentSize = CGSize(width: iconSize.width + titleSpacing + titleSize.width, height: titleSize.height) + let size = CGSize(width: contentSize.width + sideInset * 2.0, height: contentSize.height + 3.0 * 2.0) + + self.backgroundMaskLayer.backgroundColor = component.theme.list.plainBackgroundColor.cgColor + self.backgroundLayer.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + self.backgroundLayer.frame = backgroundFrame + + let badkgroundMaskFrame = backgroundFrame.insetBy(dx: -1.0 - UIScreenPixel, dy: -1.0 - UIScreenPixel) + self.backgroundMaskLayer.frame = badkgroundMaskFrame + + self.backgroundLayer.cornerRadius = backgroundFrame.height * 0.5 + self.backgroundMaskLayer.cornerRadius = badkgroundMaskFrame.height * 0.5 + + let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + iconSize.width + titleSpacing, y: floor((backgroundFrame.height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + 1.0, y: floor((backgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class PeerComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + } + + static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private var avatarNode: AvatarNode? + private let badge = ComponentView() + private let title = ComponentView() + + private var component: PeerComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(component: PeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 24.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + let avatarSize = CGSize(width: 60.0, height: 60.0) + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: avatarSize) + avatarNode.frame = avatarFrame + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + avatarNode.updateSize(size: avatarFrame.size) + + let badgeSize = self.badge.update( + transition: .immediate, + component: AnyComponent(PeerBadgeComponent( + theme: component.theme, + title: "800" + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + let badgeFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - badgeSize.width) * 0.5), y: avatarFrame.maxY - badgeSize.height + 3.0), size: badgeSize) + if let badgeView = self.badge.view { + if badgeView.superview == nil { + self.addSubview(badgeView) + } + badgeView.frame = badgeFrame + } + + let titleSpacing: CGFloat = 8.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: avatarSize.width + 10.0 * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((avatarSize.width - titleSize.width) * 0.5), y: avatarSize.height + titleSpacing), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + return CGSize(width: avatarSize.width, height: avatarSize.height + titleSpacing + titleSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ChatSendStarsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peer: EnginePeer + let balance: Int64? + let topPeers: [EnginePeer] + let completion: (Int64) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + balance: Int64?, + topPeers: [EnginePeer], + completion: @escaping (Int64) -> Void + ) { + self.context = context + self.peer = peer + self.balance = balance + self.topPeers = topPeers + self.completion = completion + } + + static func ==(lhs: ChatSendStarsScreenComponent, rhs: ChatSendStarsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.balance != rhs.balance { + return false + } + if lhs.topPeers != rhs.topPeers { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let leftButton = ComponentView() + private let closeButton = ComponentView() + + private let title = ComponentView() + private let descriptionText = ComponentView() + + private let slider = ComponentView() + private let sliderBackground = UIView() + private let sliderForeground = UIView() + private let badge = ComponentView() + + private var topPeersLeftSeparator: SimpleLayer? + private var topPeersRightSeparator: SimpleLayer? + private var topPeersTitleBackground: SimpleLayer? + private var topPeersTitle: ComponentView? + + private var topPeerItems: [EnginePeer.Id: ComponentView] = [:] + + private let actionButton = ComponentView() + private let buttonDescriptionText = ComponentView() + + private let bottomOverscrollLimit: CGFloat + + private var ignoreScrolling: Bool = false + + private var component: ChatSendStarsScreenComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? + + private var topOffsetDistance: CGFloat? + + private var amount: Int64 = 1 + private var cachedStarImage: (UIImage, PresentationTheme)? + private var cachedCloseImage: UIImage? + + override init(frame: CGRect) { + self.bottomOverscrollLimit = 200.0 + + self.dimView = UIView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 10.0 + + self.navigationBarContainer = SparseContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.layer.addSublayer(self.backgroundLayer) + + self.addSubview(self.navigationBarContainer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { + return + } + + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + + if topOffset < topOffsetDistance { + targetContentOffset.pointee.y = scrollView.contentOffset.y + scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: ComponentTransition) { + guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + self.topOffsetDistance = topOffsetDistance + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + if let buttonDescriptionTextView = self.buttonDescriptionText.view { + buttonDescriptionTextView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + if let buttonDescriptionTextView = self.buttonDescriptionText.view { + buttonDescriptionTextView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + } + + func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 16.0 + + if self.component == nil { + self.amount = 1 + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor + + var locations: [NSNumber] = [] + var colors: [CGColor] = [] + let numStops = 6 + for i in 0 ..< numStops { + let step = CGFloat(i) / CGFloat(numStops - 1) + locations.append(step as NSNumber) + colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor) + } + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 0.0 + + let sliderInset: CGFloat = sideInset + 8.0 + let sliderSize = self.slider.update( + transition: transition, + component: AnyComponent(SliderComponent( + valueCount: 1000, + value: 0, + markPositions: false, + trackBackgroundColor: .clear, + trackForegroundColor: .clear, + knobSize: 26.0, + knobColor: .white, + valueUpdated: { [weak self] value in + guard let self else { + return + } + self.amount = 1 + Int64(value) + self.state?.updated(transition: .immediate) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0) + ) + let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize) + if let sliderView = self.slider.view { + if sliderView.superview == nil { + self.scrollContentView.addSubview(self.sliderBackground) + self.scrollContentView.addSubview(self.sliderForeground) + self.scrollContentView.addSubview(sliderView) + } + transition.setFrame(view: sliderView, frame: sliderFrame) + + self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF) + self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D) + + let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0)) + transition.setFrame(view: self.sliderBackground, frame: sliderBackgroundFrame) + + let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(1000 - 1) + let sliderMinWidth = sliderBackgroundFrame.height + let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth + let sliderForegroundFrame = CGRect(origin: CGPoint(x: sliderBackgroundFrame.minX, y: sliderBackgroundFrame.minY), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) + transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame) + + self.sliderBackground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 + self.sliderForeground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 + + self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth + + let badgeSize = self.badge.update( + transition: transition, + component: AnyComponent(BadgeComponent( + theme: environment.theme, title: "\(self.amount)") + ), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize) + if let badgeView = self.badge.view as? BadgeComponent.View { + if badgeView.superview == nil { + self.scrollContentView.addSubview(badgeView) + } + + let badgeSideInset = sideInset + 15.0 + + let badgeOverflowWidth: CGFloat + if badgeFrame.minX - badgeSize.width * 0.5 < badgeSideInset { + badgeOverflowWidth = badgeSideInset - (badgeFrame.minX - badgeSize.width * 0.5) + } else if badgeFrame.minX + badgeSize.width * 0.5 > availableSize.width - badgeSideInset { + badgeOverflowWidth = availableSize.width - badgeSideInset - (badgeFrame.minX + badgeSize.width * 0.5) + } else { + badgeOverflowWidth = 0.0 + } + + badgeFrame.origin.x += badgeOverflowWidth + + badgeView.frame = badgeFrame + + badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth) + } + } + + contentHeight += 123.0 + + let leftButtonSize = self.leftButton.update( + transition: transition, + component: AnyComponent(BalanceComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + balance: component.balance + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: leftButtonSize) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.navigationBarContainer.addSubview(leftButtonView) + } + transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + } + + if themeUpdated { + self.cachedCloseImage = nil + } + let closeImage: UIImage + if let current = self.cachedCloseImage { + closeImage = current + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! + self.cachedCloseImage = closeImage + } + let closeButtonSize = self.closeButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(Image(image: closeImage)), + effectAlignment: .center, + action: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - closeButtonSize.width, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + let title = self.title + let descriptionText = self.descriptionText + let actionButton = self.actionButton + + let titleSize = title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "React with Stars", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + contentHeight += 56.0 + contentHeight += 8.0 + + let text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post." + + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + + let descriptionTextSize = descriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: text, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) + ) + let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) + if let descriptionTextView = descriptionText.view { + if descriptionTextView.superview == nil { + self.scrollContentView.addSubview(descriptionTextView) + } + transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) + } + + contentHeight += descriptionTextFrame.height + contentHeight += 22.0 + contentHeight += 2.0 + + if !component.topPeers.isEmpty { + contentHeight += 3.0 + + let topPeersLeftSeparator: SimpleLayer + if let current = self.topPeersLeftSeparator { + topPeersLeftSeparator = current + } else { + topPeersLeftSeparator = SimpleLayer() + self.topPeersLeftSeparator = topPeersLeftSeparator + self.scrollContentView.layer.addSublayer(topPeersLeftSeparator) + } + + let topPeersRightSeparator: SimpleLayer + if let current = self.topPeersRightSeparator { + topPeersRightSeparator = current + } else { + topPeersRightSeparator = SimpleLayer() + self.topPeersRightSeparator = topPeersRightSeparator + self.scrollContentView.layer.addSublayer(topPeersRightSeparator) + } + + let topPeersTitleBackground: SimpleLayer + if let current = self.topPeersTitleBackground { + topPeersTitleBackground = current + } else { + topPeersTitleBackground = SimpleLayer() + self.topPeersTitleBackground = topPeersTitleBackground + self.scrollContentView.layer.addSublayer(topPeersTitleBackground) + } + + let topPeersTitle: ComponentView + if let current = self.topPeersTitle { + topPeersTitle = current + } else { + topPeersTitle = ComponentView() + self.topPeersTitle = topPeersTitle + } + + topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + + //TODO:localize + let topPeersTitleSize = topPeersTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Top Senders", font: Font.semibold(15.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 300.0, height: 100.0) + ) + let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0) + let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize) + + topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor + topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5 + transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame) + + let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize) + if let topPeersTitleView = topPeersTitle.view { + if topPeersTitleView.superview == nil { + self.scrollContentView.addSubview(topPeersTitleView) + } + transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame) + } + + let separatorY = topPeersBackgroundFrame.midY + let separatorSpacing: CGFloat = 10.0 + transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) + transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) + + var validIds: [EnginePeer.Id] = [] + var items: [(itemView: ComponentView, size: CGSize)] = [] + for topPeer in component.topPeers { + validIds.append(topPeer.id) + + let itemView: ComponentView + if let current = self.topPeerItems[topPeer.id] { + itemView = current + } else { + itemView = ComponentView() + self.topPeerItems[topPeer.id] = itemView + } + + let itemSize = itemView.update( + transition: .immediate, + component: AnyComponent(PeerComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: topPeer + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + items.append((itemView, itemSize)) + } + var removedIds: [EnginePeer.Id] = [] + for (id, itemView) in self.topPeerItems { + if !validIds.contains(id) { + removedIds.append(id) + itemView.view?.removeFromSuperview() + } + } + for id in removedIds { + self.topPeerItems.removeValue(forKey: id) + } + + var itemsWidth: CGFloat = 0.0 + for (_, itemSize) in items { + itemsWidth += itemSize.width + } + + let maxItemSpacing = 48.0 + var itemSpacing = floor((availableSize.width - itemsWidth) / CGFloat(items.count + 1)) + itemSpacing = min(itemSpacing, maxItemSpacing) + + let totalWidth = itemsWidth + itemSpacing * CGFloat(items.count + 1) + var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing + for (itemView, itemSize) in items { + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.scrollContentView.addSubview(itemComponentView) + } + itemComponentView.frame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) + } + itemX += itemSize.width + itemSpacing + } + + contentHeight += 161.0 + } + + initialContentHeight = contentHeight + + if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { + self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) + } + + let buttonString = "Send # \(self.amount)" + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) + } + + let actionButtonSize = actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.completion(self.amount) + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + + let buttonDescriptionTextSize = self.buttonDescriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: "By sending Stars you agree to the [Terms of Service]()", attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) + ) + let buttonDescriptionSpacing: CGFloat = 14.0 + + let bottomPanelHeight = 13.0 + environment.safeInsets.bottom + actionButtonSize.height + buttonDescriptionSpacing + buttonDescriptionTextSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + let buttonDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonDescriptionTextSize.width) * 0.5), y: actionButtonFrame.maxY + buttonDescriptionSpacing), size: buttonDescriptionTextSize) + if let buttonDescriptionTextView = buttonDescriptionText.view { + if buttonDescriptionTextView.superview == nil { + self.addSubview(buttonDescriptionTextView) + } + transition.setFrame(view: buttonDescriptionTextView, frame: buttonDescriptionTextFrame) + } + + contentHeight += bottomPanelHeight + initialContentHeight += bottomPanelHeight + + clippingY = actionButtonFrame.minY - 24.0 + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 10.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class ChatSendStarsScreen: ViewControllerComponentContainer { + public final class InitialData { + let peer: EnginePeer + let balance: Int64? + let topPeers: [EnginePeer] + + fileprivate init( + peer: EnginePeer, + balance: Int64?, + topPeers: [EnginePeer] + ) { + self.peer = peer + self.balance = balance + self.topPeers = topPeers + } + } + + private let context: AccountContext + + private var isDismissed: Bool = false + + private var presenceDisposable: Disposable? + + public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64) -> Void) { + self.context = context + + super.init(context: context, component: ChatSendStarsScreenComponent( + context: context, + peer: initialData.peer, + balance: initialData.balance, + topPeers: initialData.topPeers, + completion: completion + ), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presenceDisposable?.dispose() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { + componentView.animateIn() + } + } + + public static func initialData(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + let balance: Signal + if let starsContext = context.starsContext { + balance = starsContext.state + |> map { state in + return state?.balance + } + |> take(1) + } else { + balance = .single(nil) + } + + return combineLatest( + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + balance + ) + |> map { peer, accountPeer, balance -> InitialData? in + guard let peer, let accountPeer else { + return nil + } + + return InitialData( + peer: peer, + balance: balance, + topPeers: [accountPeer, peer] + ) + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift index c43d7117dd..869462e11b 100644 --- a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift +++ b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift @@ -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 - return context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) - |> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in - return (allowedReactions, files) + 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() #if DEBUG - if "".isEmpty { + if context.sharedContext.applicationBindings.appBuildType == .internal { if let file = allowedReactionsAndFiles.files[MessageReaction.starsReactionId] { existingIds.insert(.custom(MessageReaction.starsReactionId)) diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 746a4ca950..ba062d1d2a 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -60,6 +60,7 @@ final class PeerAllowedReactionsScreenComponent: Component { private var reactionsInfoText: ComponentView? private var reactionInput: ComponentView? private var reactionCountSection: ComponentView? + private var paidReactionsSection: ComponentView? private let actionButton = ComponentView() private var reactionSelectionControl: ComponentView? @@ -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 - contentHeight += 12.0 + + if "".isEmpty { + contentHeight += 32.0 + + let paidReactionsSection: ComponentView + 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] = [] diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift index c8a0e106e4..081731cbb3 100644 --- a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -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 - sliderView.lineSize = 4.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 - 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))) - }) + 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) diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/Contents.json new file mode 100644 index 0000000000..4fa2af457d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/star8.pdf b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/star8.pdf new file mode 100644 index 0000000000..503aceed13 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsPeerBadgeStarIcon.imageset/star8.pdf @@ -0,0 +1,62 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Filter /FlateDecode + /Length 3 0 R + >> +stream +xe͊G -~kNcSxw{Z:utwvK)߂__Z+ J||x9x"izNIkjx4\-'t8 L|siURu ->YZ!ģ2pGXu6|YҼں!!s5õ.kp9 uڵ<.0JN,k$w/]|; +Bv 0'hD~JVhA+BG5F-\f_6`8~ƠYE mw:\W,&)2n6ϪtgẒi&+ +ָ'0%w^G[-&k%_:ϠY[k j vFs]O`XHf- +@lpu3l>9( @zC /d<1UlI`z xtyqpPm&~ϴF< Wr ]{-e4.0Mh-wD{bPWp05xKɅ0S)`, {CqHfx.0mXA/=HI>Ȭg…,#Y Nd +YCD-|su۴R͎*yӖ9g2h{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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/Contents.json new file mode 100644 index 0000000000..17da40a8ad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/star24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/star24.pdf new file mode 100644 index 0000000000..4c22fe1f79 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/SendStarsStarSliderIcon.imageset/star24.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 90f223fdba..f7cd0a4b84 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index e36db96f85..f65710f110 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -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,21 +212,28 @@ extension ChatControllerImpl { var dismissController: ((@escaping () -> Void) -> Void)? - var 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 - dismissController?({ [weak self] in - guard let self else { - return + 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 + 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 existingIds = Set() @@ -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) diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 01fabaa0ac..030e3c3a58 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -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)?) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 10ceb37411..0d68fcff21 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -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() + 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 {