Channel boost

This commit is contained in:
Ilya Laktyushin 2023-09-13 17:21:53 +04:00
parent 9de5689328
commit f22e9176f2
31 changed files with 1674 additions and 486 deletions

View File

@ -300,6 +300,7 @@ public enum ResolvedUrl {
case premiumOffer(reference: String?)
case chatFolder(slug: String)
case story(peerId: PeerId, id: Int32)
case boost(peerId: PeerId, status: ChannelBoostStatus?, canApplyStatus: CanApplyBoostStatus)
}
public enum NavigateToChatKeepStack {
@ -896,7 +897,7 @@ public protocol SharedAccountContext: AnyObject {
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController
func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Void) -> ViewController
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController
@ -953,6 +954,7 @@ public enum PremiumIntroSource {
case storiesFormatting
case storiesExpirationDurations
case storiesSuggestedReactions
case channelBoost(EnginePeer.Id)
}
public enum PremiumDemoSubject {
@ -985,7 +987,7 @@ public enum PremiumLimitSubject {
case expiringStories
case storiesWeekly
case storiesMonthly
case storiesChannelBoost(level: Int32, link: String?)
case storiesChannelBoost(peer: EnginePeer, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, boosted: Bool)
}
public protocol ComposeController: ViewController {

View File

@ -284,7 +284,9 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
let count = data.includePeers.peers.count - 1
if count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
return true
})
chatListController?.push(controller)
return
} else if count >= limit && !isPremium {
@ -292,6 +294,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -384,10 +387,14 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
let isPremium = limitsData.0?.isPremium ?? false
if isPremium {
if case .filter = location {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
return true
})
chatListController?.push(controller)
} else {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
return true
})
chatListController?.push(controller)
}
} else {
@ -396,6 +403,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
replaceImpl?(premiumScreen)
return true
})
chatListController?.push(controller)
replaceImpl = { [weak controller] c in
@ -406,6 +414,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
replaceImpl?(premiumScreen)
return true
})
chatListController?.push(controller)
replaceImpl = { [weak controller] c in

View File

@ -1418,6 +1418,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -1533,6 +1534,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -1576,6 +1578,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -1604,7 +1607,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let premiumLimit = premiumLimits.maxFolderChatsCount
if data.includePeers.peers.count >= premiumLimit {
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {})
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {
return true
})
strongSelf.push(controller)
f(.dismissWithoutContent)
return
@ -1613,6 +1618,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .chatsPerFolder)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -2134,6 +2140,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -2607,6 +2614,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = PremiumLimitScreen(context: context, subject: .expiringStories, count: Int32(storiesCount), action: {
let controller = PremiumIntroScreen(context: context, source: .stories)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -5631,6 +5639,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -792,7 +792,9 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
chatListFilters: allFilters
)), options: [], filters: [], alwaysEnabled: true, limit: isPremium ? premiumLimit : limit, reachedLimit: { count in
if count >= premiumLimit {
let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: min(premiumLimit, count), action: {})
let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: min(premiumLimit, count), action: {
return true
})
pushImpl?(limitController)
return
} else if count >= limit && !isPremium {
@ -800,6 +802,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: count, action: {
let introController = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(introController)
return true
})
replaceImpl = { [weak limitController] c in
limitController?.replace(with: c)
@ -1226,7 +1229,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
let premiumLimit = premiumLimits.maxFolderChatsCount
if currentIncludePeers.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(currentIncludePeers.count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(currentIncludePeers.count), action: {
return true
})
pushControllerImpl?(controller)
return
} else if currentIncludePeers.count >= limit && !isPremium {
@ -1234,6 +1239,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(currentIncludePeers.count), action: {
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -1914,6 +1920,7 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
case let .sharedFolderLimitExceeded(limit, _):
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: {
pushPremiumController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders))
return true
})
pushController(limitController)
@ -1921,6 +1928,7 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
case let .limitExceeded(limit, _):
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, forceDark: false, cancel: {}, action: {
pushPremiumController(PremiumIntroScreen(context: context, source: .linksPerSharedFolder))
return true
})
pushController(limitController)
@ -1928,6 +1936,7 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
case let .tooManyChannels(limit, _):
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, forceDark: false, cancel: {}, action: {
pushPremiumController(PremiumIntroScreen(context: context, source: .groupsAndChannels))
return true
})
pushController(limitController)
@ -1935,6 +1944,7 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
case let .tooManyChannelsInAccount(limit, _):
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: {
pushPremiumController(PremiumIntroScreen(context: context, source: .groupsAndChannels))
return true
})
pushController(limitController)

View File

@ -304,7 +304,9 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let limit = limits.maxFoldersCount
let premiumLimit = premiumLimits.maxFoldersCount
if filters.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
return true
})
pushControllerImpl?(controller)
return
} else if filters.count >= limit && !isPremium {
@ -312,6 +314,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
@ -353,7 +356,9 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let limit = limits.maxFoldersCount
let premiumLimit = premiumLimits.maxFoldersCount
if filters.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
return true
})
pushControllerImpl?(controller)
return
} else if filters.count >= limit && !isPremium {
@ -361,6 +366,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -1433,10 +1433,14 @@ public final class ChatListNode: ListView {
case let .limitExceeded(count, _):
if isPremium {
if case .filter = location {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
return true
})
strongSelf.push?(controller)
} else {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {})
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
return true
})
strongSelf.push?(controller)
}
} else {
@ -1445,6 +1449,7 @@ public final class ChatListNode: ListView {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
replaceImpl?(premiumScreen)
return true
})
strongSelf.push?(controller)
replaceImpl = { [weak controller] c in
@ -1455,6 +1460,7 @@ public final class ChatListNode: ListView {
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
replaceImpl?(premiumScreen)
return true
})
strongSelf.push?(controller)
replaceImpl = { [weak controller] c in

View File

@ -409,6 +409,29 @@ public struct Transition {
}
}
public func setAnchorPoint(layer: CALayer, anchorPoint: CGPoint, completion: ((Bool) -> Void)? = nil) {
if layer.anchorPoint == anchorPoint {
completion?(true)
return
}
switch self.animation {
case .none:
layer.anchorPoint = anchorPoint
layer.removeAnimation(forKey: "anchorPoint")
completion?(true)
case .curve:
let previousAnchorPoint: CGPoint
if layer.animation(forKey: "anchorPoint") != nil, let presentation = layer.presentation() {
previousAnchorPoint = presentation.anchorPoint
} else {
previousAnchorPoint = layer.anchorPoint
}
layer.anchorPoint = anchorPoint
self.animateAnchorPoint(layer: layer, from: previousAnchorPoint, to: layer.anchorPoint, completion: completion)
}
}
public func attachAnimation(view: UIView, id: String, completion: @escaping (Bool) -> Void) {
switch self.animation {
case .none:
@ -738,6 +761,26 @@ public struct Transition {
)
}
}
public func animateAnchorPoint(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:
completion?(true)
case let .curve(duration, curve):
layer.animate(
from: NSValue(cgPoint: fromValue),
to: NSValue(cgPoint: toValue),
keyPath: "anchorPoint",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: additive,
completion: completion
)
}
}
public func animateBounds(layer: CALayer, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self.animation {

View File

@ -56,12 +56,13 @@ public class ItemListPlaceholderItem: ListViewItem, ItemListItem {
public let selectable = false
}
private let textFont = Font.regular(13.0)
private let textFont = Font.regular(15.0)
public class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
public let textNode: TextNode
@ -80,6 +81,9 @@ public class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode {
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
@ -118,7 +122,7 @@ public class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode {
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let height: CGFloat = 34.0 + textLayout.size.height
let height: CGFloat = 42.0 + textLayout.size.height
switch item.style {
case .plain:
@ -156,7 +160,9 @@ public class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode {
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
@ -168,26 +174,39 @@ public class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode {
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - textLayout.size.width) / 2.0), y: 17.0), size: textLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - textLayout.size.width) / 2.0), y: floorToScreenPixels((height - textLayout.size.height) / 2.0)), size: textLayout.size)
}
})
}

View File

@ -226,6 +226,12 @@ public enum PremiumSource: Equatable {
} else {
return false
}
case let .channelBoost(peerId):
if case .channelBoost(peerId) = rhs {
return true
} else {
return false
}
}
}
@ -262,6 +268,7 @@ public enum PremiumSource: Equatable {
case storiesFormatting
case storiesExpirationDurations
case storiesSuggestedReactions
case channelBoost(EnginePeer.Id)
var identifier: String? {
switch self {
@ -333,6 +340,8 @@ public enum PremiumSource: Equatable {
return "stories__expiration_durations"
case .storiesSuggestedReactions:
return "stories__suggested_reactions"
case let .channelBoost(peerId):
return "channel_boost__\(peerId.id._internalGetInt64Value())"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -219,6 +219,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: context, source: .accounts)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -158,6 +158,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: context, source: .accounts)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -33,6 +33,8 @@ swift_library(
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/ContextUI:ContextUI",
"//submodules/PremiumUI:PremiumUI",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/ShareController:ShareController",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,165 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import Markdown
import PremiumUI
import ComponentFlow
final class BoostLevelHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let count: Int32
let position: CGFloat
let activeText: String
let inactiveText: String
let sectionId: ItemListSectionId
init(theme: PresentationTheme, count: Int32, position: CGFloat, activeText: String, inactiveText: String, sectionId: ItemListSectionId) {
self.theme = theme
self.count = count
self.position = position
self.activeText = activeText
self.inactiveText = inactiveText
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = IncreaseLimitHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? IncreaseLimitHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.semibold(17.0)
private let textFont = Font.regular(15.0)
private let boldTextFont = Font.semibold(15.0)
class IncreaseLimitHeaderItemNode: ListViewItemNode {
private var hostView: ComponentHostView<Empty>?
private var params: (AnyComponent<Empty>, CGSize, ListViewItemNodeLayout)?
private var item: BoostLevelHeaderItem?
init() {
super.init(layerBacked: false, dynamicBounce: false)
}
override func didLoad() {
super.didLoad()
let hostView = ComponentHostView<Empty>()
self.hostView = hostView
self.view.addSubview(hostView)
if let (component, containerSize, layout) = self.params {
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -5.0), size: size)
}
}
func asyncLayout() -> (_ item: BoostLevelHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let topInset: CGFloat = 2.0
let badgeHeight: CGFloat = 200.0
let bottomInset: CGFloat = -86.0
let contentSize = CGSize(width: params.width, height: topInset + badgeHeight + bottomInset - 5.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let gradientColors: [UIColor]
gradientColors = [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]
let component = AnyComponent(PremiumLimitDisplayComponent(
inactiveColor: item.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
activeColors: gradientColors,
inactiveTitle: item.inactiveText,
inactiveValue: "",
inactiveTitleColor: item.theme.list.itemPrimaryTextColor,
activeTitle: "",
activeValue: item.activeText,
activeTitleColor: .white,
badgeIconName: "Premium/Boost",
badgeText: "\(item.count)",
badgePosition: item.position,
badgeGraphPosition: item.position,
invertProgress: true,
isPremiumDisabled: false
))
let containerSize = CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0)
if let hostView = strongSelf.hostView {
let size = hostView.update(
transition: .immediate,
component: component,
environment: {},
containerSize: containerSize
)
hostView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: -5.0), size: size)
}
strongSelf.params = (component, containerSize, layout)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -15,18 +15,26 @@ import PresentationDataUtils
import AppBundle
import GraphUI
import ContextUI
import ItemListPeerItem
import InviteLinksUI
import UndoUI
import ShareController
private final class ChannelStatsControllerArguments {
let context: AccountContext
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
let openMessageStats: (MessageId) -> Void
let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void) {
let copyBoostLink: (String) -> Void
let shareBoostLink: (String) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void) {
self.context = context
self.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage
self.contextAction = contextAction
self.copyBoostLink = copyBoostLink
self.shareBoostLink = shareBoostLink
}
}
@ -42,6 +50,10 @@ private enum StatsSection: Int32 {
case postInteractions
case recentPosts
case instantPageInteractions
case boostLevel
case boostOverview
case boosters
case boostLink
}
private enum StatsEntry: ItemListNodeEntry {
@ -78,6 +90,20 @@ private enum StatsEntry: ItemListNodeEntry {
case instantPageInteractionsTitle(PresentationTheme, String)
case instantPageInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case boostLevel(PresentationTheme, Int32, Int32, CGFloat)
case boostOverviewTitle(PresentationTheme, String)
case boostOverview(PresentationTheme, ChannelBoostStatus)
case boostersTitle(PresentationTheme, String)
case boostersPlaceholder(PresentationTheme, String)
case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32)
case boostersInfo(PresentationTheme, String)
case boostLinkTitle(PresentationTheme, String)
case boostLink(PresentationTheme, String)
case boostLinkInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .overviewTitle, .overview:
@ -102,6 +128,14 @@ private enum StatsEntry: ItemListNodeEntry {
return StatsSection.recentPosts.rawValue
case .instantPageInteractionsTitle, .instantPageInteractionsGraph:
return StatsSection.instantPageInteractions.rawValue
case .boostLevel:
return StatsSection.boostLevel.rawValue
case .boostOverviewTitle, .boostOverview:
return StatsSection.boostOverview.rawValue
case .boostersTitle, .boostersPlaceholder, .booster, .boostersInfo:
return StatsSection.boosters.rawValue
case .boostLinkTitle, .boostLink, .boostLinkInfo:
return StatsSection.boostLink.rawValue
}
}
@ -151,6 +185,26 @@ private enum StatsEntry: ItemListNodeEntry {
return 20
case let .post(index, _, _, _, _, _):
return 21 + index
case .boostLevel:
return 2000
case .boostOverviewTitle:
return 2001
case .boostOverview:
return 2002
case .boostersTitle:
return 2003
case .boostersPlaceholder:
return 2004
case let .booster(index, _, _, _, _):
return 2005 + index
case .boostersInfo:
return 10000
case .boostLinkTitle:
return 10001
case .boostLink:
return 10002
case .boostLinkInfo:
return 10003
}
}
@ -288,6 +342,66 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .boostLevel(lhsTheme, lhsBoosts, lhsLevel, lhsPosition):
if case let .boostLevel(rhsTheme, rhsBoosts, rhsLevel, rhsPosition) = rhs, lhsTheme === rhsTheme, lhsBoosts == rhsBoosts, lhsLevel == rhsLevel, lhsPosition == rhsPosition {
return true
} else {
return false
}
case let .boostOverviewTitle(lhsTheme, lhsText):
if case let .boostOverviewTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .boostOverview(lhsTheme, lhsStats):
if case let .boostOverview(rhsTheme, rhsStats) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats {
return true
} else {
return false
}
case let .boostersTitle(lhsTheme, lhsText):
if case let .boostersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .boostersPlaceholder(lhsTheme, lhsText):
if case let .boostersPlaceholder(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsExpires):
if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsExpires) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsExpires == rhsExpires {
return true
} else {
return false
}
case let .boostersInfo(lhsTheme, lhsText):
if case let .boostersInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .boostLinkTitle(lhsTheme, lhsText):
if case let .boostLinkTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .boostLink(lhsTheme, lhsText):
if case let .boostLink(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .boostLinkInfo(lhsTheme, lhsText):
if case let .boostLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
@ -309,8 +423,14 @@ private enum StatsEntry: ItemListNodeEntry {
let .languagesTitle(_, text),
let .postInteractionsTitle(_, text),
let .postsTitle(_, text),
let .instantPageInteractionsTitle(_, text):
let .instantPageInteractionsTitle(_, text),
let .boostOverviewTitle(_, text),
let .boostersTitle(_, text),
let .boostLinkTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .boostersInfo(_, text),
let .boostLinkInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .overview(_, stats):
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
case let .growthGraph(_, _, _, graph, type),
@ -336,81 +456,196 @@ private enum StatsEntry: ItemListNodeEntry {
}, contextAction: { node, gesture in
arguments.contextAction(message.id, node, gesture)
})
case let .booster(_, _, dateTimeFormat, peer, expires):
let expiresValue = stringForFullDate(timestamp: expires, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text("Boost expires on \(expiresValue)", .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: false, sectionId: self.section, action: {
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .boostLevel(_, count, level, position):
let inactiveText = "Level \(level)"
let activeText = "Level \(level + 1)"
return BoostLevelHeaderItem(theme: presentationData.theme, count: count, position: position, activeText: activeText, inactiveText: inactiveText, sectionId: self.section)
case let .boostOverview(_, stats):
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
case let .boostLink(_, link):
let invite: ExportedInvitation = .link(link: link, title: nil, isPermanent: false, requestApproval: false, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil)
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
arguments.copyBoostLink(link)
}, shareAction: {
arguments.shareBoostLink(link)
}, contextAction: nil, viewAction: nil, tag: nil)
case let .boostersPlaceholder(_, text):
return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks)
}
}
}
private func channelStatsControllerEntries(data: ChannelStats?, messages: [Message]?, interactions: [MessageId: ChannelStatsMessageInteractions]?, presentationData: PresentationData) -> [StatsEntry] {
private struct ChannelStatsControllerState: Equatable {
enum Section {
case stats
case boosts
}
let section: Section
init() {
self.section = .stats
}
init(section: Section) {
self.section = section
}
static func ==(lhs: ChannelStatsControllerState, rhs: ChannelStatsControllerState) -> Bool {
if lhs.section != rhs.section {
return false
}
return true
}
func withUpdatedSection(_ section: Section) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: section)
}
}
private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, interactions: [MessageId: ChannelStatsMessageInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, presentationData: PresentationData) -> [StatsEntry] {
var entries: [StatsEntry] = []
if let data = data {
let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings)
let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings)
entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_Overview, "\(minDate) \(maxDate)"))
entries.append(.overview(presentationData.theme, data))
if !data.growthGraph.isEmpty {
entries.append(.growthTitle(presentationData.theme, presentationData.strings.Stats_GrowthTitle))
entries.append(.growthGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.growthGraph, .lines))
}
if !data.followersGraph.isEmpty {
entries.append(.followersTitle(presentationData.theme, presentationData.strings.Stats_FollowersTitle))
entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.followersGraph, .lines))
}
if !data.muteGraph.isEmpty {
entries.append(.notificationsTitle(presentationData.theme, presentationData.strings.Stats_NotificationsTitle))
entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines))
}
if !data.topHoursGraph.isEmpty {
entries.append(.viewsByHourTitle(presentationData.theme, presentationData.strings.Stats_ViewsByHoursTitle))
entries.append(.viewsByHourGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep))
}
if !data.viewsBySourceGraph.isEmpty {
entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle))
entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars))
}
if !data.newFollowersBySourceGraph.isEmpty {
entries.append(.followersBySourceTitle(presentationData.theme, presentationData.strings.Stats_FollowersBySourceTitle))
entries.append(.followersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newFollowersBySourceGraph, .bars))
}
if !data.languagesGraph.isEmpty {
entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_LanguagesTitle))
entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie))
}
if !data.interactionsGraph.isEmpty {
entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle))
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
}
if !data.instantPageInteractionsGraph.isEmpty {
entries.append(.instantPageInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InstantViewInteractionsTitle))
entries.append(.instantPageInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.instantPageInteractionsGraph, .twoAxisStep))
}
if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle))
var index: Int32 = 0
for message in messages {
if let interactions = interactions[message.id] {
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, message, interactions))
index += 1
switch state.section {
case .stats:
if let data = data {
let minDate = stringForDate(timestamp: data.period.minDate, strings: presentationData.strings)
let maxDate = stringForDate(timestamp: data.period.maxDate, strings: presentationData.strings)
entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_Overview, "\(minDate) \(maxDate)"))
entries.append(.overview(presentationData.theme, data))
if !data.growthGraph.isEmpty {
entries.append(.growthTitle(presentationData.theme, presentationData.strings.Stats_GrowthTitle))
entries.append(.growthGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.growthGraph, .lines))
}
if !data.followersGraph.isEmpty {
entries.append(.followersTitle(presentationData.theme, presentationData.strings.Stats_FollowersTitle))
entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.followersGraph, .lines))
}
if !data.muteGraph.isEmpty {
entries.append(.notificationsTitle(presentationData.theme, presentationData.strings.Stats_NotificationsTitle))
entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines))
}
if !data.topHoursGraph.isEmpty {
entries.append(.viewsByHourTitle(presentationData.theme, presentationData.strings.Stats_ViewsByHoursTitle))
entries.append(.viewsByHourGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep))
}
if !data.viewsBySourceGraph.isEmpty {
entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle))
entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars))
}
if !data.newFollowersBySourceGraph.isEmpty {
entries.append(.followersBySourceTitle(presentationData.theme, presentationData.strings.Stats_FollowersBySourceTitle))
entries.append(.followersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newFollowersBySourceGraph, .bars))
}
if !data.languagesGraph.isEmpty {
entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_LanguagesTitle))
entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie))
}
if !data.interactionsGraph.isEmpty {
entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle))
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
}
if !data.instantPageInteractionsGraph.isEmpty {
entries.append(.instantPageInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InstantViewInteractionsTitle))
entries.append(.instantPageInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.instantPageInteractionsGraph, .twoAxisStep))
}
if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle))
var index: Int32 = 0
for message in messages {
if let interactions = interactions[message.id] {
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, message, interactions))
index += 1
}
}
}
}
case .boosts:
if let boostData {
let progress: CGFloat
if let nextLevelBoosts = boostData.nextLevelBoosts {
progress = CGFloat(boostData.boosts - boostData.currentLevelBoosts) / CGFloat(nextLevelBoosts - boostData.currentLevelBoosts)
} else {
progress = 1.0
}
entries.append(.boostLevel(presentationData.theme, Int32(boostData.level), Int32(boostData.boosts), progress))
entries.append(.boostOverviewTitle(presentationData.theme, "OVERVIEW"))
entries.append(.boostOverview(presentationData.theme, boostData))
let boostersTitle: String
let boostersPlaceholder: String?
let boostersFooter: String?
if let boostersState, boostersState.count > 0 {
boostersTitle = "\(boostersState.count) BOOSTERS"
boostersPlaceholder = nil
boostersFooter = "Your channel is currently boosted by these users."
} else {
boostersTitle = "BOOSTERS"
boostersPlaceholder = "No users currently boost your channel"
boostersFooter = nil
}
entries.append(.boostersTitle(presentationData.theme, boostersTitle))
if let boostersPlaceholder {
entries.append(.boostersPlaceholder(presentationData.theme, boostersPlaceholder))
}
if let boostersState {
var boosterIndex: Int32 = 0
for booster in boostersState.boosters {
entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster.peer, booster.expires))
boosterIndex += 1
}
}
if let boostersFooter {
entries.append(.boostersInfo(presentationData.theme, boostersFooter))
}
entries.append(.boostLinkTitle(presentationData.theme, "LINK FOR BOOSTING"))
if let peer {
let link: String
if let addressName = peer.addressName, !addressName.isEmpty {
link = "t.me/\(addressName)?boost"
} else {
link = "t.me/boost?=\(peer.id.id._internalGetInt64Value())"
}
entries.append(.boostLink(presentationData.theme, link))
}
entries.append(.boostLinkInfo(presentationData.theme, "Share this link with your subscribers to get more boosts."))
}
}
return entries
}
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, statsDatacenterId: Int32?) -> ViewController {
let statePromise = ValuePromise(ChannelStatsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelStatsControllerState())
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var openMessageStatsImpl: ((MessageId) -> Void)?
var contextActionImpl: ((MessageId, ASDisplayNode, ContextGesture?) -> Void)?
@ -439,12 +674,65 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
})
dataPromise.set(.single(nil) |> then(dataSignal))
let boostData = context.engine.peers.getChannelBoostStatus(peerId: peerId)
let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId)
var presentImpl: ((ViewController) -> Void)?
let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
return statsContext.loadDetailedGraph(graph, x: x)
}, openMessage: { messageId in
openMessageStatsImpl?(messageId)
}, contextAction: { messageId, node, gesture in
contextActionImpl?(messageId, node, gesture)
}, copyBoostLink: { link in
UIPasteboard.general.string = "https://\(link)"
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }))
}, shareBoostLink: { link in
let link = "https://\(link)"
let shareController = ShareController(context: context, subject: .url(link), updatedPresentationData: updatedPresentationData)
shareController.completed = { peerIds in
let _ = (context.engine.data.get(
EngineDataList(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
)
)
|> deliverOnMainQueue).start(next: { peerList in
let peers = peerList.compactMap { $0 }
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
var savedMessages = false
if peerIds.count == 1, let peerId = peerIds.first, peerId == context.account.peerId {
text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_SavedMessages_One
savedMessages = true
} else {
if peers.count == 1, let peer = peers.first {
let peerName = peer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_Chat_One(peerName).string
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
let firstPeerName = firstPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
let secondPeerName = secondPeer.id == context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
} else if let peer = peers.first {
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.InviteLink_InviteLinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
} else {
text = ""
}
}
presentImpl?(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }))
})
}
shareController.actionCompleted = {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }))
}
presentImpl?(shareController)
})
let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil)
@ -458,15 +746,31 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let previousData = Atomic<ChannelStats?>(value: nil)
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
let signal = combineLatest(
presentationData,
statePromise.get(),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
dataPromise.get(),
messagesPromise.get(),
boostData,
boostersContext.state,
longLoadingSignal
)
|> deliverOnMainQueue
|> map { presentationData, data, messageView, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, state, peer, data, messageView, boostData, boostersState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data)
var emptyStateItem: ItemListControllerEmptyStateItem?
if data == nil {
if longLoading {
emptyStateItem = StatsEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings)
} else {
switch state.section {
case .stats:
if data == nil {
if longLoading {
emptyStateItem = StatsEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings)
} else {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
}
case .boosts:
if boostData == nil {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
}
@ -480,8 +784,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
return map
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(data: data, messages: messages, interactions: interactions, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl(["Statistics", "Boosts"], state.section == .boosts ? 1 : 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(state: state, peer: peer, data: data, messages: messages, interactions: interactions, boostData: boostData, boostersState: boostersState, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
@ -498,6 +802,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}
})
}
controller.titleControlValueChanged = { value in
updateState { $0.withUpdatedSection(value == 1 ? .boosts : .stats) }
}
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
@ -530,6 +837,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let contextController = ContextController(presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
controller.presentInGlobalOverlay(contextController)
}
presentImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
return controller
}

View File

@ -20,6 +20,10 @@ extension GroupStats: PeerStats {
}
extension ChannelBoostStatus: PeerStats {
}
class StatsOverviewItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let stats: PeerStats
@ -223,7 +227,41 @@ class StatsOverviewItemNode: ListViewItemNode {
return (abs(deltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", delta, deltaPercentage * 100.0) : "", deltaValue > 0.0, abs(deltaValue) > 0.0)
}
if let stats = item.stats as? ChannelStats {
if let stats = item.stats as? ChannelBoostStatus {
topLeftValueLabelLayoutAndApply = makeTopLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(stats.level)", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
topRightValueLabelLayoutAndApply = makeTopRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(Int(stats.premiumAudience?.value ?? 0))", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
bottomLeftValueLabelLayoutAndApply = makeBottomLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(stats.boosts)", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
bottomRightValueLabelLayoutAndApply = makeBottomRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(stats.nextLevelBoosts ?? 0)", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
topLeftTitleLabelLayoutAndApply = makeTopLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Level", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
topRightTitleLabelLayoutAndApply = makeTopRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Premium Subscribers", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
bottomLeftTitleLabelLayoutAndApply = makeBottomLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Existing boosts", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
bottomRightTitleLabelLayoutAndApply = makeBottomRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Boosts to level up", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
topLeftDeltaLabelLayoutAndApply = nil
var premiumSubscribers: Double = 0.0
if let premiumAudience = stats.premiumAudience, premiumAudience.total > 0 {
premiumSubscribers = premiumAudience.value / premiumAudience.total
}
topRightDeltaLabelLayoutAndApply = makeTopRightDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", premiumSubscribers * 100.0), font: deltaFont, textColor: item.presentationData.theme.list.freeTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
bottomLeftDeltaLabelLayoutAndApply = nil
bottomRightDeltaLabelLayoutAndApply = nil
height += topRightValueLabelLayoutAndApply!.0.size.height + topRightTitleLabelLayoutAndApply!.0.size.height
height += verticalSpacing
height += bottomRightValueLabelLayoutAndApply!.0.size.height + bottomRightTitleLabelLayoutAndApply!.0.size.height
} else if let stats = item.stats as? ChannelStats {
let viewsPerPostDelta = deltaText(stats.viewsPerPost)
let sharesPerPostDelta = deltaText(stats.sharesPerPost)

View File

@ -2476,6 +2476,7 @@ public class CameraScreen: ViewController {
})
})
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -1147,36 +1147,40 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
case let .dialogFilterLimitExceeded(limit, _):
let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: limit, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .folders))
return true
})
controller.push(limitController)
controller.dismiss()
case let .sharedFolderLimitExceeded(limit, _):
let limitController = PremiumLimitScreen(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders))
return true
})
controller.push(limitController)
controller.dismiss()
case let .tooManyChannels(limit, _):
let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder))
return true
})
controller.push(limitController)
controller.dismiss()
case let .tooManyChannelsInAccount(limit, _):
let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels))
return true
})
controller.push(limitController)
controller.dismiss()
@ -1409,9 +1413,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
case let .sharedFolderLimitExceeded(limit, _):
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders))
return true
})
controller.push(limitController)
@ -1420,9 +1425,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
case let .limitExceeded(limit, _):
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .linksPerSharedFolder))
return true
})
controller.push(limitController)
@ -1430,9 +1436,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
case let .tooManyChannels(limit, _):
let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder))
return true
})
controller.push(limitController)
controller.dismiss()
@ -1441,9 +1448,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
case let .tooManyChannelsInAccount(limit, _):
let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in
guard let navigationController else {
return
return true
}
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels))
return true
})
controller.push(limitController)
controller.dismiss()

View File

@ -4435,22 +4435,54 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
private var didComplete = false
func requestCompletion(animated: Bool) {
func requestCompletion(animated: Bool, skipSendCheck: Bool = false) {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, !self.didComplete else {
return
}
// if "".isEmpty { // let sendAsPeerId = self.state.privacy.sendAsPeerId, sendAsPeerId.namespace == Namespaces.Peer.CloudChannel {
// let controller = self.context.sharedContext.makePremiumLimitController(context: self.context, subject: .storiesChannelBoost(level: 0, link: "t.me/channel?boost"), count: 5, forceDark: true, cancel: {}, action: { [weak self] in
// guard let self else {
// return
// }
// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
// self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current)
// })
// self.push(controller)
// return
// }
if !skipSendCheck, let sendAsPeerId = self.state.privacy.sendAsPeerId, sendAsPeerId.namespace == Namespaces.Peer.CloudChannel {
let _ = (self.context.engine.messages.checkStoriesUploadAvailability(target: .peer(sendAsPeerId))
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self else {
return
}
switch status {
case .available:
self.requestCompletion(animated: animated, skipSendCheck: true)
case .channelBoostRequired:
let _ = combineLatest(
queue: Queue.mainQueue(),
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: sendAsPeerId)),
self.context.engine.peers.getChannelBoostStatus(peerId: sendAsPeerId)
).start(next: { [weak self] peer, status in
guard let self, let peer, let status else {
return
}
let link: String
if let addressName = peer.addressName, !addressName.isEmpty {
link = "t.me/\(peer.addressName ?? "")?boost"
} else {
link = "t.me/boost?=\(peer.id.id._internalGetInt64Value())"
}
let controller = self.context.sharedContext.makePremiumLimitController(context: self.context, subject: .storiesChannelBoost(peer: peer, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, boosted: false), count: Int32(status.boosts), forceDark: true, cancel: {}, action: { [weak self] in
guard let self else {
return true
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current)
UIPasteboard.general.string = "https://\(link)"
return true
})
self.push(controller)
})
default:
break
}
})
return
}
self.didComplete = true

View File

@ -203,6 +203,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
let controller = context.sharedContext.makePremiumLimitController(context: context, subject: .folders, count: strongSelf.tabContainerNode?.filtersCount ?? 0, forceDark: false, cancel: {}, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folders, forceDark: false, dismissed: nil)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -296,6 +296,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
let controller = context.sharedContext.makePremiumLimitController(context: context, subject: .folders, count: strongSelf.controller?.tabContainerNode?.filtersCount ?? 0, forceDark: false, cancel: {}, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folders, forceDark: false, dismissed: nil)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -2098,6 +2098,7 @@ final class StoryItemSetContainerSendMessage {
if let item = item {
if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 {
let controller = PremiumLimitScreen(context: component.context, subject: .files, count: 4, action: {
return true
})
component.controller()?.push(controller)
return
@ -2106,6 +2107,7 @@ final class StoryItemSetContainerSendMessage {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .files, count: 2, action: {
replaceImpl?(PremiumIntroScreen(context: context, source: .upload))
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -13128,7 +13128,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
self.attachmentController?.dismiss(animated: true, completion: nil)
let openBotApp = { [weak self] allowWrite in
let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in
guard let strongSelf = self else {
return
}
@ -13212,6 +13212,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
controller.navigationPresentation = .flatModal
strongSelf.currentWebAppController = controller
strongSelf.push(controller)
if justInstalled {
let content: UndoOverlayContent
// if bot.flags.contains(.showInSettings) {
content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0)
// } else {
// content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0)
// }
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
}
}, error: { [weak self] error in
if let strongSelf = self {
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
@ -13252,17 +13262,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite)
|> deliverOnMainQueue).start(error: { _ in
}, completed: {
openBotApp(allowWrite)
openBotApp(allowWrite, true)
})
})
self.present(controller, in: .window(.root))
} else {
openBotApp(false)
openBotApp(false, false)
}
} else {
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).start()
openBotApp(allowWrite)
openBotApp(allowWrite, false)
}, showMore: { [weak self] in
if let self {
self.openResolved(result: .peer(botPeer._asPeer(), .info), sourceMessageId: nil)
@ -13271,32 +13281,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(controller, in: .window(.root))
}
} else {
openBotApp(false)
openBotApp(false, false)
}
})
// let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id)
// |> deliverOnMainQueue).start(next: { [weak self] value in
// guard let self else {
// return
// }
//
// if !value || concealed || botApp.flags.contains(.notActivated) {
//
//
// let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in
// let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).start()
// openBotApp(allowWrite)
// }, showMore: { [weak self] in
// if let self {
// self.openResolved(result: .peer(botPeer._asPeer(), .info), sourceMessageId: nil)
// }
// })
// self.present(controller, in: .window(.root))
// } else {
// openBotApp(false)
// }
// })
}
private func presentAttachmentPremiumGift() {
@ -14208,6 +14195,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let item = item {
if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 {
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .files, count: 4, action: {
return true
})
strongSelf.push(controller)
return
@ -14216,6 +14204,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .files, count: 2, action: {
replaceImpl?(PremiumIntroScreen(context: context, source: .upload))
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -1066,6 +1066,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case .startAttach:
break
case .boost:
break
}
}
}))

View File

@ -835,5 +835,92 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
}), nil)
}
})
case let .boost(peerId, status, canApplyStatus):
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer, let status else {
return
}
var boosted = false
if case let .error(error) = canApplyStatus, case .peerBoostAlreadyActive = error {
boosted = true
}
let subject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: nil, boosted: boosted)
let nextSubject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: nil, boosted: true)
let nextCount = Int32(status.boosts + 1)
var updateImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
let controller = PremiumLimitScreen(context: context, subject: subject, count: Int32(status.boosts), action: {
if boosted {
return true
}
var dismiss = false
switch canApplyStatus {
case .ok:
updateImpl?()
case let .replace(previousPeer):
let text = "You currently boost **\(previousPeer.compactDisplayTitle)**. Do you want to boost **\(peer.compactDisplayTitle)** instead?"
let controller = replaceBoostConfirmationController(context: context, fromPeer: previousPeer, toPeer: peer, text: text, commit: {
updateImpl?()
})
present(controller, nil)
case let .error(error):
let title: String?
let text: String
var actions: [TextAlertAction] = [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
]
switch error {
case .generic:
title = nil
text = presentationData.strings.Login_UnknownError
case let .floodWait(timeout):
title = "Can't Boost Too Often"
let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime, preferLowerValue: false)
text = "You can change the channel you boost only once a day. Next time you can boost is in **\(valueText)**."
dismiss = true
case .peerBoostAlreadyActive:
title = "Already Boosted"
text = "You are already boosting this channel."
case .premiumRequired:
title = "Premium Needed"
text = "Only **Telegram Premium** subscribers can boost channels. Do you want to subscribe to **Telegram Premium**?"
actions = [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
dismissImpl?()
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: false, dismissed: nil)
navigationController?.pushViewController(controller)
})
]
case .giftedPremiumNotAllowed:
title = "Can't Boost with Gifted Premium"
text = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost channels."
dismiss = true
}
let controller = textAlertController(sharedContext: context.sharedContext, title: title, text: text, actions: actions, parseMarkdown: true)
present(controller, nil)
}
return dismiss
},
openPeer: { peer in
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))
})
navigationController?.pushViewController(controller)
updateImpl = { [weak controller] in
let _ = context.engine.peers.applyChannelBoost(peerId: peerId).start()
controller?.updateSubject(nextSubject, count: nextCount)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
})
}
}

View File

@ -858,6 +858,27 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
convertedUrl = "https://t.me/addlist/\(slug)"
}
}
} else if parsedUrl.host == "boost" {
if let components = URLComponents(string: "/?" + query) {
var domain: String?
var channel: Int64?
if let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "domain" {
domain = value
} else if queryItem.name == "channel" {
channel = Int64(value)
}
}
}
}
if let domain {
convertedUrl = "https://t.me/\(domain)?boost"
} else if let channel {
convertedUrl = "https://t.me/boost?channel=\(channel)"
}
}
}
} else {
if parsedUrl.host == "importStickers" {

View File

@ -8440,6 +8440,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .accounts)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)

View File

@ -0,0 +1,258 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let text: String
private let textNode: ASTextNode
private let avatarNode: AvatarNode
private let arrowNode: ASImageNode
private let secondAvatarNode: AvatarNode
private let iconNode: ASImageNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeer: EnginePeer, toPeer: EnginePeer, text: String, actions: [TextAlertAction]) {
self.strings = strings
self.text = text
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = UIImage(bundleImageName: "Premium/AvatarBoost")
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.secondAvatarNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeer)
self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame)
if let icon = self.iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
}
origin.y += avatarSize.height + 10.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
func replaceBoostConfirmationController(context: AccountContext, fromPeer: EnginePeer, toPeer: EnginePeer, text: String, commit: @escaping () -> Void) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var dismissImpl: ((Bool) -> Void)?
var contentNode: PhotoUpdateConfirmationAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: "Replace", action: {
dismissImpl?(true)
commit()
})]
contentNode = PhotoUpdateConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, fromPeer: fromPeer, toPeer: toPeer, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

View File

@ -1712,6 +1712,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSource = .storiesExpirationDurations
case .storiesSuggestedReactions:
mappedSource = .storiesSuggestedReactions
case let .channelBoost(peerId):
mappedSource = .channelBoost(peerId)
}
let controller = PremiumIntroScreen(context: context, source: mappedSource, forceDark: forceDark)
controller.wasDismissed = dismissed
@ -1755,7 +1757,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return PremiumDemoScreen(context: context, subject: mappedSubject, action: action)
}
public func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Void) -> ViewController {
public func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController {
let mappedSubject: PremiumLimitScreen.Subject
switch subject {
case .folders:
@ -1780,8 +1782,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .storiesWeekly
case .storiesMonthly:
mappedSubject = .storiesMonthly
case let .storiesChannelBoost(level, link):
mappedSubject = .storiesChannelBoost(level: level, link: link)
case let .storiesChannelBoost(peer, level, currentLevelBoosts, nextLevelBoosts, link, boosted):
mappedSubject = .storiesChannelBoost(peer: peer, level: level, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: link, boosted: boosted)
}
return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action)
}

View File

@ -218,6 +218,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
self.accountSettingsController = accountSettingsController
self.rootTabController = tabBarController
self.pushViewController(tabBarController, animated: false)
// Queue.mainQueue().after(0.5) {
// let controller = self.context.sharedContext.makePremiumLimitController(context: self.context, subject: .storiesChannelBoost(title: "MYChannel", level: 0, currentLevelBoosts: 0, nextLevelBoosts: 1, link: "t.me/mychannel?boost"), count: 0, forceDark: true, cancel: {}, action: {})
// chatListController.present(controller, in: .window(.root))
// }
}
public func updateRootControllers(showCallsTab: Bool) {

View File

@ -73,6 +73,7 @@ public enum ParsedInternalPeerUrlParameter {
case voiceChat(String?)
case appStart(String, String?)
case story(Int32)
case boost
}
public enum ParsedInternalUrl {
@ -272,6 +273,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
}
}
return .peer(.name(peerName), .groupBotStart("", botAdminRights))
} else if queryItem.name == "boost" {
return .peer(.name(peerName), .boost)
}
}
}
@ -713,6 +716,14 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|> map { _ -> ResolvedUrl? in
}
|> then(.single(.story(peerId: peer.id, id: id)))
case .boost:
return combineLatest(
context.engine.peers.getChannelBoostStatus(peerId: peer.id),
context.engine.peers.canApplyChannelBoost(peerId: peer.id)
)
|> map { boostStatus, canApplyStatus -> ResolvedUrl? in
return .boost(peerId: peer.id, status: boostStatus, canApplyStatus: canApplyStatus)
}
}
} else {
return .single(.peer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)))