mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Channel boost
This commit is contained in:
parent
9de5689328
commit
f22e9176f2
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -33,6 +33,8 @@ swift_library(
|
||||
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/PremiumUI:PremiumUI",
|
||||
"//submodules/InviteLinksUI:InviteLinksUI",
|
||||
"//submodules/ShareController:ShareController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
165
submodules/StatisticsUI/Sources/BoostLevelHeaderItem.swift
Normal file
165
submodules/StatisticsUI/Sources/BoostLevelHeaderItem.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -2476,6 +2476,7 @@ public class CameraScreen: ViewController {
|
||||
})
|
||||
})
|
||||
replaceImpl?(controller)
|
||||
return true
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -1066,6 +1066,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
break
|
||||
case .startAttach:
|
||||
break
|
||||
case .boost:
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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" {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)))
|
||||
|
Loading…
x
Reference in New Issue
Block a user