From 8b1bd1dec0946caea0f6b6ba86c82ed7539aefcb Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 12 Mar 2024 17:43:21 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 10 + .../Sources/AccountContext.swift | 6 +- .../Sources/GalleryController.swift | 8 + .../AccountContext/Sources/Premium.swift | 15 + submodules/DrawingUI/BUILD | 1 + .../DrawingUI/Sources/DrawingScreen.swift | 19 +- submodules/ItemListUI/BUILD | 1 + .../Sources/ItemListController.swift | 19 + .../Sources/ItemListControllerNode.swift | 54 +- ...ItemListControllerSegmentedTitleView.swift | 3 +- .../ItemListControllerTabsContentNode.swift | 124 ++ .../Items/ItemListDisclosureItem.swift | 29 +- submodules/PremiumUI/BUILD | 1 + .../Sources/BusinessPageComponent.swift | 31 +- .../Sources/PremiumBoostLevelsScreen.swift | 186 +-- .../PremiumUI/Sources/PremiumDemoScreen.swift | 1 + .../Sources/PremiumIntroScreen.swift | 29 +- .../Sources/PremiumLimitScreen.swift | 100 +- .../Sources/PremiumLimitsListScreen.swift | 20 + .../QrCodeUI/Sources/QrCodeScanScreen.swift | 8 + .../Sources/SolidRoundedButtonNode.swift | 4 + submodules/StatisticsUI/BUILD | 56 +- .../Sources/ChannelStatsController.swift | 1031 +++++++++++++---- .../StatisticsUI/Sources/CpmSliderItem.swift | 322 +++++ .../Sources/GroupStatsController.swift | 2 +- .../Sources/MessageStatsController.swift | 2 +- .../Sources/MonetizationBalanceItem.swift | 488 ++++++++ .../Sources/MonetizationIntroScreen.swift | 591 ++++++++++ .../Sources/MonetizationUtils.swift | 62 + .../Sources/StatsOverviewItem.swift | 158 ++- .../Sources/TransactionInfoScreen.swift | 475 ++++++++ submodules/StickerPackPreviewUI/BUILD | 2 +- .../Sources/StickerPackScreen.swift | 155 ++- .../PendingMessageUploadedContent.swift | 4 +- .../StandaloneUploadedMedia.swift | 4 +- .../Sources/State/ApplyUpdateMessage.swift | 2 +- .../Sources/State/UpdateMessageService.swift | 4 +- .../Sources/State/UpdatesApiUtils.swift | 12 +- .../Stickers/ImportStickers.swift | 56 +- .../Stickers/TelegramEngineStickers.swift | 4 + submodules/TelegramUI/BUILD | 3 +- .../Sources/ChatMessageBubbleItemNode.swift | 1 + .../ChatMessageMediaBubbleContentNode.swift | 2 +- .../Sources/EmojiPagerContentComponent.swift | 16 + .../MediaEditor/Sources/MediaEditor.swift | 16 + .../Components/MediaEditorScreen/BUILD | 3 +- .../Sources/MediaEditorDrafts.swift | 2 + .../Sources/MediaEditorScreen.swift | 311 +++-- .../Sources/PeerInfoScreen.swift | 2 +- .../PremiumPeerShortcutComponent/BUILD | 24 + .../PremiumPeerShortcutComponent.swift | 106 ++ .../Sources/ShareWithPeersScreen.swift | 4 +- .../Components/StickerPickerScreen/BUILD | 46 + .../Sources/StickerPickerScreen.swift | 103 +- .../Sources/TabSelectorComponent.swift | 44 +- .../Chart/Ads.imageset/Contents.json | 12 + .../Chart/Ads.imageset/ads_30.pdf | 99 ++ .../Chart/Monetization.imageset/Contents.json | 12 + .../Monetization.imageset/monetization_90.pdf | 188 +++ .../Chart/Split.imageset/Contents.json | 12 + .../Chart/Split.imageset/split_30.pdf | 416 +++++++ .../Chart/Withdrawal.imageset/Contents.json | 12 + .../Chart/Withdrawal.imageset/ton_30.pdf | 117 ++ .../BoostPerk/NoAds.imageset/Contents.json | 12 + .../BoostPerk/NoAds.imageset/noads_30.pdf | 111 ++ .../Business/Intro.imageset/Contents.json | 12 + .../Business/Intro.imageset/intro_30.pdf | 226 ++++ .../BusinessPerk/Intro.imageset/Contents.json | 12 + .../Intro.imageset/introsetting_30.pdf | 182 +++ .../TelegramUI/Sources/ChatController.swift | 34 +- .../ChatControllerOpenAttachmentMenu.swift | 68 +- .../Sources/SharedAccountContext.swift | 62 +- .../Source/UIKitRuntimeUtils/UIKitUtils.h | 1 + .../Source/UIKitRuntimeUtils/UIKitUtils.m | 5 + 74 files changed, 5570 insertions(+), 805 deletions(-) create mode 100644 submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift create mode 100644 submodules/StatisticsUI/Sources/CpmSliderItem.swift create mode 100644 submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift create mode 100644 submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift create mode 100644 submodules/StatisticsUI/Sources/MonetizationUtils.swift create mode 100644 submodules/StatisticsUI/Sources/TransactionInfoScreen.swift create mode 100644 submodules/TelegramUI/Components/PremiumPeerShortcutComponent/BUILD create mode 100644 submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift create mode 100644 submodules/TelegramUI/Components/StickerPickerScreen/BUILD rename submodules/{DrawingUI => TelegramUI/Components/StickerPickerScreen}/Sources/StickerPickerScreen.swift (97%) create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/ads_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/monetization_90.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/split_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/ton_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/noads_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/intro_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/introsetting_30.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 130afaee33..bd9d21d0c2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11347,6 +11347,9 @@ Sorry for the inconvenience."; "Premium.Business.Away.Title" = "Away Messages"; "Premium.Business.Away.Text" = "Define messages that are automatically sent when you are off."; +"Premium.Business.Intro.Title" = "Intro"; +"Premium.Business.Intro.Text" = "Customize the message people see before they start a chat with you."; + "Premium.Business.Chatbots.Title" = "Chatbots"; "Premium.Business.Chatbots.Text" = "Add any third party chatbots that will process customer interactions."; @@ -11360,6 +11363,7 @@ Sorry for the inconvenience."; "Business.GreetingMessages" = "Greeting Messages"; "Business.AwayMessages" = "Away Messages"; "Business.Chatbots" = "Chatbots"; +"Business.Intro" = "Intro"; "Business.LocationInfo" = "Display the location of your business on your account."; "Business.OpeningHoursInfo" = "Show to your customers when you are open for business."; @@ -11367,6 +11371,7 @@ Sorry for the inconvenience."; "Business.GreetingMessagesInfo" = "Create greetings that will be automatically sent to new customers."; "Business.AwayMessagesInfo" = "Define messages that are automatically sent when you are off."; "Business.ChatbotsInfo" = "Add any third-party chatbots that will process customer interactions."; +"Business.IntroInfo" = "Customize the message people see before they start a chat with you."; "Business.MoreFeaturesTitle" = "MORE BUSINESS FEATURES"; "Business.MoreFeaturesInfo" = "Check this section later for new business features."; @@ -11606,3 +11611,8 @@ Sorry for the inconvenience."; "CollectibleItemInfo.ShareInlineText.LearnMore" = "Learn more >"; "Stickers.Edit" = "EDIT"; + +"ChannelBoost.Table.NoAds" = "Switch Off Ads"; + +"ChannelBoost.NoAds" = "Switch Off Ads"; +"ChannelBoost.EnableNoAdsLevelText" = "Your channel needs **Level %1$@** to switch off ads."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8b6d0a1825..d76ab65342 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -991,17 +991,19 @@ public protocol SharedAccountContext: AnyObject { func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController - func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController + func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], isEditing: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController -// func makeStickerEditorScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, initialSticker: TelegramMediaFile?, targetStickerPack: StickerPackReference?) -> ViewController + func makeStickerEditorScreen(context: AccountContext, source: Any, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController + func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController + func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController diff --git a/submodules/AccountContext/Sources/GalleryController.swift b/submodules/AccountContext/Sources/GalleryController.swift index c2ef9fcb0a..cdaccb9ee0 100644 --- a/submodules/AccountContext/Sources/GalleryController.swift +++ b/submodules/AccountContext/Sources/GalleryController.swift @@ -35,3 +35,11 @@ public final class GalleryControllerActionInteraction { self.updateCanReadHistory = updateCanReadHistory } } + +public protocol StickerPackScreen { + +} + +public protocol StickerPickerInput { + +} diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index c327bc5597..fc27194fcd 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -101,6 +101,21 @@ public enum PremiumPrivacySubject { case readTime } +public enum BoostSubject: Equatable { + case stories + case channelReactions(reactionCount: Int32) + case nameColors(colors: PeerNameColor) + case nameIcon + case profileColors(colors: PeerNameColor) + case profileIcon + case emojiStatus + case wallpaper + case customWallpaper + case audioTranscription + case emojiPack + case noAds +} + public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { return PremiumConfiguration( diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index 2b6b32ab63..950c031331 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -107,6 +107,7 @@ swift_library( "//submodules/Camera", "//submodules/TelegramUI/Components/DustEffect", "//submodules/TelegramUI/Components/DynamicCornerRadiusView", + "//submodules/TelegramUI/Components/StickerPickerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 4688708e5c..562d411db9 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -24,6 +24,7 @@ import EntityKeyboard import TelegramUIPreferences import FastBlur import MediaEditor +import StickerPickerScreen public struct DrawingResultData { public let data: Data? @@ -493,7 +494,7 @@ private final class DrawingScreenComponent: CombinedComponent { let context: AccountContext let sourceHint: DrawingScreen.SourceHint? - let existingStickerPickerInputData: Promise? + let existingStickerPickerInputData: Promise? let isVideo: Bool let isAvatar: Bool let isInteractingWithEntities: Bool @@ -529,7 +530,7 @@ private final class DrawingScreenComponent: CombinedComponent { init( context: AccountContext, sourceHint: DrawingScreen.SourceHint?, - existingStickerPickerInputData: Promise?, + existingStickerPickerInputData: Promise?, isVideo: Bool, isAvatar: Bool, isInteractingWithEntities: Bool, @@ -682,11 +683,11 @@ private final class DrawingScreenComponent: CombinedComponent { var lastSize: CGFloat = 0.5 - private let stickerPickerInputData: Promise + private let stickerPickerInputData: Promise init( context: AccountContext, - existingStickerPickerInputData: Promise?, + existingStickerPickerInputData: Promise?, updateToolState: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, @@ -728,7 +729,7 @@ private final class DrawingScreenComponent: CombinedComponent { if let existingStickerPickerInputData { self.stickerPickerInputData = existingStickerPickerInputData } else { - self.stickerPickerInputData = Promise() + self.stickerPickerInputData = Promise() let stickerPickerInputData = self.stickerPickerInputData Queue.concurrentDefaultQueue().after(0.5, { @@ -762,7 +763,7 @@ private final class DrawingScreenComponent: CombinedComponent { let signal = combineLatest(queue: .mainQueue(), emojiItems, stickerItems - ) |> map { emoji, stickers -> StickerPickerInputData in + ) |> map { emoji, stickers -> StickerPickerInput in return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil) } @@ -1012,7 +1013,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.currentMode = .sticker self.updateEntitiesPlayback.invoke(false) - let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), hasInteractiveStickers: false) + let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, hasInteractiveStickers: false) if let presentGallery = self.presentGallery { controller.presentGallery = presentGallery } @@ -2753,7 +2754,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let externalDrawingView: DrawingView? private let externalEntitiesView: DrawingEntitiesView? private let externalSelectionContainerView: DrawingSelectionContainerView? - private let existingStickerPickerInputData: Promise? + private let existingStickerPickerInputData: Promise? public var requestDismiss: () -> Void = {} public var requestApply: () -> Void = {} @@ -2762,7 +2763,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U public var presentGallery: (() -> Void)? - public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise? = nil) { + public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise? = nil) { self.context = context self.sourceHint = sourceHint self.size = size diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index 1791a5db3c..883dd0cbed 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramCore", "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/Components/ComponentDisplayAdapters", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index e0bfc8910e..bd92eb109c 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -60,6 +60,7 @@ public enum ItemListControllerTitle: Equatable { case text(String) case textWithSubtitle(String, String) case sectionControl([String], Int) + case textWithTabs(String, [String], Int) } public final class ItemListControllerTabBarItem: Equatable { @@ -113,6 +114,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable private var tabBarItemInfo: ItemListControllerTabBarItem? private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?, secondaryRight: (() -> Void)?) = (nil, nil, nil) private var segmentedTitleView: ItemListControllerSegmentedTitleView? + private var tabsNavigationContentNode: ItemListControllerTabsContentNode? private var presentationData: ItemListPresentationData @@ -318,10 +320,12 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable strongSelf.title = text strongSelf.navigationItem.titleView = nil strongSelf.segmentedTitleView = nil + strongSelf.navigationBar?.setContentNode(nil, animated: false) case let .textWithSubtitle(title, subtitle): strongSelf.title = "" strongSelf.navigationItem.titleView = ItemListTextWithSubtitleTitleView(theme: controllerState.presentationData.theme, title: title, subtitle: subtitle) strongSelf.segmentedTitleView = nil + strongSelf.navigationBar?.setContentNode(nil, animated: false) case let .sectionControl(sections, index): strongSelf.title = "" if let segmentedTitleView = strongSelf.segmentedTitleView, segmentedTitleView.segments == sections { @@ -336,6 +340,21 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } } + strongSelf.navigationBar?.setContentNode(nil, animated: false) + case let .textWithTabs(title, sections, index): + strongSelf.title = title + if let tabsNavigationContentNode = strongSelf.tabsNavigationContentNode, tabsNavigationContentNode.segments == sections { + tabsNavigationContentNode.index = index + } else { + let tabsNavigationContentNode = ItemListControllerTabsContentNode(theme: controllerState.presentationData.theme, segments: sections, selectedIndex: index) + strongSelf.tabsNavigationContentNode = tabsNavigationContentNode + strongSelf.navigationBar?.setContentNode(tabsNavigationContentNode, animated: false) + tabsNavigationContentNode.indexUpdated = { index in + if let strongSelf = self { + strongSelf.titleControlValueChanged?(index) + } + } + } } } strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action) diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 995c5a2d1f..5a54d9dfa3 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -361,14 +361,18 @@ open class ItemListControllerNode: ASDisplayNode { } self.listNode.visibleContentOffsetChanged = { [weak self] offset in + guard let strongSelf = self else { + return + } var inVoiceOver = false if let validLayout = self?.validLayout { inVoiceOver = validLayout.0.inVoiceOver } + strongSelf.contentOffsetChanged?(offset, inVoiceOver) - self?.contentOffsetChanged?(offset, inVoiceOver) - - if let strongSelf = self { + if let navigationContentNode = strongSelf.navigationBar.contentNode, case .expansion = navigationContentNode.mode { + strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate) + } else { var previousContentOffsetValue: CGFloat? if let previousContentOffset = strongSelf.previousContentOffset { if case let .known(value) = previousContentOffset { @@ -378,30 +382,30 @@ open class ItemListControllerNode: ASDisplayNode { } } switch offset { - case let .known(value): - let transition: ContainedViewLayoutTransition - if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue >= 30.0 { - transition = .animated(duration: 0.2, curve: .easeInOut) - } else { - transition = .immediate - } - if let headerItemNode = strongSelf.headerItemNode { - headerItemNode.updateContentOffset(value, transition: transition) - strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate) - } else { - strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition) - } - case .unknown, .none: - if let headerItemNode = strongSelf.headerItemNode { - headerItemNode.updateContentOffset(1000.0, transition: .immediate) - strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate) - } else { - strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate) - } + case let .known(value): + let transition: ContainedViewLayoutTransition + if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue >= 30.0 { + transition = .animated(duration: 0.2, curve: .easeInOut) + } else { + transition = .immediate + } + if let headerItemNode = strongSelf.headerItemNode { + headerItemNode.updateContentOffset(value, transition: transition) + strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate) + } else { + strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition) + } + case .unknown, .none: + if let headerItemNode = strongSelf.headerItemNode { + headerItemNode.updateContentOffset(1000.0, transition: .immediate) + strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate) + } else { + strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate) + } } - - strongSelf.previousContentOffset = offset } + + strongSelf.previousContentOffset = offset } self.listNode.beganInteractiveDragging = { [weak self] _ in diff --git a/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift b/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift index 7da5bebbdd..4af1edb6f2 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerSegmentedTitleView.swift @@ -1,10 +1,9 @@ import Foundation import UIKit -import SegmentedControlNode +import Display import TelegramPresentationData import ComponentFlow import TabSelectorComponent -import Display public final class ItemListControllerSegmentedTitleView: UIView { private let tabSelector = ComponentView() diff --git a/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift new file mode 100644 index 0000000000..1982f6053a --- /dev/null +++ b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift @@ -0,0 +1,124 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import TabSelectorComponent + +private let searchBarFont = Font.regular(17.0) + +final class ItemListControllerTabsContentNode: NavigationBarContentNode { + private let tabSelector = ComponentView() + + var theme: PresentationTheme { + didSet { + if self.theme !== oldValue { + self.update() + } + } + } + var segments: [String] { + didSet { + if self.segments != oldValue { + self.update() + } + } + } + + var index: Int { + didSet { + if self.index != oldValue { + self.update(transition: .animated(duration: 0.35, curve: .spring)) + } + } + } + + var indexUpdated: ((Int) -> Void)? + + private var validLayout: (CGSize, CGFloat, CGFloat)? + + init(theme: PresentationTheme, segments: [String], selectedIndex: Int) { + self.theme = theme + self.segments = segments + self.index = selectedIndex + + super.init() + } + + override func didLoad() { + super.didLoad() + } + + private func update(transition: ContainedViewLayoutTransition = .immediate) { + guard let (size, leftInset, rightInset) = self.validLayout else { + return + } + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition) + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + let isFirstTime = self.validLayout == nil + self.validLayout = (size, leftInset, rightInset) + + let mappedItems = zip(0 ..< self.segments.count, self.segments).map { index, segment in + return TabSelectorComponent.Item( + id: AnyHashable(index), + title: segment + ) + } + + let tabSelectorSize = self.tabSelector.update( + transition: Transition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: self.theme.list.itemSecondaryTextColor, + selection: self.theme.list.itemAccentColor + ), + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 24.0, + lineSelection: true + ), + items: mappedItems, + selectedId: AnyHashable(self.index), + setSelectedId: { [weak self] id in + guard let self, let index = id.base as? Int else { + return + } + self.indexUpdated?(index) + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 44.0) + ) + let tabSelectorFrame = CGRect(origin: CGPoint(x: floor((size.width - tabSelectorSize.width) / 2.0), y: floor((size.height - tabSelectorSize.height) / 2.0) + 4.0), size: tabSelectorSize) + if let tabSelectorView = self.tabSelector.view { + if tabSelectorView.superview == nil { + self.view.addSubview(tabSelectorView) + } + transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame) + } + + if isFirstTime { + self.requestContainerLayout(.immediate) + } + } + + override var height: CGFloat { + return self.nominalHeight + } + + override var clippedHeight: CGFloat { + return self.nominalHeight + } + + override var nominalHeight: CGFloat { + return 54.0// + self.additionalHeight + } + + override var mode: NavigationBarContentMode { + return .expansion + } +} diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index c0e84873b1..d19322435d 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -39,19 +39,28 @@ public enum ItemListDisclosureLabelStyle { case image(image: UIImage, size: CGSize) } +public enum ItemListDisclosureItemDetailLabelColor { + case generic + case constructive + case destructive +} + public class ItemListDisclosureItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let icon: UIImage? let context: AccountContext? let iconPeer: EnginePeer? let title: String + let attributedTitle: NSAttributedString? let titleColor: ItemListDisclosureItemTitleColor let titleFont: ItemListDisclosureItemTitleFont let titleIcon: UIImage? let enabled: Bool let label: String + let attributedLabel: NSAttributedString? let labelStyle: ItemListDisclosureLabelStyle let additionalDetailLabel: String? + let additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor public let sectionId: ItemListSectionId let style: ItemListStyle let disclosureStyle: ItemListDisclosureStyle @@ -60,19 +69,22 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon self.context = context self.iconPeer = iconPeer self.title = title + self.attributedTitle = attributedTitle self.titleColor = titleColor self.titleFont = titleFont self.titleIcon = titleIcon self.enabled = enabled self.labelStyle = labelStyle self.label = label + self.attributedLabel = attributedLabel self.additionalDetailLabel = additionalDetailLabel + self.additionalDetailLabelColor = additionalDetailLabelColor self.sectionId = sectionId self.style = style self.disclosureStyle = disclosureStyle @@ -358,7 +370,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { maxTitleWidth -= 12.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) @@ -392,7 +404,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { multilineLabel = true } - let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: item.attributedLabel ?? NSAttributedString(string: item.label, font: labelFont, textColor: labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var additionalDetailLabelInfo: (TextNodeLayout, () -> TextNode)? if let additionalDetailLabel = item.additionalDetailLabel { @@ -400,7 +412,16 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { if labelLayout.size.width != 0 { detailRightInset += labelLayout.size.width + 12.0 } - additionalDetailLabelInfo = makeAdditionalDetailLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: additionalDetailLabel, font: detailFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - detailRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let additionalDetailColor: UIColor + switch item.additionalDetailLabelColor { + case .generic: + additionalDetailColor = item.presentationData.theme.list.itemSecondaryTextColor + case .constructive: + additionalDetailColor = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor + case .destructive: + additionalDetailColor = item.presentationData.theme.list.itemDestructiveColor + } + additionalDetailLabelInfo = makeAdditionalDetailLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: additionalDetailLabel, font: detailFont, textColor: additionalDetailColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - detailRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) } let verticalInset: CGFloat diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index de9195fd95..5872e7f481 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -116,6 +116,7 @@ swift_library( "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", "//submodules/TelegramUI/Components/EntityKeyboard", + "//submodules/TelegramUI/Components/PremiumPeerShortcutComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/BusinessPageComponent.swift b/submodules/PremiumUI/Sources/BusinessPageComponent.swift index 4a1be46cf0..f9e44ead0c 100644 --- a/submodules/PremiumUI/Sources/BusinessPageComponent.swift +++ b/submodules/PremiumUI/Sources/BusinessPageComponent.swift @@ -301,14 +301,13 @@ private final class BusinessListComponent: CombinedComponent { let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings let colors = [ - UIColor(rgb: 0x007aff), - UIColor(rgb: 0x798aff), - UIColor(rgb: 0xac64f3), - UIColor(rgb: 0xc456ae), - UIColor(rgb: 0xe95d44), - UIColor(rgb: 0xf2822a), - UIColor(rgb: 0xe79519), - UIColor(rgb: 0xe7ad19) + UIColor(rgb: 0xef6922), + UIColor(rgb: 0xe54937), + UIColor(rgb: 0xdb374b), + UIColor(rgb: 0xbc4395), + UIColor(rgb: 0x9b4fed), + UIColor(rgb: 0x8958ff), + UIColor(rgb: 0x676bff) ] let titleColor = theme.list.itemPrimaryTextColor @@ -397,6 +396,20 @@ private final class BusinessListComponent: CombinedComponent { ) ) + items.append( + AnyComponentWithIdentity( + id: "intro", + component: AnyComponent(ParagraphComponent( + title: strings.Premium_Business_Intro_Title, + titleColor: titleColor, + text: strings.Premium_Business_Intro_Text, + textColor: textColor, + iconName: "Premium/Business/Intro", + iconColor: colors[5] + )) + ) + ) + items.append( AnyComponentWithIdentity( id: "chatbots", @@ -406,7 +419,7 @@ private final class BusinessListComponent: CombinedComponent { text: strings.Premium_Business_Chatbots_Text, textColor: textColor, iconName: "Premium/Business/Chatbots", - iconColor: colors[5] + iconColor: colors[6] )) ) ) diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 96c2cbfb8f..2cae1c711a 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -20,6 +20,7 @@ import SolidRoundedButtonComponent import BlurredBackgroundComponent import UndoUI import ConfettiEffect +import PremiumPeerShortcutComponent func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 { switch subject { @@ -55,22 +56,12 @@ func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: Acco return configuration.minGroupAudioTranscriptionLevel case .emojiPack: return configuration.minGroupEmojiPackLevel + case .noAds: + return 30 } } -public enum BoostSubject: Equatable { - case stories - case channelReactions(reactionCount: Int32) - case nameColors(colors: PeerNameColor) - case nameIcon - case profileColors(colors: PeerNameColor) - case profileIcon - case emojiStatus - case wallpaper - case customWallpaper - case audioTranscription - case emojiPack - +extension BoostSubject { public func requiredLevel(group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 { return requiredBoostSubjectLevel(subject: self, group: group, context: context, configuration: configuration) } @@ -247,6 +238,7 @@ private final class LevelSectionComponent: CombinedComponent { case customWallpaper case audioTranscription case emojiPack + case noAds func title(strings: PresentationStrings, isGroup: Bool) -> String { switch self { @@ -274,6 +266,8 @@ private final class LevelSectionComponent: CombinedComponent { return strings.GroupBoost_Table_Group_VoiceToText case .emojiPack: return strings.GroupBoost_Table_Group_EmojiPack + case .noAds: + return strings.ChannelBoost_Table_NoAds } } @@ -303,6 +297,8 @@ private final class LevelSectionComponent: CombinedComponent { return "Premium/BoostPerk/AudioTranscription" case .emojiPack: return "Premium/BoostPerk/EmojiPack" + case .noAds: + return "Premium/BoostPerk/NoAds" } } } @@ -639,6 +635,8 @@ private final class SheetContent: CombinedComponent { textString = "" case .emojiPack: textString = strings.GroupBoost_EnableEmojiPackLevelText("\(requiredLevel)").string + case .noAds: + textString = strings.ChannelBoost_EnableNoAdsLevelText("\(requiredLevel)").string } } else { let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining)) @@ -733,11 +731,10 @@ private final class SheetContent: CombinedComponent { let peerShortcut = peerShortcut.update( component: Button( content: AnyComponent( - PeerShortcutComponent( + PremiumPeerShortcutComponent( context: component.context, theme: component.theme, peer: peer - ) ), action: { @@ -1095,85 +1092,100 @@ private final class SheetContent: CombinedComponent { isFeatures = true } - if let nextLevels { - for level in nextLevels { - var perks: [LevelSectionComponent.Perk] = [] - - perks.append(.story(level)) - - if !isGroup { - perks.append(.reaction(level)) - } - - var nameColorsCount: Int32 = 0 - for (colorLevel, count) in nameColorsAtLevel { - if level >= colorLevel && colorLevel == 1 { - nameColorsCount = count - } - } - if !isGroup && nameColorsCount > 0 { - perks.append(.nameColor(nameColorsCount)) - } - - var profileColorsCount: Int32 = 0 - for (colorLevel, count) in profileColorsAtLevel { - if level >= colorLevel { - profileColorsCount += count - } - } - if profileColorsCount > 0 { - perks.append(.profileColor(profileColorsCount)) - } + + func layoutLevel(_ level: Int32) { + var perks: [LevelSectionComponent.Perk] = [] - if isGroup && level >= requiredBoostSubjectLevel(subject: .emojiPack, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.emojiPack) - } + perks.append(.story(level)) - if level >= requiredBoostSubjectLevel(subject: .profileIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.profileIcon) + if !isGroup { + perks.append(.reaction(level)) + } + + var nameColorsCount: Int32 = 0 + for (colorLevel, count) in nameColorsAtLevel { + if level >= colorLevel && colorLevel == 1 { + nameColorsCount = count } - - if isGroup && level >= requiredBoostSubjectLevel(subject: .audioTranscription, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.audioTranscription) + } + if !isGroup && nameColorsCount > 0 { + perks.append(.nameColor(nameColorsCount)) + } + + var profileColorsCount: Int32 = 0 + for (colorLevel, count) in profileColorsAtLevel { + if level >= colorLevel { + profileColorsCount += count } - - var linkColorsCount: Int32 = 0 - for (colorLevel, count) in nameColorsAtLevel { - if level >= colorLevel { - linkColorsCount += count - } + } + if profileColorsCount > 0 { + perks.append(.profileColor(profileColorsCount)) + } + + if isGroup && level >= requiredBoostSubjectLevel(subject: .emojiPack, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.emojiPack) + } + + if level >= requiredBoostSubjectLevel(subject: .profileIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.profileIcon) + } + + if isGroup && level >= requiredBoostSubjectLevel(subject: .audioTranscription, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.audioTranscription) + } + + var linkColorsCount: Int32 = 0 + for (colorLevel, count) in nameColorsAtLevel { + if level >= colorLevel { + linkColorsCount += count } - if !isGroup && linkColorsCount > 0 { - perks.append(.linkColor(linkColorsCount)) - } - - if !isGroup && level >= requiredBoostSubjectLevel(subject: .nameIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.linkIcon) - } - if level >= requiredBoostSubjectLevel(subject: .emojiStatus, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.emojiStatus) - } - if level >= requiredBoostSubjectLevel(subject: .wallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.wallpaper(8)) - } - if level >= requiredBoostSubjectLevel(subject: .customWallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) { - perks.append(.customWallpaper) - } - - levelItems.append( - AnyComponentWithIdentity( - id: level, component: AnyComponent( - LevelSectionComponent( - theme: component.theme, - strings: component.strings, - level: level, - isFirst: !isFeatures && levelItems.isEmpty, - perks: perks.reversed(), - isGroup: isGroup - ) + } + if !isGroup && linkColorsCount > 0 { + perks.append(.linkColor(linkColorsCount)) + } + + if !isGroup && level >= requiredBoostSubjectLevel(subject: .nameIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.linkIcon) + } + if level >= requiredBoostSubjectLevel(subject: .emojiStatus, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.emojiStatus) + } + if level >= requiredBoostSubjectLevel(subject: .wallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.wallpaper(8)) + } + if level >= requiredBoostSubjectLevel(subject: .customWallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.customWallpaper) + } + if !isGroup && level >= requiredBoostSubjectLevel(subject: .noAds, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.noAds) + } + + levelItems.append( + AnyComponentWithIdentity( + id: level, component: AnyComponent( + LevelSectionComponent( + theme: component.theme, + strings: component.strings, + level: level, + isFirst: !isFeatures && levelItems.isEmpty, + perks: perks.reversed(), + isGroup: isGroup ) ) ) + ) + } + + if let nextLevels { + for level in nextLevels { + layoutLevel(level) + } + } + + if !isGroup { + let noAdsLevel = requiredBoostSubjectLevel(subject: .noAds, group: false, context: component.context, configuration: premiumConfiguration) + if level < noAdsLevel { + layoutLevel(noAdsLevel) } } @@ -1443,6 +1455,8 @@ private final class BoostLevelsContainerComponent: CombinedComponent { titleString = strings.GroupBoost_AudioTranscription case .emojiPack: titleString = strings.GroupBoost_EmojiPack + case .noAds: + titleString = strings.ChannelBoost_NoAds } } else { titleString = isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 5957a9920f..ea5cbfdde3 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1424,6 +1424,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case businessQuickReplies case businessAwayMessage case businessChatBots + case businessIntro } public enum Source: Equatable { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 7355af6502..729c907ca1 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -456,6 +456,7 @@ public enum PremiumPerk: CaseIterable { case businessQuickReplies case businessAwayMessage case businessChatBots + case businessIntro public static var allCases: [PremiumPerk] { return [ @@ -488,10 +489,11 @@ public enum PremiumPerk: CaseIterable { return [ .businessLocation, .businessHours, - .businessGreetingMessage, .businessQuickReplies, + .businessGreetingMessage, .businessAwayMessage, - .businessChatBots, + .businessIntro, + .businessChatBots // .emojiStatus, // .folderTags, // .stories, @@ -567,6 +569,8 @@ public enum PremiumPerk: CaseIterable { return "away_message" case .businessChatBots: return "business_bots" + case .businessIntro: + return "business_intro" } } @@ -629,6 +633,8 @@ public enum PremiumPerk: CaseIterable { return strings.Business_AwayMessages case .businessChatBots: return strings.Business_Chatbots + case .businessIntro: + return strings.Business_Intro } } @@ -691,6 +697,8 @@ public enum PremiumPerk: CaseIterable { return strings.Business_AwayMessagesInfo case .businessChatBots: return strings.Business_ChatbotsInfo + case .businessIntro: + return strings.Business_IntroInfo } } @@ -753,6 +761,8 @@ public enum PremiumPerk: CaseIterable { return "Premium/BusinessPerk/Away" case .businessChatBots: return "Premium/BusinessPerk/Chatbots" + case .businessIntro: + return "Premium/BusinessPerk/Intro" } } } @@ -782,12 +792,13 @@ struct PremiumIntroConfiguration { .premiumStickers, .business ], businessPerks: [ + .businessLocation, + .businessHours, + .businessQuickReplies, .businessGreetingMessage, .businessAwayMessage, - .businessQuickReplies, - .businessChatBots, - .businessHours, - .businessLocation + .businessIntro, + .businessChatBots // .emojiStatus, // .folderTags, // .stories @@ -853,6 +864,9 @@ struct PremiumIntroConfiguration { if businessPerks.count < 4 { businessPerks = PremiumIntroConfiguration.defaultValue.businessPerks } + if !businessPerks.contains(.businessIntro) { + businessPerks.append(.businessIntro) + } return PremiumIntroConfiguration(perks: perks, businessPerks: businessPerks) } else { @@ -2134,6 +2148,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0xdb374b), UIColor(rgb: 0xbc4395), UIColor(rgb: 0x9b4fed), + UIColor(rgb: 0x8958ff), UIColor(rgb: 0x8958ff) ] @@ -2244,6 +2259,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .businessAwayMessage case .businessChatBots: demoSubject = .businessChatBots + case .businessIntro: + demoSubject = .businessIntro default: fatalError() } diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index b25ef82a2a..81cb2778cb 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -19,6 +19,7 @@ import ConfettiEffect import AvatarNode import TextFormat import RoundedRectWithTailPath +import PremiumPeerShortcutComponent func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in @@ -1140,7 +1141,7 @@ private final class LimitSheetContent: CombinedComponent { peerShortcutChild = peerShortcut.update( component: Button( content: AnyComponent( - PeerShortcutComponent( + PremiumPeerShortcutComponent( context: component.context, theme: environment.theme, peer: peer @@ -1894,103 +1895,6 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { } } -final class PeerShortcutComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let peer: EnginePeer - - init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { - self.context = context - self.theme = theme - self.peer = peer - } - - static func ==(lhs: PeerShortcutComponent, rhs: PeerShortcutComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.peer != rhs.peer { - return false - } - return true - } - - final class View: UIView { - private let backgroundView = UIView() - private let avatarNode: AvatarNode - private let text = ComponentView() - - private var component: PeerShortcutComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0)) - - super.init(frame: frame) - - self.backgroundView.clipsToBounds = true - self.backgroundView.layer.cornerRadius = 16.0 - - self.addSubview(self.backgroundView) - self.addSubnode(self.avatarNode) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: PeerShortcutComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.component = component - self.state = state - - self.backgroundView.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3) - - self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0)) - self.avatarNode.setPeer( - context: component.context, - theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, - peer: component.peer, - synchronousLoad: true - ) - - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)) - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width - 50.0, height: availableSize.height) - ) - - let size = CGSize(width: 30.0 + textSize.width + 20.0, height: 32.0) - if let view = self.text.view { - if view.superview == nil { - self.addSubview(view) - } - let textFrame = CGRect(origin: CGPoint(x: 38.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) - view.frame = textFrame - } - - self.backgroundView.frame = CGRect(origin: .zero, size: size) - - return size - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - public final class BoostIconComponent: Component { let hasIcon: Bool let text: String diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index dc6accd648..4f7c3b6a0c 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -979,6 +979,26 @@ public class PremiumLimitsListScreen: ViewController { ) ) + availableItems[.businessIntro] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessIntro, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: videos["business_intro"], + decoration: .business + )), + title: strings.Business_Intro, + text: strings.Business_IntroInfo, + textColor: textColor + ) + ) + ) + ) + if let order = controller.order { var items: [DemoPagerComponent.Item] = order.compactMap { availableItems[$0] } let initialIndex: Int diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 7ec0009c5e..2df200944e 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -82,6 +82,7 @@ public final class QrCodeScanScreen: ViewController { public enum Subject { case authTransfer(activeSessionsContext: ActiveSessionsContext) case peer + case cryptoAddress case custom(info: String) } @@ -264,6 +265,8 @@ public final class QrCodeScanScreen: ViewController { strongSelf.controllerNode.updateFocusedRect(nil) })) } + case .cryptoAddress: + break case .peer: if let _ = URL(string: code) { strongSelf.controllerNode.resolveCode(code: code, completion: { [weak self] result in @@ -479,6 +482,9 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie case .peer: title = "" text = "" + case .cryptoAddress: + title = "" + text = "" case let .custom(info): title = presentationData.strings.AuthSessions_AddDevice_ScanTitle text = info @@ -596,6 +602,8 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie filteredCodes = codes.filter { $0.message.hasPrefix("tg://") } case .peer: filteredCodes = codes.filter { $0.message.hasPrefix("https://t.me/") || $0.message.hasPrefix("t.me/") } + case .cryptoAddress: + filteredCodes = codes.filter { $0.message.hasPrefix("ton://") } case .custom: filteredCodes = codes } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index dc9083423c..fa80ebebbd 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -34,6 +34,10 @@ public final class SolidRoundedButtonTheme: Equatable { self.disabledForegroundColor = disabledForegroundColor } + public func withUpdated(disabledBackgroundColor: UIColor, disabledForegroundColor: UIColor) -> SolidRoundedButtonTheme { + return SolidRoundedButtonTheme(backgroundColor: self.backgroundColor, backgroundColors: self.backgroundColors, foregroundColor: self.foregroundColor, disabledBackgroundColor: disabledBackgroundColor, disabledForegroundColor: disabledForegroundColor) + } + public static func ==(lhs: SolidRoundedButtonTheme, rhs: SolidRoundedButtonTheme) -> Bool { if lhs.backgroundColor != rhs.backgroundColor { return false diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index 6bf260d9c3..6115705572 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -10,35 +10,41 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", "//submodules/ComponentFlow", - "//submodules/Postbox:Postbox", - "//submodules/TelegramCore:TelegramCore", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/AccountContext:AccountContext", - "//submodules/ItemListUI:ItemListUI", - "//submodules/AvatarNode:AvatarNode", - "//submodules/TelegramStringFormatting:TelegramStringFormatting", - "//submodules/AlertUI:AlertUI", - "//submodules/PresentationDataUtils:PresentationDataUtils", - "//submodules/MergeLists:MergeLists", - "//submodules/PhotoResources:PhotoResources", - "//submodules/GraphCore:GraphCore", - "//submodules/GraphUI:GraphUI", - "//submodules/AnimatedStickerNode:AnimatedStickerNode", - "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", - "//submodules/ItemListPeerItem:ItemListPeerItem", - "//submodules/ItemListPeerActionItem:ItemListPeerActionItem", - "//submodules/ContextUI:ContextUI", - "//submodules/PremiumUI:PremiumUI", - "//submodules/InviteLinksUI:InviteLinksUI", - "//submodules/ShareController:ShareController", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/ItemListUI", + "//submodules/AvatarNode", + "//submodules/TelegramStringFormatting", + "//submodules/AlertUI", + "//submodules/PresentationDataUtils", + "//submodules/MergeLists", + "//submodules/PhotoResources", + "//submodules/GraphCore", + "//submodules/GraphUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/ItemListPeerItem", + "//submodules/ItemListPeerActionItem", + "//submodules/ContextUI", + "//submodules/PremiumUI", + "//submodules/InviteLinksUI", + "//submodules/ShareController", + "//submodules/TextFormat", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/QrCodeUI", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index e9ecb15ee6..dc1bcab574 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -22,6 +22,7 @@ import ShareController import ItemListPeerActionItem import PremiumUI import StoryContainerScreen +import QrCodeUI private let initialBoostersDisplayedLimit: Int32 = 5 @@ -38,8 +39,16 @@ private final class ChannelStatsControllerArguments { let openGifts: () -> Void let createPrepaidGiveaway: (PrepaidGiveaway) -> Void let updateGiftsSelected: (Bool) -> Void - - init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) { + + let updateMonetizationAddress: (String) -> Void + let requestWithdraw: () -> Void + let openQrCodeScan: () -> Void + let openTransaction: (MonetizationTransaction) -> Void + let updateCpmEnabled: (Bool) -> Void + let presentCpmLocked: () -> Void + let dismissInput: () -> Void + + init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void, updateMonetizationAddress: @escaping (String) -> Void, requestWithdraw: @escaping () -> Void, openQrCodeScan: @escaping () -> Void, openTransaction: @escaping (MonetizationTransaction) -> Void, updateCpmEnabled: @escaping (Bool) -> Void, presentCpmLocked: @escaping () -> Void, dismissInput: @escaping () -> Void) { self.context = context self.loadDetailedGraph = loadDetailedGraph self.openPostStats = openPostStats @@ -52,6 +61,13 @@ private final class ChannelStatsControllerArguments { self.openGifts = openGifts self.createPrepaidGiveaway = createPrepaidGiveaway self.updateGiftsSelected = updateGiftsSelected + self.updateMonetizationAddress = updateMonetizationAddress + self.requestWithdraw = requestWithdraw + self.openQrCodeScan = openQrCodeScan + self.openTransaction = openTransaction + self.updateCpmEnabled = updateCpmEnabled + self.presentCpmLocked = presentCpmLocked + self.dismissInput = dismissInput } } @@ -70,12 +86,21 @@ private enum StatsSection: Int32 { case storyInteractions case storyReactionsByEmotion case recentPosts + case boostLevel case boostOverview case boostPrepaid case boosters case boostLink - case gifts + case boostGifts + + case adsHeader + case adsImpressions + case adsRevenue + case adsProceeds + case adsBalance + case adsTransactions + case adsCpm } enum StatsPostItem: Equatable { @@ -180,8 +205,30 @@ private enum StatsEntry: ItemListNodeEntry { case boostLink(PresentationTheme, String) case boostLinkInfo(PresentationTheme, String) - case gifts(PresentationTheme, String) - case giftsInfo(PresentationTheme, String) + case boostGifts(PresentationTheme, String) + case boostGiftsInfo(PresentationTheme, String) + + case adsHeader(PresentationTheme, String) + + case adsImpressionsTitle(PresentationTheme, String) + case adsImpressionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) + + case adsRevenueTitle(PresentationTheme, String) + case adsRevenueGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) + + case adsProceedsTitle(PresentationTheme, String) + case adsProceedsOverview(PresentationTheme, MonetizationStats, TelegramMediaFile?) + + case adsBalanceTitle(PresentationTheme, String) + case adsBalance(PresentationTheme, MonetizationStats, Bool, TelegramMediaFile?, String) + case adsBalanceInfo(PresentationTheme, String) + + case adsTransactionsTitle(PresentationTheme, String) + case adsTransaction(Int32, PresentationTheme, MonetizationTransaction) + + case adsCpmToggle(PresentationTheme, String, Bool?) + case adsCpm(PresentationTheme, PresentationStrings, Int32, TelegramMediaFile?) + case adsCpmInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -223,8 +270,22 @@ private enum StatsEntry: ItemListNodeEntry { return StatsSection.boosters.rawValue case .boostLinkTitle, .boostLink, .boostLinkInfo: return StatsSection.boostLink.rawValue - case .gifts, .giftsInfo: - return StatsSection.gifts.rawValue + case .boostGifts, .boostGiftsInfo: + return StatsSection.boostGifts.rawValue + case .adsHeader: + return StatsSection.adsHeader.rawValue + case .adsImpressionsTitle, .adsImpressionsGraph: + return StatsSection.adsImpressions.rawValue + case .adsRevenueTitle, .adsRevenueGraph: + return StatsSection.adsRevenue.rawValue + case .adsProceedsTitle, .adsProceedsOverview: + return StatsSection.adsProceeds.rawValue + case .adsBalanceTitle, .adsBalance, .adsBalanceInfo: + return StatsSection.adsBalance.rawValue + case .adsTransactionsTitle, .adsTransaction: + return StatsSection.adsTransactions.rawValue + case .adsCpmToggle, .adsCpm, .adsCpmInfo: + return StatsSection.adsCpm.rawValue } } @@ -316,10 +377,40 @@ private enum StatsEntry: ItemListNodeEntry { return 10003 case .boostLinkInfo: return 10004 - case .gifts: + case .boostGifts: return 10005 - case .giftsInfo: + case .boostGiftsInfo: return 10006 + case .adsHeader: + return 20000 + case .adsImpressionsTitle: + return 20001 + case .adsImpressionsGraph: + return 20002 + case .adsRevenueTitle: + return 20003 + case .adsRevenueGraph: + return 20004 + case .adsProceedsTitle: + return 20005 + case .adsProceedsOverview: + return 20006 + case .adsBalanceTitle: + return 20007 + case .adsBalance: + return 20008 + case .adsBalanceInfo: + return 20009 + case .adsTransactionsTitle: + return 20010 + case let .adsTransaction(index, _, _): + return 20011 + index + case .adsCpmToggle: + return 21000 + case .adsCpm: + return 21001 + case .adsCpmInfo: + return 21002 } } @@ -583,14 +674,104 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } - case let .gifts(lhsTheme, lhsText): - if case let .gifts(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .boostGifts(lhsTheme, lhsText): + if case let .boostGifts(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .giftsInfo(lhsTheme, lhsText): - if case let .giftsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .boostGiftsInfo(lhsTheme, lhsText): + if case let .boostGiftsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsHeader(lhsTheme, lhsText): + if case let .adsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsImpressionsTitle(lhsTheme, lhsText): + if case let .adsImpressionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsImpressionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): + if case let .adsImpressionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { + return true + } else { + return false + } + case let .adsRevenueTitle(lhsTheme, lhsText): + if case let .adsRevenueTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsRevenueGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType): + if case let .adsRevenueGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType { + return true + } else { + return false + } + case let .adsProceedsTitle(lhsTheme, lhsText): + if case let .adsProceedsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsProceedsOverview(lhsTheme, lhsStatus, lhsAnimatedEmoji): + if case let .adsProceedsOverview(rhsTheme, rhsStatus, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStatus == rhsStatus, lhsAnimatedEmoji == rhsAnimatedEmoji { + return true + } else { + return false + } + case let .adsBalanceTitle(lhsTheme, lhsText): + if case let .adsBalanceTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsBalance(lhsTheme, lhsStats, lhsInProgress, lhsAnimatedEmoji, lhsAddress): + if case let .adsBalance(rhsTheme, rhsStats, rhsInProgress, rhsAnimatedEmoji, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsInProgress == rhsInProgress, lhsAnimatedEmoji == rhsAnimatedEmoji, lhsAddress == rhsAddress { + return true + } else { + return false + } + case let .adsBalanceInfo(lhsTheme, lhsText): + if case let .adsBalanceInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsTransactionsTitle(lhsTheme, lhsText): + if case let .adsTransactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .adsTransaction(lhsIndex, lhsTheme, lhsTransaction): + if case let .adsTransaction(rhsIndex, rhsTheme, rhsTransaction) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTransaction == rhsTransaction { + return true + } else { + return false + } + case let .adsCpmToggle(lhsTheme, lhsText, lhsValue): + if case let .adsCpmToggle(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .adsCpm(lhsTheme, lhsStrings, lhsValue, lhsAnimatedEmoji): + if case let .adsCpm(rhsTheme, rhsStrings, rhsValue, rhsAnimatedEmoji) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsValue == rhsValue, lhsAnimatedEmoji == rhsAnimatedEmoji { + return true + } else { + return false + } + case let .adsCpmInfo(lhsTheme, lhsText): + if case let .adsCpmInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -623,15 +804,22 @@ private enum StatsEntry: ItemListNodeEntry { let .boostOverviewTitle(_, text), let .boostPrepaidTitle(_, text), let .boostersTitle(_, text), - let .boostLinkTitle(_, text): + let .boostLinkTitle(_, text), + let .adsImpressionsTitle(_, text), + let .adsRevenueTitle(_, text), + let .adsProceedsTitle(_, text), + let .adsBalanceTitle(_, text), + let .adsTransactionsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .boostPrepaidInfo(_, text), let .boostersInfo(_, text), let .boostLinkInfo(_, text), - let .giftsInfo(_, text): + let .boostGiftsInfo(_, text), + let .adsBalanceInfo(_, text), + let .adsCpmInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .overview(_, stats): - return StatsOverviewItem(presentationData: presentationData, isGroup: false, stats: stats, sectionId: self.section, style: .blocks) + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, sectionId: self.section, style: .blocks) case let .growthGraph(_, _, _, graph, type), let .followersGraph(_, _, _, graph, type), let .notificationsGraph(_, _, _, graph, type), @@ -640,7 +828,9 @@ private enum StatsEntry: ItemListNodeEntry { let .followersBySourceGraph(_, _, _, graph, type), let .languagesGraph(_, _, _, graph, type), let .reactionsByEmotionGraph(_, _, _, graph, type), - let .storyReactionsByEmotionGraph(_, _, _, graph, type): + let .storyReactionsByEmotionGraph(_, _, _, graph, type), + let .adsImpressionsGraph(_, _, _, graph, type), + let .adsRevenueGraph(_, _, _, graph, type): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks) case let .postInteractionsGraph(_, _, _, graph, type), let .instantPageInteractionsGraph(_, _, _, graph, type), @@ -731,7 +921,7 @@ private enum StatsEntry: ItemListNodeEntry { let activeText = presentationData.strings.ChannelBoost_Level("\(level + 1)").string return BoostLevelHeaderItem(theme: presentationData.theme, count: count, position: position, activeText: activeText, inactiveText: inactiveText, sectionId: self.section) case let .boostOverview(_, stats, isGroup): - return StatsOverviewItem(presentationData: presentationData, isGroup: isGroup, stats: stats, sectionId: self.section, style: .blocks) + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: isGroup, 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: { @@ -741,7 +931,7 @@ private enum StatsEntry: ItemListNodeEntry { }, contextAction: nil, viewAction: nil, tag: nil) case let .boostersPlaceholder(_, text): return ItemListPlaceholderItem(theme: presentationData.theme, text: text, sectionId: self.section, style: .blocks) - case let .gifts(theme, title): + case let .boostGifts(theme, title): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addBoostsIcon(theme), title: title, sectionId: self.section, editing: false, action: { arguments.openGifts() }) @@ -760,6 +950,79 @@ private enum StatsEntry: ItemListNodeEntry { return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity * 4)", subtitle: subtitle, label: nil, sectionId: self.section, action: { arguments.createPrepaidGiveaway(prepaidGiveaway) }) + case let .adsHeader(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case let .adsProceedsOverview(_, stats, animatedEmoji): + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats, animatedEmoji: animatedEmoji, sectionId: self.section, style: .blocks) + case let .adsBalance(_, stats, _, animatedEmoji, address): + return MonetizationBalanceItem( + context: arguments.context, + presentationData: presentationData, + stats: stats, + animatedEmoji: animatedEmoji, + address: address, + withdrawAction: { + arguments.requestWithdraw() + }, + qrAction: { + arguments.openQrCodeScan() + }, + action: { + arguments.dismissInput() + }, + textUpdated: { text in + arguments.updateMonetizationAddress(text) + }, + shouldUpdateText: { text in + return isValidAddress(text) + }, + processPaste: nil, + sectionId: self.section, + style: .blocks + ) + case let .adsTransaction(_, theme, transaction): + let font = Font.regular(presentationData.fontSize.itemListBaseFontSize) + let smallLabelFont = Font.regular(floor(presentationData.fontSize.itemListBaseFontSize / 17.0 * 13.0)) + let fixedFont = Font.monospace(presentationData.fontSize.itemListBaseFontSize / 17.0 * 16.0) + let labelColor: UIColor + if transaction.amount < 0 { + labelColor = theme.list.itemDestructiveColor + } else { + labelColor = theme.list.itemDisclosureActions.constructive.fillColor + } + let title: NSAttributedString + let detailText: String + switch transaction { + case let .incoming(_, fromTimestamp, toTimestamp): + title = NSAttributedString(string: "Proceeds from Ads", font: font, textColor: theme.list.itemPrimaryTextColor) + detailText = "\(stringForMediumDate(timestamp: fromTimestamp, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)) – \(stringForMediumDate(timestamp: toTimestamp, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat))" + case let .outgoing(_, timestamp, address, _): + let string = NSMutableAttributedString(string: "Balance Withdrawal to", font: font, textColor: theme.list.itemPrimaryTextColor) + string.append(NSAttributedString(string: "\n\(formatAddress(address))", font: fixedFont, textColor: theme.list.itemPrimaryTextColor)) + title = string + detailText = stringForMediumDate(timestamp: timestamp, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + } + + let label = amountAttributedString(formatBalanceText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + label.append(NSAttributedString(string: " TON", font: smallLabelFont, textColor: labelColor)) + + return ItemListDisclosureItem(presentationData: presentationData, title: "", attributedTitle: title, label: "", attributedLabel: label, labelStyle: .coloredText(labelColor), additionalDetailLabel: detailText, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + arguments.openTransaction(transaction) + }) + case let .adsCpmToggle(_, title, value): + return ItemListSwitchItem(presentationData: presentationData, title: title, value: value == true, enableInteractiveChanges: value != nil, enabled: true, displayLocked: value == nil, sectionId: self.section, style: .blocks, updated: { updatedValue in + if value != nil { + arguments.updateCpmEnabled(updatedValue) + } else { + arguments.presentCpmLocked() + } + }, activatedWhileDisabled: { + arguments.presentCpmLocked() + }) + case let .adsCpm(theme, strings, value, animatedEmoji): + return CpmSliderItem(context: arguments.context, theme: theme, strings: strings, value: value, enabled: true, animatedEmoji: animatedEmoji, sectionId: self.section, updated: { _ in + + }) } } } @@ -767,6 +1030,7 @@ private enum StatsEntry: ItemListNodeEntry { public enum ChannelStatsSection { case stats case boosts + case monetization } private struct ChannelStatsControllerState: Equatable { @@ -774,19 +1038,23 @@ private struct ChannelStatsControllerState: Equatable { let boostersExpanded: Bool let moreBoostersDisplayed: Int32 let giftsSelected: Bool + + let monetizationAddress: String init() { self.section = .stats self.boostersExpanded = false self.moreBoostersDisplayed = 0 self.giftsSelected = false + self.monetizationAddress = "" } - init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool) { + init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool, monetizationAddress: String) { self.section = section self.boostersExpanded = boostersExpanded self.moreBoostersDisplayed = moreBoostersDisplayed self.giftsSelected = giftsSelected + self.monetizationAddress = monetizationAddress } static func ==(lhs: ChannelStatsControllerState, rhs: ChannelStatsControllerState) -> Bool { @@ -802,253 +1070,374 @@ private struct ChannelStatsControllerState: Equatable { if lhs.giftsSelected != rhs.giftsSelected { return false } + if lhs.monetizationAddress != rhs.monetizationAddress { + return false + } return true } func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, monetizationAddress: self.monetizationAddress) } func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, monetizationAddress: self.monetizationAddress) } func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected, monetizationAddress: self.monetizationAddress) } func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected, monetizationAddress: self.monetizationAddress) + } + + func withUpdatedMonetizationAddress(_ monetizationAddress: String) -> ChannelStatsControllerState { + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected, monetizationAddress: monetizationAddress) } } - -private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, stories: PeerStoryListContext.State?, interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, presentationData: PresentationData, giveawayAvailable: Bool, isGroup: Bool, boostsOnly: Bool) -> [StatsEntry] { +private func statsEntries( + presentationData: PresentationData, + data: ChannelStats, + peer: EnginePeer?, + messages: [Message]?, + stories: PeerStoryListContext.State?, + interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]? +) -> [StatsEntry] { var entries: [StatsEntry] = [] - 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 !data.reactionsByEmotionGraph.isEmpty { - entries.append(.reactionsByEmotionTitle(presentationData.theme, presentationData.strings.Stats_ReactionsByEmotionTitle)) - entries.append(.reactionsByEmotionGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.reactionsByEmotionGraph, .bars)) - } - - if !data.storyInteractionsGraph.isEmpty { - entries.append(.storyInteractionsTitle(presentationData.theme, presentationData.strings.Stats_StoryInteractionsTitle)) - entries.append(.storyInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.storyInteractionsGraph, .twoAxisStep)) - } - - if !data.storyReactionsByEmotionGraph.isEmpty { - entries.append(.storyReactionsByEmotionTitle(presentationData.theme, presentationData.strings.Stats_StoryReactionsByEmotionTitle)) - entries.append(.storyReactionsByEmotionGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.storyReactionsByEmotionGraph, .bars)) - } - - if let peer, let interactions { - var posts: [StatsPostItem] = [] - if let messages { - for message in messages { - if let _ = interactions[.message(id: message.id)] { - posts.append(.message(message)) - } - } - } - if let stories { - for story in stories.items { - if let _ = interactions[.story(peerId: peer.id, id: story.id)] { - posts.append(.story(peer, story)) - } - } - } - posts.sort(by: { $0.timestamp > $1.timestamp }) - - if !posts.isEmpty { - entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle)) - var index: Int32 = 0 - for post in posts { - switch post { - case let .message(message): - if let interactions = interactions[.message(id: message.id)] { - entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) - } - case let .story(_, story): - if let interactions = interactions[.story(peerId: peer.id, id: story.id)] { - entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) - } - } - index += 1 - } + 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 !data.reactionsByEmotionGraph.isEmpty { + entries.append(.reactionsByEmotionTitle(presentationData.theme, presentationData.strings.Stats_ReactionsByEmotionTitle)) + entries.append(.reactionsByEmotionGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.reactionsByEmotionGraph, .bars)) + } + + if !data.storyInteractionsGraph.isEmpty { + entries.append(.storyInteractionsTitle(presentationData.theme, presentationData.strings.Stats_StoryInteractionsTitle)) + entries.append(.storyInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.storyInteractionsGraph, .twoAxisStep)) + } + + if !data.storyReactionsByEmotionGraph.isEmpty { + entries.append(.storyReactionsByEmotionTitle(presentationData.theme, presentationData.strings.Stats_StoryReactionsByEmotionTitle)) + entries.append(.storyReactionsByEmotionGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.storyReactionsByEmotionGraph, .bars)) + } + + if let peer, let interactions { + var posts: [StatsPostItem] = [] + if let messages { + for message in messages { + if let _ = interactions[.message(id: message.id)] { + posts.append(.message(message)) } } } - case .boosts: - if let boostData { - if !boostsOnly { - let progress: CGFloat - if let nextLevelBoosts = boostData.nextLevelBoosts { - progress = CGFloat(boostData.boosts - boostData.currentLevelBoosts) / CGFloat(nextLevelBoosts - boostData.currentLevelBoosts) - } else { - progress = 1.0 + if let stories { + for story in stories.items { + if let _ = interactions[.story(peerId: peer.id, id: story.id)] { + posts.append(.story(peer, story)) } - entries.append(.boostLevel(presentationData.theme, Int32(boostData.boosts), Int32(boostData.level), progress)) } - - entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader)) - entries.append(.boostOverview(presentationData.theme, boostData, isGroup)) - - if !boostData.prepaidGiveaways.isEmpty { - entries.append(.boostPrepaidTitle(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysTitle)) - var i: Int32 = 0 - for giveaway in boostData.prepaidGiveaways { - entries.append(.boostPrepaid(i, presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawayCount(giveaway.quantity), presentationData.strings.Stats_Boosts_PrepaidGiveawayMonths("\(giveaway.months)").string, giveaway)) - i += 1 - } - entries.append(.boostPrepaidInfo(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysInfo)) - } - - let boostersTitle: String - let boostersPlaceholder: String? - let boostersFooter: String? - if let boostersState, boostersState.count > 0 { - boostersTitle = presentationData.strings.Stats_Boosts_Boosts(boostersState.count) - boostersPlaceholder = nil - boostersFooter = isGroup ? presentationData.strings.Stats_Boosts_Group_BoostersInfo : presentationData.strings.Stats_Boosts_BoostersInfo - } else { - boostersTitle = presentationData.strings.Stats_Boosts_BoostsNone - boostersPlaceholder = isGroup ? presentationData.strings.Stats_Boosts_Group_NoBoostersYet : presentationData.strings.Stats_Boosts_NoBoostersYet - boostersFooter = nil - } - entries.append(.boostersTitle(presentationData.theme, boostersTitle)) - - if let boostersPlaceholder { - entries.append(.boostersPlaceholder(presentationData.theme, boostersPlaceholder)) - } - - var boostsCount: Int32 = 0 - if let boostersState { - boostsCount = boostersState.count - } - var giftsCount: Int32 = 0 - if let giftsState { - giftsCount = giftsState.count - } - - if boostsCount > 0 && giftsCount > 0 && boostsCount != giftsCount { - entries.append(.boosterTabs(presentationData.theme, presentationData.strings.Stats_Boosts_TabBoosts(boostsCount), presentationData.strings.Stats_Boosts_TabGifts(giftsCount), state.giftsSelected)) - } - - let selectedState: ChannelBoostersContext.State? - if state.giftsSelected { - selectedState = giftsState - } else { - selectedState = boostersState - } - - if let selectedState { - var boosterIndex: Int32 = 0 - - var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts - - var limit: Int32 - if state.boostersExpanded { - limit = 25 + state.moreBoostersDisplayed - } else { - limit = initialBoostersDisplayedLimit - } - boosters = Array(boosters.prefix(Int(limit))) - - for booster in boosters { - entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster)) - boosterIndex += 1 - } - - let totalBoostsCount = boosters.reduce(Int32(0)) { partialResult, boost in - return partialResult + boost.multiplier - } - - if totalBoostsCount < selectedState.count { - let moreCount: Int32 - if !state.boostersExpanded { - moreCount = min(80, selectedState.count - totalBoostsCount) - } else { - moreCount = min(200, selectedState.count - totalBoostsCount) + } + posts.sort(by: { $0.timestamp > $1.timestamp }) + + if !posts.isEmpty { + entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle)) + var index: Int32 = 0 + for post in posts { + switch post { + case let .message(message): + if let interactions = interactions[.message(id: message.id)] { + entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) + } + case let .story(_, story): + if let interactions = interactions[.story(peerId: peer.id, id: story.id)] { + entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) } - entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(moreCount))) } - } - - if let boostersFooter { - entries.append(.boostersInfo(presentationData.theme, boostersFooter)) - } - - entries.append(.boostLinkTitle(presentationData.theme, presentationData.strings.Stats_Boosts_LinkHeader)) - entries.append(.boostLink(presentationData.theme, boostData.url)) - entries.append(.boostLinkInfo(presentationData.theme, isGroup ? presentationData.strings.Stats_Boosts_Group_LinkInfo : presentationData.strings.Stats_Boosts_LinkInfo)) - - if giveawayAvailable { - entries.append(.gifts(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoosts)) - entries.append(.giftsInfo(presentationData.theme, isGroup ? presentationData.strings.Stats_Boosts_Group_GetBoostsInfo : presentationData.strings.Stats_Boosts_GetBoostsInfo)) + index += 1 } } } + return entries +} + +private func boostsEntries( + presentationData: PresentationData, + state: ChannelStatsControllerState, + isGroup: Bool, + boostData: ChannelBoostStatus, + boostsOnly: Bool, + boostersState: ChannelBoostersContext.State?, + giftsState: ChannelBoostersContext.State?, + giveawayAvailable: Bool +) -> [StatsEntry] { + var entries: [StatsEntry] = [] + + if !boostsOnly { + 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.boosts), Int32(boostData.level), progress)) + } + + entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader)) + entries.append(.boostOverview(presentationData.theme, boostData, isGroup)) + + if !boostData.prepaidGiveaways.isEmpty { + entries.append(.boostPrepaidTitle(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysTitle)) + var i: Int32 = 0 + for giveaway in boostData.prepaidGiveaways { + entries.append(.boostPrepaid(i, presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawayCount(giveaway.quantity), presentationData.strings.Stats_Boosts_PrepaidGiveawayMonths("\(giveaway.months)").string, giveaway)) + i += 1 + } + entries.append(.boostPrepaidInfo(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysInfo)) + } + + let boostersTitle: String + let boostersPlaceholder: String? + let boostersFooter: String? + if let boostersState, boostersState.count > 0 { + boostersTitle = presentationData.strings.Stats_Boosts_Boosts(boostersState.count) + boostersPlaceholder = nil + boostersFooter = isGroup ? presentationData.strings.Stats_Boosts_Group_BoostersInfo : presentationData.strings.Stats_Boosts_BoostersInfo + } else { + boostersTitle = presentationData.strings.Stats_Boosts_BoostsNone + boostersPlaceholder = isGroup ? presentationData.strings.Stats_Boosts_Group_NoBoostersYet : presentationData.strings.Stats_Boosts_NoBoostersYet + boostersFooter = nil + } + entries.append(.boostersTitle(presentationData.theme, boostersTitle)) + + if let boostersPlaceholder { + entries.append(.boostersPlaceholder(presentationData.theme, boostersPlaceholder)) + } + + var boostsCount: Int32 = 0 + if let boostersState { + boostsCount = boostersState.count + } + var giftsCount: Int32 = 0 + if let giftsState { + giftsCount = giftsState.count + } + + if boostsCount > 0 && giftsCount > 0 && boostsCount != giftsCount { + entries.append(.boosterTabs(presentationData.theme, presentationData.strings.Stats_Boosts_TabBoosts(boostsCount), presentationData.strings.Stats_Boosts_TabGifts(giftsCount), state.giftsSelected)) + } + + let selectedState: ChannelBoostersContext.State? + if state.giftsSelected { + selectedState = giftsState + } else { + selectedState = boostersState + } + + if let selectedState { + var boosterIndex: Int32 = 0 + + var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts + + var limit: Int32 + if state.boostersExpanded { + limit = 25 + state.moreBoostersDisplayed + } else { + limit = initialBoostersDisplayedLimit + } + boosters = Array(boosters.prefix(Int(limit))) + + for booster in boosters { + entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster)) + boosterIndex += 1 + } + + let totalBoostsCount = boosters.reduce(Int32(0)) { partialResult, boost in + return partialResult + boost.multiplier + } + + if totalBoostsCount < selectedState.count { + let moreCount: Int32 + if !state.boostersExpanded { + moreCount = min(80, selectedState.count - totalBoostsCount) + } else { + moreCount = min(200, selectedState.count - totalBoostsCount) + } + entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(moreCount))) + } + } + + if let boostersFooter { + entries.append(.boostersInfo(presentationData.theme, boostersFooter)) + } + + entries.append(.boostLinkTitle(presentationData.theme, presentationData.strings.Stats_Boosts_LinkHeader)) + entries.append(.boostLink(presentationData.theme, boostData.url)) + entries.append(.boostLinkInfo(presentationData.theme, isGroup ? presentationData.strings.Stats_Boosts_Group_LinkInfo : presentationData.strings.Stats_Boosts_LinkInfo)) + + if giveawayAvailable { + entries.append(.boostGifts(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoosts)) + entries.append(.boostGiftsInfo(presentationData.theme, isGroup ? presentationData.strings.Stats_Boosts_Group_GetBoostsInfo : presentationData.strings.Stats_Boosts_GetBoostsInfo)) + } + return entries +} + +private func monetizationEntries( + presentationData: PresentationData, + state: ChannelStatsControllerState, + stats: ChannelStats, + data: MonetizationStats, + animatedEmojis: [String: TelegramMediaFile] +) -> [StatsEntry] { + var entries: [StatsEntry] = [] + //TODO:localize + entries.append(.adsHeader(presentationData.theme, "Telegram shares 50% of the revenue from ads displayed in your channel. [Learn More]()")) + + entries.append(.adsImpressionsTitle(presentationData.theme, "AD IMPRESSIONS (BY HOURS)")) + if !stats.topHoursGraph.isEmpty { + entries.append(.adsImpressionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, stats.topHoursGraph, .hourlyStep)) + } + + entries.append(.adsRevenueTitle(presentationData.theme, "AD REVENUE")) + if !stats.growthGraph.isEmpty { + entries.append(.adsRevenueGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, stats.growthGraph, .bars)) + } + + entries.append(.adsProceedsTitle(presentationData.theme, "PROCEEDS OVERVIEW")) + entries.append(.adsProceedsOverview(presentationData.theme, data, animatedEmojis["💎"])) + + entries.append(.adsBalanceTitle(presentationData.theme, "AVAILABLE BALANCE")) + entries.append(.adsBalance(presentationData.theme, data, false, animatedEmojis["💎"], state.monetizationAddress)) + entries.append(.adsBalanceInfo(presentationData.theme, "We will transfer your balance to the TON wallet address you specify. [Learn More]()")) + + entries.append(.adsTransactionsTitle(presentationData.theme, "TRANSACTION HISTORY")) + + var transactions: [MonetizationTransaction] = [] + transactions.append(.outgoing(amount: -52870000000, timestamp: 1710100918, address: "UQDYzZmfsrGzhObKJUw4gzdeIxEai3jAFbiGKGwxvxHinf4K", explorerUrl: "")) + transactions.append(.incoming(amount: 52870000000, fromTimestamp: 1710090018, toTimestamp: 1710100618)) + var i: Int32 = 0 + for transaction in transactions { + entries.append(.adsTransaction(i, presentationData.theme, transaction)) + i += 1 + } + + entries.append(.adsCpmToggle(presentationData.theme, "Switch off Ads", nil)) + entries.append(.adsCpm(presentationData.theme, presentationData.strings, 5, animatedEmojis["💎"])) + entries.append(.adsCpmInfo(presentationData.theme, "Switch off ads or set their minimum CPM.")) return entries } +private func channelStatsControllerEntries( + presentationData: PresentationData, + state: ChannelStatsControllerState, + peer: EnginePeer?, + data: ChannelStats?, + messages: [Message]?, + stories: PeerStoryListContext.State?, + interactions: [ChannelStatsPostInteractions.PostId: ChannelStatsPostInteractions]?, + boostData: ChannelBoostStatus?, + boostersState: ChannelBoostersContext.State?, + giftsState: ChannelBoostersContext.State?, + giveawayAvailable: Bool, + isGroup: Bool, + boostsOnly: Bool, + animatedEmojis: [String: TelegramMediaFile], + monetizationData: MonetizationStats? +) -> [StatsEntry] { + switch state.section { + case .stats: + if let data { + return statsEntries( + presentationData: presentationData, + data: data, + peer: peer, + messages: messages, + stories: stories, + interactions: interactions + ) + } + case .boosts: + if let boostData { + return boostsEntries( + presentationData: presentationData, + state: state, + isGroup: isGroup, + boostData: boostData, + boostsOnly: boostsOnly, + boostersState: boostersState, + giftsState: giftsState, + giveawayAvailable: giveawayAvailable + ) + } + case .monetization: + if let data, let monetizationData { + return monetizationEntries( + presentationData: presentationData, + state: state, + stats: data, + data: monetizationData, + animatedEmojis: animatedEmojis + ) + } + } + return [] +} + public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController { - let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false), ignoreRepeated: true) - let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false)) + let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, monetizationAddress: ""), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, monetizationAddress: "")) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -1100,6 +1489,31 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true) + let animatedEmojiStickers = Promise<[String: TelegramMediaFile]>() + animatedEmojiStickers.set(.single([:]) + |> then( + context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> [String: TelegramMediaFile] in + var animatedEmojiStickers: [String: TelegramMediaFile] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for item in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji.basicEmoji.0] = item.file + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = item.file + } + } + } + default: + break + } + return animatedEmojiStickers + } + ) + ) + var dismissAllTooltipsImpl: (() -> Void)? var presentImpl: ((ViewController) -> Void)? var pushImpl: ((ViewController) -> Void)? @@ -1107,7 +1521,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD var navigateToChatImpl: ((EnginePeer) -> Void)? var navigateToMessageImpl: ((EngineMessage.Id) -> Void)? var openBoostImpl: ((Bool) -> Void)? + var openTransactionImpl: ((MonetizationTransaction) -> Void)? + var requestWithdrawImpl: (() -> Void)? var updateStatusBarImpl: ((StatusBarStyle) -> Void)? + var dismissInputImpl: (() -> Void)? let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal in return statsContext.loadDetailedGraph(graph, x: x) @@ -1227,6 +1644,40 @@ public func channelStatsController(context: AccountContext, updatedPresentationD }, updateGiftsSelected: { selected in updateState { $0.withUpdatedGiftsSelected(selected).withUpdatedBoostersExpanded(false) } + }, + updateMonetizationAddress: { address in + updateState { $0.withUpdatedMonetizationAddress(address) } + }, + requestWithdraw: { + requestWithdrawImpl?() + }, + openQrCodeScan: { + let controller = QrCodeScanScreen(context: context, subject: .cryptoAddress) + pushImpl?(controller) + }, + openTransaction: { transaction in + openTransactionImpl?(transaction) + }, + updateCpmEnabled: { value in + + }, + presentCpmLocked: { + let _ = combineLatest( + queue: Queue.mainQueue(), + context.engine.peers.getChannelBoostStatus(peerId: peerId), + context.engine.peers.getMyBoostStatus() + ).startStandalone(next: { boostStatus, myBoostStatus in + guard let boostStatus, let myBoostStatus else { + return + } + boostDataPromise.set(.single(boostStatus)) + + let controller = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: peerId, subject: .noAds, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil) + pushImpl?(controller) + }) + }, + dismissInput: { + dismissInputImpl?() }) let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 200, fixedCombinedReadStates: nil) @@ -1245,25 +1696,35 @@ public func channelStatsController(context: AccountContext, updatedPresentationD ) ) + let peer = Promise() + peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + let longLoadingSignal: Signal = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue())) let previousData = Atomic(value: nil) + let monetizationData = MonetizationStats( + availableBalance: MonetizationStats.Amount(cryptoAmount: 52870000000, amount: 10050, currency: "USD"), + proceedsSinceWithdraw: MonetizationStats.Amount(cryptoAmount: 100000000, amount: 10050, currency: "USD"), + lifetimeProceeds: MonetizationStats.Amount(cryptoAmount: 100000000, amount: 10050, currency: "USD") + ) + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest( presentationData, statePromise.get(), - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), + peer.get(), dataPromise.get(), messagesPromise.get(), storiesPromise.get(), boostDataPromise.get(), boostsContext.state, giftsContext.state, - longLoadingSignal + longLoadingSignal, + animatedEmojiStickers.get() ) |> deliverOnMainQueue - |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, longLoading, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in var isGroup = false if let peer, case let .channel(channel) = peer, case .group = channel.info { isGroup = true @@ -1284,6 +1745,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD if boostData == nil { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } + case .monetization: + emptyStateItem = nil +// emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } var existingGroupingKeys = Set() @@ -1331,11 +1795,21 @@ public func channelStatsController(context: AccountContext, updatedPresentationD leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}) boostsOnly = true } else { - title = .sectionControl([presentationData.strings.Stats_Statistics, presentationData.strings.Stats_Boosts], state.section == .boosts ? 1 : 0) + var index: Int + switch state.section { + case .stats: + index = 0 + case .boosts: + index = 1 + case .monetization: + index = 2 + } + //TODO:localize + title = .textWithTabs(peer?.compactDisplayTitle ?? "", [presentationData.strings.Stats_Statistics, presentationData.strings.Stats_Boosts, "Monetization"], index) } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: leftNavigationButton, 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, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, presentationData: presentationData, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(presentationData: presentationData, state: state, peer: peer, data: data, messages: messages, stories: stories, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, giveawayAvailable: premiumConfiguration.giveawayGiftsPurchaseAvailable, isGroup: isGroup, boostsOnly: boostsOnly, animatedEmojis: animatedEmojiStickers, monetizationData: monetizationData), style: .blocks, emptyStateItem: emptyStateItem, headerItem: headerItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -1353,8 +1827,26 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } }) } - controller.titleControlValueChanged = { value in - updateState { $0.withUpdatedSection(value == 1 ? .boosts : .stats) } + controller.titleControlValueChanged = { [weak controller] value in + updateState { state in + let section: ChannelStatsSection + switch value { + case 0: + section = .stats + case 1: + section = .boosts + case 2: + section = .monetization + let _ = (animatedEmojiStickers.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller] animatedEmojis in + controller?.push(MonetizationIntroScreen(context: context, animatedEmojis: animatedEmojis, openMore: {})) + }) + default: + section = .stats + } + return state.withUpdatedSection(section) + } } controller.didDisappear = { [weak controller] _ in controller?.clearItemNodesHighlight(animated: true) @@ -1497,7 +1989,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } }) } - openBoostImpl = { [weak controller] features in + openBoostImpl = { features in if features { let boostController = PremiumBoostLevelsScreen( context: context, @@ -1506,13 +1998,13 @@ public func channelStatsController(context: AccountContext, updatedPresentationD status: nil, myBoostStatus: nil ) - controller?.push(boostController) + pushImpl?(boostController) } else { let _ = combineLatest( queue: Queue.mainQueue(), context.engine.peers.getChannelBoostStatus(peerId: peerId), context.engine.peers.getMyBoostStatus() - ).startStandalone(next: { [weak controller] boostStatus, myBoostStatus in + ).startStandalone(next: { boostStatus, myBoostStatus in guard let boostStatus, let myBoostStatus else { return } @@ -1524,21 +2016,54 @@ public func channelStatsController(context: AccountContext, updatedPresentationD mode: .owner(subject: nil), status: boostStatus, myBoostStatus: myBoostStatus, - openGift: { [weak controller] in + openGift: { let giveawayController = createGiveawayController(context: context, peerId: peerId, subject: .generic) - controller?.push(giveawayController) + pushImpl?(giveawayController) } ) boostController.boostStatusUpdated = { boostStatus, _ in boostDataPromise.set(.single(boostStatus)) } - controller?.push(boostController) + pushImpl?(boostController) }) } } + requestWithdrawImpl = { + let state = stateValue.with { $0 } + //TODO:localize + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let titleFontSize = presentationData.listsFontSize.baseDisplaySize + let textFontSize = floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0) + let title = NSAttributedString(string: "Send \(formatBalanceText(monetizationData.availableBalance.cryptoAmount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)) TON", font: Font.semibold(titleFontSize), textColor: presentationData.theme.actionSheet.primaryTextColor) + + let text = NSMutableAttributedString(string: "Recipient:\n", font: Font.regular(textFontSize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + text.append(NSAttributedString(string: formatAddress(state.monetizationAddress), font: Font.monospace(textFontSize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)) + text.append(NSAttributedString(string: "\n\nThis action can not be undone. If the wallet address is incorrect, you will lose your TON.", font: Font.regular(textFontSize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)) + + let alertController = richTextAlertController(context: context, title: title, text: text, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: "Send", action: {}) + ]) + presentImpl?(alertController) + } + openTransactionImpl = { transaction in + let _ = (peer.get() + |> take(1) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + pushImpl?(TransactionInfoScreen(context: context, peer: peer, transaction: transaction, openExplorer: { url in + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + })) + }) + } updateStatusBarImpl = { [weak controller] style in controller?.setStatusBarStyle(style, animated: true) } + dismissInputImpl = { [weak controller] in + controller?.view.endEditing(true) + } return controller } @@ -1564,3 +2089,15 @@ final class ChannelStatsContextExtractedContentSource: ContextExtractedContentSo return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } } + +struct MonetizationStats: Equatable { + struct Amount: Equatable { + let cryptoAmount: Int64 + let amount: Int64 + let currency: String + } + + let availableBalance: Amount + let proceedsSinceWithdraw: Amount + let lifetimeProceeds: Amount +} diff --git a/submodules/StatisticsUI/Sources/CpmSliderItem.swift b/submodules/StatisticsUI/Sources/CpmSliderItem.swift new file mode 100644 index 0000000000..81862904a3 --- /dev/null +++ b/submodules/StatisticsUI/Sources/CpmSliderItem.swift @@ -0,0 +1,322 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramUIPreferences +import TelegramPresentationData +import LegacyComponents +import ItemListUI +import PresentationDataUtils +import EmojiTextAttachmentView +import TextFormat +import AccountContext +import UIKitRuntimeUtils + +final class CpmSliderItem: ListViewItem, ItemListItem { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let value: Int32 + let animatedEmoji: TelegramMediaFile? + let sectionId: ItemListSectionId + let updated: (Int32) -> Void + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, value: Int32, enabled: Bool, animatedEmoji: TelegramMediaFile?, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) { + self.context = context + self.theme = theme + self.strings = strings + self.value = value + self.animatedEmoji = animatedEmoji + self.sectionId = sectionId + self.updated = updated + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = CpmSliderItemNode() + 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 { + if let nodeValue = node() as? CpmSliderItemNode { + 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 allowedValues: [Int32] = [1, 2, 3, 4, 5] + +class CpmSliderItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let minTextNode: TextNode + private let maxTextNode: TextNode + private let textNode: TextNode + private var sliderView: TGPhotoEditorSliderView? + private var animatedEmojiLayer: InlineStickerItemLayer? + private var maxAnimatedEmojiLayer: InlineStickerItemLayer? + + private var item: CpmSliderItem? + private var layoutParams: ListViewItemLayoutParams? + private var reportedValue: Int32? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + self.minTextNode = TextNode() + self.minTextNode.isUserInteractionEnabled = false + self.minTextNode.displaysAsynchronously = false + + self.maxTextNode = TextNode() + self.maxTextNode.isUserInteractionEnabled = false + self.maxTextNode.displaysAsynchronously = false + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.textNode) + self.addSubnode(self.minTextNode) + self.addSubnode(self.maxTextNode) + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveTransitionGestureRecognizer = true + + let sliderView = TGPhotoEditorSliderView() + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 2.0 + sliderView.lineSize = 4.0 + sliderView.dotSize = 5.0 + sliderView.minimumValue = 0.0 + sliderView.maximumValue = 1.0 + sliderView.startValue = 0.0 + sliderView.displayEdges = true + sliderView.disablesInteractiveTransitionGestureRecognizer = true + if let item = self.item, let params = self.layoutParams { + sliderView.value = CGFloat(item.value) / 50.0 + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSwitchColors.frameColor + sliderView.startColor = item.theme.list.itemSwitchColors.frameColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) + } + self.view.addSubview(sliderView) + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + self.sliderView = sliderView + } + + func asyncLayout() -> (_ item: CpmSliderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeMinTextLayout = TextNode.asyncLayout(self.minTextNode) + let makeMaxTextLayout = TextNode.asyncLayout(self.maxTextNode) + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + //TODO:localize + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.value == 0 ? "No Ads" : "\(item.value) CPM", font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No Ads", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "50 CPM", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + contentSize = CGSize(width: params.width, height: 88.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + 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: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeOffset = 0.0 + } + + 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: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + let _ = textApply() + let textFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width) / 2.0), y: 12.0), size: textLayout.size) + strongSelf.textNode.frame = textFrame + + if let animatedEmoji = item.animatedEmoji { + let itemSize = floorToScreenPixels(17.0 * 20.0 / 17.0) + + var itemFrame = CGRect(origin: CGPoint(x: textFrame.minX - itemSize / 2.0 - 1.0, y: textFrame.midY), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) + itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) + + let itemLayer: InlineStickerItemLayer + if let current = strongSelf.animatedEmojiLayer { + itemLayer = current + } else { + let pointSize = floor(itemSize * 1.3) + itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil) + strongSelf.animatedEmojiLayer = itemLayer + strongSelf.layer.addSublayer(itemLayer) + + itemLayer.isVisibleForAnimations = true + } + itemLayer.frame = itemFrame + } + + let _ = minTextApply() + strongSelf.minTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: minTextLayout.size) + + let _ = maxTextApply() + let maxTextFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - maxTextLayout.size.width, y: 16.0), size: maxTextLayout.size) + strongSelf.maxTextNode.frame = maxTextFrame + + if let animatedEmoji = item.animatedEmoji { + let itemSize = floorToScreenPixels(13.0 * 20.0 / 17.0) + + var itemFrame = CGRect(origin: CGPoint(x: maxTextFrame.minX - itemSize / 2.0 - 1.0, y: maxTextFrame.midY), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) + itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) + + let itemLayer: InlineStickerItemLayer + if let current = strongSelf.maxAnimatedEmojiLayer { + itemLayer = current + } else { + let pointSize = floor(itemSize * 1.3) + itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil) + strongSelf.maxAnimatedEmojiLayer = itemLayer + strongSelf.layer.addSublayer(itemLayer) + + itemLayer.isVisibleForAnimations = true + + if let filter = makeMonochromeFilter() { + filter.setValue([1.0, 1.0, 1.0, 1.0] as [NSNumber], forKey: "inputColor") + filter.setValue(1.0 as NSNumber, forKey: "inputAmount") + itemLayer.filters = [filter] + } + } + itemLayer.frame = itemFrame + } + + if let sliderView = strongSelf.sliderView { + if themeUpdated { + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSwitchColors.frameColor + sliderView.startColor = item.theme.list.itemSwitchColors.frameColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + } + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) + } + } + }) + } + } + + 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) + } + + @objc func sliderValueChanged() { + guard let item = self.item, let sliderView = self.sliderView else { + return + } + item.updated(Int32(sliderView.value * 50.0)) + } +} diff --git a/submodules/StatisticsUI/Sources/GroupStatsController.swift b/submodules/StatisticsUI/Sources/GroupStatsController.swift index 6c05f8096f..c56e2c81ff 100644 --- a/submodules/StatisticsUI/Sources/GroupStatsController.swift +++ b/submodules/StatisticsUI/Sources/GroupStatsController.swift @@ -381,7 +381,7 @@ private enum StatsEntry: ItemListNodeEntry { let .topInvitersTitle(_, text, dates): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section) case let .overview(_, stats): - return StatsOverviewItem(presentationData: presentationData, isGroup: true, stats: stats, sectionId: self.section, style: .blocks) + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: true, stats: stats, sectionId: self.section, style: .blocks) case let .growthGraph(_, _, _, graph, type), let .membersGraph(_, _, _, graph, type), let .newMembersBySourceGraph(_, _, _, graph, type), diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index 1c818fe3ed..11463ad99b 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -160,7 +160,7 @@ private enum StatsEntry: ItemListNodeEntry { let .publicForwardsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .overview(_, stats, storyViews, publicShares): - return StatsOverviewItem(presentationData: presentationData, isGroup: false, stats: stats as! Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks) + return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats as! Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks) case let .interactionsGraph(_, _, _, graph, type, noInitialZoom), let .reactionsGraph(_, _, _, graph, type, noInitialZoom): return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, noInitialZoom: noInitialZoom, getDetailsData: { date, completion in let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift new file mode 100644 index 0000000000..55d9c4e17a --- /dev/null +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -0,0 +1,488 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import ItemListUI +import SolidRoundedButtonNode +import TelegramCore +import EmojiTextAttachmentView +import TextFormat + +final class MonetizationBalanceItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let stats: MonetizationStats + let animatedEmoji: TelegramMediaFile? + let address: String + let withdrawAction: () -> Void + let qrAction: () -> Void + let action: (() -> Void)? + let textUpdated: (String) -> Void + let shouldUpdateText: (String) -> Bool + let processPaste: ((String) -> Void)? + let sectionId: ItemListSectionId + let style: ItemListStyle + + init( + context: AccountContext, + presentationData: ItemListPresentationData, + stats: MonetizationStats, + animatedEmoji: TelegramMediaFile?, + address: String, + withdrawAction: @escaping () -> Void, + qrAction: @escaping () -> Void, + action: (() -> Void)?, + textUpdated: @escaping (String) -> Void, + shouldUpdateText: @escaping (String) -> Bool, + processPaste: ((String) -> Void)?, + sectionId: ItemListSectionId, + style: ItemListStyle + ) { + self.context = context + self.presentationData = presentationData + self.stats = stats + self.animatedEmoji = animatedEmoji + self.address = address + self.withdrawAction = withdrawAction + self.qrAction = qrAction + self.action = action + self.textUpdated = textUpdated + self.shouldUpdateText = shouldUpdateText + self.processPaste = processPaste + self.sectionId = sectionId + self.style = style + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = MonetizationBalanceItemNode() + 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 { + if let nodeValue = node() as? MonetizationBalanceItemNode { + 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() + }) + } + } + } + } + } + + var selectable: Bool = false +} + +final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode, ASEditableTextNodeDelegate { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private var animatedEmojiLayer: InlineStickerItemLayer? + private let balanceTextNode: TextNode + private let valueTextNode: TextNode + + private let fieldNode: ASImageNode + private let textClippingNode: ASDisplayNode + private let textNode: EditableTextNode + private let measureTextNode: TextNode + + private let qrButtonNode: HighlightableButtonNode + private var withdrawButtonNode: SolidRoundedButtonNode? + + private let activateArea: AccessibilityAreaNode + + private var item: MonetizationBalanceItem? + + override var canBeSelected: Bool { + return false + } + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.balanceTextNode = TextNode() + self.balanceTextNode.isUserInteractionEnabled = false + self.balanceTextNode.displaysAsynchronously = false + + self.valueTextNode = TextNode() + self.valueTextNode.isUserInteractionEnabled = false + self.valueTextNode.displaysAsynchronously = false + + self.fieldNode = ASImageNode() + self.fieldNode.displaysAsynchronously = false + self.fieldNode.displayWithoutProcessing = true + + self.textClippingNode = ASDisplayNode() + self.textClippingNode.clipsToBounds = true + + self.textNode = EditableTextNode() + self.measureTextNode = TextNode() + + self.qrButtonNode = HighlightableButtonNode() + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.balanceTextNode) + self.addSubnode(self.valueTextNode) + self.addSubnode(self.fieldNode) + self.addSubnode(self.qrButtonNode) + + self.textClippingNode.addSubnode(self.textNode) + self.addSubnode(self.textClippingNode) + + self.qrButtonNode.addTarget(self, action: #selector(self.qrButtonPressed), forControlEvents: .touchUpInside) + } + + override public func didLoad() { + super.didLoad() + + var textColor: UIColor = .black + if let item = self.item { + textColor = item.presentationData.theme.list.itemPrimaryTextColor + self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] + } else { + self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor] + } + self.textNode.clipsToBounds = true + self.textNode.delegate = self + self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + } + + @objc private func qrButtonPressed() { + guard let item = self.item else { + return + } + item.qrAction() + } + + func asyncLayout() -> (_ item: MonetizationBalanceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + let makeBalanceTextLayout = TextNode.asyncLayout(self.balanceTextNode) + let makeValueTextLayout = TextNode.asyncLayout(self.valueTextNode) + let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + let leftInset = 16.0 + params.leftInset + let rightInset = 16.0 + params.rightInset + let constrainedWidth = params.width - leftInset - rightInset + + let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold) + let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold) + + let cryptoValue = formatBalanceText(item.stats.availableBalance.cryptoAmount, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + + let amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) + + let (balanceLayout, balanceApply) = makeBalanceTextLayout(TextNodeLayoutArguments(attributedString: amountString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let value = "≈$100" + let (valueLayout, valueApply) = makeValueTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + var measureText = item.address + if measureText.hasSuffix("\n") || measureText.isEmpty { + measureText += "|" + } + let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: .black) + let attributedText = NSAttributedString(string: item.address, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let (textLayout, _) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0 - 36.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let attributedPlaceholderText = NSAttributedString(string: "Enter your TON address", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor) + + let verticalInset: CGFloat = 16.0 + let fieldHeight: CGFloat = max(52.0, textLayout.size.height + 32.0) + let fieldSpacing: CGFloat = 16.0 + let buttonHeight: CGFloat = 50.0 + + var height: CGFloat = verticalInset * 2.0 + balanceLayout.size.height + 7.0 + if valueLayout.size.height > 0.0 { + height += valueLayout.size.height + height += fieldHeight + fieldSpacing + buttonHeight + } + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = .clear + insets = UIEdgeInsets() + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + contentSize = CGSize(width: params.width, height: height) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + let _ = balanceApply() + let _ = valueApply() + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityTraits = [] + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.fieldNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: item.presentationData.theme.list.itemInputField.backgroundColor) + + strongSelf.qrButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/QrButtonIcon"), color: item.presentationData.theme.list.itemAccentColor), for: .normal) + } + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + 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 { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + 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: + 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.presentationData.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)) + } + + var emojiItemFrame: CGRect = .zero + var emojiItemSize: CGFloat = 0.0 + if let animatedEmoji = item.animatedEmoji { + emojiItemSize = floorToScreenPixels(46.0 * 20.0 / 17.0) + + emojiItemFrame = CGRect(origin: CGPoint(x: -emojiItemSize / 2.0 - 5.0, y: -3.0), size: CGSize()).insetBy(dx: -emojiItemSize / 2.0, dy: -emojiItemSize / 2.0) + emojiItemFrame.origin.x = floorToScreenPixels(emojiItemFrame.origin.x) + emojiItemFrame.origin.y = floorToScreenPixels(emojiItemFrame.origin.y) + + let itemLayer: InlineStickerItemLayer + if let current = strongSelf.animatedEmojiLayer { + itemLayer = current + } else { + let pointSize = floor(emojiItemSize * 1.3) + itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil) + strongSelf.animatedEmojiLayer = itemLayer + strongSelf.layer.addSublayer(itemLayer) + + itemLayer.isVisibleForAnimations = true + } + } + + let balanceTotalWidth: CGFloat = emojiItemSize + balanceLayout.size.width + let balanceTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - balanceTotalWidth) / 2.0) + emojiItemSize, y: 13.0), size: balanceLayout.size) + strongSelf.balanceTextNode.frame = balanceTextFrame + strongSelf.animatedEmojiLayer?.frame = emojiItemFrame.offsetBy(dx: balanceTextFrame.minX, dy: balanceTextFrame.midY) + + strongSelf.valueTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - valueLayout.size.width) / 2.0), y: balanceTextFrame.maxY - 5.0), size: valueLayout.size) + + strongSelf.textNode.textView.autocapitalizationType = .none + strongSelf.textNode.textView.autocorrectionType = .no + strongSelf.textNode.textView.returnKeyType = .done + + if let currentText = strongSelf.textNode.attributedText { + if currentText.string != attributedText.string || updatedTheme != nil { + strongSelf.textNode.attributedText = attributedText + } + } else { + strongSelf.textNode.attributedText = attributedText + } + + if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { + strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText + } + strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance + + let textTopInset: CGFloat = 108.0 + if strongSelf.animationForKey("apparentHeight") == 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.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: textTopInset + 15.0), size: CGSize(width: params.width - leftInset - rightInset - 12.0 - 36.0, height: textLayout.size.height)) + } + strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset - 12.0 - 36.0, height: textLayout.size.height + 1.0)) + + let fieldFrame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - rightInset, height: fieldHeight)) + strongSelf.fieldNode.frame = fieldFrame + + let qrButtonSize = CGSize(width: 32.0, height: 32.0) + let qrButtonFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - qrButtonSize.width - 5.0, y: fieldFrame.midY - qrButtonSize.height / 2.0), size: qrButtonSize) + strongSelf.qrButtonNode.frame = qrButtonFrame + + let withdrawButtonNode: SolidRoundedButtonNode + if let currentShareButtonNode = strongSelf.withdrawButtonNode { + withdrawButtonNode = currentShareButtonNode + } else { + var buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) + buttonTheme = buttonTheme.withUpdated(disabledBackgroundColor: buttonTheme.backgroundColor, disabledForegroundColor: buttonTheme.foregroundColor.withAlphaComponent(0.6)) + withdrawButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: buttonHeight, cornerRadius: 11.0) + withdrawButtonNode.pressed = { [weak self] in + if let self, let item = self.item { + item.withdrawAction() + } + } + strongSelf.addSubnode(withdrawButtonNode) + strongSelf.withdrawButtonNode = withdrawButtonNode + } + if cryptoValue != "0" { + withdrawButtonNode.title = "Transfer \(cryptoValue) TON" + } + withdrawButtonNode.isEnabled = (strongSelf.textNode.attributedText?.string.count ?? 0) == walletAddressLength + + let buttonWidth = contentSize.width - leftInset - rightInset + let _ = withdrawButtonNode.updateLayout(width: buttonWidth, transition: .immediate) + withdrawButtonNode.frame = CGRect(x: leftInset, y: fieldFrame.maxY + fieldSpacing, width: buttonWidth, height: buttonHeight) + } + }) + } + } + + public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if let item = self.item { + if text.count > 1, let processPaste = item.processPaste { + processPaste(text) + return false + } + + if let action = item.action, text == "\n" { + action() + return false + } + + let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) + if !item.shouldUpdateText(newText) { + return false + } + } + return true + } + + public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + if let item = self.item { + if let text = self.textNode.attributedText { + let updatedText = text.string + let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor) + if text.string != updatedAttributedText.string { + self.textNode.attributedText = updatedAttributedText + } + self.withdrawButtonNode?.isEnabled = (self.textNode.attributedText?.string.count ?? 0) == walletAddressLength + item.textUpdated(updatedText) + } else { + item.textUpdated("") + } + } + } + + public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { + if let _ = self.item { + let text: String? = UIPasteboard.general.string + if let _ = text { + return true + } + } + return false + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift new file mode 100644 index 0000000000..4a65da6bfa --- /dev/null +++ b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift @@ -0,0 +1,591 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BundleIconComponent +import BalancedTextComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import SolidRoundedButtonComponent +import LottieComponent +import AccountContext + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let animatedEmojis: [String: TelegramMediaFile] + let openMore: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + animatedEmojis: [String: TelegramMediaFile], + openMore: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.animatedEmojis = animatedEmojis + self.openMore = openMore + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var cachedIconImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? + + let playOnce = ActionSlot() + private var didPlayAnimation = false + + func playAnimationIfNeeded() { + guard !self.didPlayAnimation else { + return + } + self.didPlayAnimation = true + self.playOnce.invoke(Void()) + } + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let iconBackground = Child(Image.self) + let icon = Child(BundleIconComponent.self) + + let title = Child(BalancedTextComponent.self) + let list = Child(List.self) + let actionButton = Child(SolidRoundedButtonComponent.self) + + let infoBackground = Child(RoundedRectangle.self) + let infoTitle = Child(MultilineTextWithEntitiesComponent.self) + let infoText = Child(MultilineTextComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let theme = environment.theme +// let strings = environment.strings + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 30.0 + environment.safeInsets.left + + let titleFont = Font.semibold(20.0) + let textFont = Font.regular(15.0) + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + //TODO:localize + + let spacing: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 32.0) + + let iconSize = CGSize(width: 90.0, height: 90.0) + let gradientImage: UIImage + + if let (current, currentTheme) = state.cachedIconImage, currentTheme === theme { + gradientImage = current + } else { + gradientImage = generateGradientFilledCircleImage(diameter: iconSize.width, colors: [ + UIColor(rgb: 0x4bbb45).cgColor, + UIColor(rgb: 0x9ad164).cgColor + ])! + context.state.cachedIconImage = (gradientImage, theme) + } + + let iconBackground = iconBackground.update( + component: Image(image: gradientImage), + availableSize: iconSize, + transition: .immediate + ) + context.add(iconBackground + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0)) + ) + + let icon = icon.update( + component: BundleIconComponent(name: "Chart/Monetization", tintColor: theme.list.itemCheckColors.foregroundColor), + availableSize: CGSize(width: 90, height: 90), + transition: .immediate + ) + + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0)) + ) + contentSize.height += iconSize.height + contentSize.height += spacing + 5.0 + + let title = title.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: "Earn From Your Channel", font: titleFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += spacing + + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "ads", + component: AnyComponent(ParagraphComponent( + title: "Telegram Ads", + titleColor: textColor, + text: "Telegram can display ads in your channel.", + textColor: secondaryTextColor, + iconName: "Chart/Ads", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "split", + component: AnyComponent(ParagraphComponent( + title: "50:50 Revenue Split", + titleColor: textColor, + text: "You receive 50% of the ad revenue in TON.", + textColor: secondaryTextColor, + iconName: "Chart/Split", + iconColor: linkColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "withdrawal", + component: AnyComponent(ParagraphComponent( + title: "Flexible Withdrawals", + titleColor: textColor, + text: "You can withdraw your TON any time.", + textColor: secondaryTextColor, + iconName: "Chart/Withdrawal", + iconColor: linkColor + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - sideInset, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) + ) + contentSize.height += list.size.height + contentSize.height += spacing - 9.0 + + let infoTitleString = "What's #TON?"//.replacingOccurrences(of: "#", with: "# ") + let infoTitleAttributedString = NSMutableAttributedString(string: infoTitleString, font: titleFont, textColor: textColor) + let range = (infoTitleAttributedString.string as NSString).range(of: "#") + if range.location != NSNotFound, let emojiFile = component.animatedEmojis["💎"] { + infoTitleAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range) + } + let infoTitle = infoTitle.update( + component: MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + text: .plain(infoTitleAttributedString), + horizontalAlignment: .center + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + } + + let infoString = "TON is a blockchain platform and cryptocurrency that Telegram uses for its record scalability and ultra low commissions on transactions.\n[Learn More >]()" + let infoAttributedString = parseMarkdownIntoAttributedString(infoString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = infoAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + infoAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: infoAttributedString.string)) + } + let infoText = infoText.update( + component: MultilineTextComponent( + text: .plain(infoAttributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - (textSideInset + sideInset - 2.0) * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let infoPadding: CGFloat = 17.0 + let infoSpacing: CGFloat = 12.0 + let totalInfoHeight = infoPadding + infoTitle.size.height + infoSpacing + infoText.size.height + infoPadding + + let infoBackground = infoBackground.update( + component: RoundedRectangle( + color: theme.list.blocksBackgroundColor, + cornerRadius: 10.0 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight), + transition: .immediate + ) + context.add(infoBackground + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoBackground.size.height / 2.0)) + ) + contentSize.height += infoPadding + + context.add(infoTitle + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoTitle.size.height / 2.0)) + ) + contentSize.height += infoTitle.size.height + contentSize.height += infoSpacing + + context.add(infoText + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoText.size.height / 2.0)) + ) + contentSize.height += infoText.size.height + contentSize.height += infoPadding + contentSize.height += spacing + + let actionButton = actionButton.update( + component: SolidRoundedButtonComponent( + title: "Understood", + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: theme.list.itemCheckColors.fillColor, + backgroundColors: [], + foregroundColor: theme.list.itemCheckColors.foregroundColor + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(actionButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) + ) + contentSize.height += actionButton.size.height + contentSize.height += 22.0 + + contentSize.height += environment.safeInsets.bottom + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let animatedEmojis: [String: TelegramMediaFile] + let openMore: () -> Void + + init( + context: AccountContext, + animatedEmojis: [String: TelegramMediaFile], + openMore: @escaping () -> Void + ) { + self.context = context + self.animatedEmojis = animatedEmojis + self.openMore = openMore + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + animatedEmojis: context.component.animatedEmojis, + openMore: context.component.openMore, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + externalState: sheetExternalState, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + + +final class MonetizationIntroScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let animatedEmojis: [String: TelegramMediaFile] + private var openMore: (() -> Void)? + + init( + context: AccountContext, + animatedEmojis: [String: TelegramMediaFile], + openMore: @escaping () -> Void + ) { + self.context = context + self.animatedEmojis = animatedEmojis + self.openMore = openMore + + super.init( + context: context, + component: SheetContainerComponent( + context: context, + animatedEmojis: animatedEmojis, + openMore: openMore + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class ParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let iconName: String + let iconColor: UIColor + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + iconName: String, + iconColor: UIColor + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.iconName = iconName + self.iconColor = iconColor + } + + static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + + return { context in + let component = context.component + + let leftInset: CGFloat = 64.0 + let rightInset: CGFloat = 32.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: textColor), + linkAttribute: { _ in + return nil + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 47.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) + } + } +} diff --git a/submodules/StatisticsUI/Sources/MonetizationUtils.swift b/submodules/StatisticsUI/Sources/MonetizationUtils.swift new file mode 100644 index 0000000000..a238c48b35 --- /dev/null +++ b/submodules/StatisticsUI/Sources/MonetizationUtils.swift @@ -0,0 +1,62 @@ +import Foundation +import UIKit + +let walletAddressLength: Int = 48 + +func formatAddress(_ address: String) -> String { + var address = address + address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2)) + return address +} + +func formatBalanceText(_ value: Int64, decimalSeparator: String, showPlus: Bool = false) -> String { + var balanceText = "\(abs(value))" + while balanceText.count < 10 { + balanceText.insert("0", at: balanceText.startIndex) + } + balanceText.insert(contentsOf: decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9)) + while true { + if balanceText.hasSuffix("0") { + if balanceText.hasSuffix("\(decimalSeparator)0") { + balanceText.removeLast() + balanceText.removeLast() + break + } else { + balanceText.removeLast() + } + } else { + break + } + } + if value < 0 { + balanceText.insert("-", at: balanceText.startIndex) + } else if showPlus { + balanceText.insert("+", at: balanceText.startIndex) + } + return balanceText +} + +private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted +func isValidAddress(_ address: String, exactLength: Bool = false) -> Bool { + if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil { + return false + } + if exactLength && address.count != walletAddressLength { + return false + } + return true +} + +private let amountDelimeterCharacters = CharacterSet(charactersIn: "0123456789-+").inverted +func amountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString { + let result = NSMutableAttributedString() + if let range = string.rangeOfCharacter(from: amountDelimeterCharacters) { + let integralPart = String(string[.. Void)? @@ -115,13 +127,13 @@ private final class ValueItemNode: ASDisplayNode { self.addSubnode(self.deltaNode) } - static func asyncLayout(_ current: ValueItemNode?) -> (_ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?) -> (CGSize, () -> ValueItemNode) { + static func asyncLayout(_ current: ValueItemNode?) -> (_ context: AccountContext, _ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?, _ animatedEmoji: TelegramMediaFile?) -> (CGSize, () -> ValueItemNode) { let maybeMakeValueLayout = (current?.valueNode).flatMap(TextNode.asyncLayout) let maybeMakeTitleLayout = (current?.titleNode).flatMap(TextNode.asyncLayout) let maybeMakeDeltaLayout = (current?.deltaNode).flatMap(TextNode.asyncLayout) - return { width, presentationData, value, title, delta in + return { context, width, presentationData, value, title, delta, animatedEmoji in let targetNode: ValueItemNode if let current = current { targetNode = current @@ -150,7 +162,8 @@ private final class ValueItemNode: ASDisplayNode { makeDeltaLayout = TextNode.asyncLayout(targetNode.deltaNode) } - let valueFont = Font.semibold(presentationData.fontSize.itemListBaseFontSize) + let fontSize = presentationData.fontSize.itemListBaseFontSize + let valueFont = Font.semibold(fontSize) let titleFont = Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize) let deltaFont = Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize) @@ -170,6 +183,7 @@ private final class ValueItemNode: ASDisplayNode { } else { deltaColor = presentationData.theme.list.freeTextErrorColor } + let placeholderColor = presentationData.theme.list.mediaPlaceholderColor let constrainedSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) let (valueLayout, valueApply) = makeValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: valueFont, textColor: valueColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -185,9 +199,33 @@ private final class ValueItemNode: ASDisplayNode { let _ = titleApply() let _ = deltaApply() - let valueFrame = CGRect(origin: .zero, size: valueLayout.size) + var valueOffset: CGFloat = 0.0 + if let animatedEmoji { + let itemSize = floorToScreenPixels(fontSize * 20.0 / 17.0) + + var itemFrame = CGRect(origin: CGPoint(x: itemSize / 2.0 - 1.0, y: itemSize / 2.0), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) + itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) + + let itemLayer: InlineStickerItemLayer + if let current = targetNode.animatedEmojiLayer { + itemLayer = current + } else { + let pointSize = floor(itemSize * 1.3) + itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: context.animationCache, renderer: context.animationRenderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil) + targetNode.animatedEmojiLayer = itemLayer + targetNode.layer.addSublayer(itemLayer) + + itemLayer.isVisibleForAnimations = true + } + valueOffset += 22.0 + + itemLayer.frame = itemFrame + } + + let valueFrame = CGRect(origin: CGPoint(x: valueOffset, y: 0.0), size: valueLayout.size) let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: valueFrame.maxY), size: titleLayout.size) - let deltaFrame = CGRect(origin: CGPoint(x: valueFrame.maxX + horizontalSpacing, y: valueFrame.maxY - deltaLayout.size.height - 2.0), size: deltaLayout.size) + let deltaFrame = CGRect(origin: CGPoint(x: valueFrame.maxX + horizontalSpacing, y: valueFrame.maxY - deltaLayout.size.height - 2.0 - UIScreenPixel), size: deltaLayout.size) targetNode.valueNode.frame = valueFrame targetNode.titleNode.frame = titleFrame @@ -296,7 +334,7 @@ class StatsOverviewItemNode: ListViewItemNode { insets = itemListNeighborsGroupedInsets(neighbors, params) } - let twoColumnLayout = "".isEmpty + var twoColumnLayout = true var topLeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)? var topRightItemLayoutAndApply: (CGSize, () -> ValueItemNode)? @@ -321,78 +359,96 @@ class StatsOverviewItemNode: ListViewItemNode { if let stats = item.stats as? MessageStats { topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(stats.views), item.presentationData.strings.Stats_Message_Views, + nil, nil ) topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, params.width, item.presentationData, item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", item.presentationData.strings.Stats_Message_PublicShares, + nil, nil ) middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(stats.reactions), item.presentationData.strings.Stats_Message_Reactions, + nil, nil ) middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( + item.context, params.width, item.presentationData, item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, + nil, nil ) height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing } else if let _ = item.stats as? StoryStats, let views = item.storyViews { topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(views.seenCount), item.presentationData.strings.Stats_Message_Views, + nil, nil ) topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, params.width, item.presentationData, item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", item.presentationData.strings.Stats_Message_PublicShares, + nil, nil ) middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(views.reactedCount), item.presentationData.strings.Stats_Message_Reactions, + nil, nil ) middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( + item.context, params.width, item.presentationData, item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, + nil, nil ) height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing } else if let stats = item.stats as? ChannelBoostStatus { topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, params.width, item.presentationData, "\(stats.level)", item.presentationData.strings.Stats_Boosts_Level, + nil, nil ) @@ -402,18 +458,22 @@ class StatsOverviewItemNode: ListViewItemNode { } topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, params.width, item.presentationData, "≈\(Int(stats.premiumAudience?.value ?? 0))", item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers, - (String(format: "%.02f%%", premiumSubscribers * 100.0), .generic) + (String(format: "%.02f%%", premiumSubscribers * 100.0), .generic), + nil ) middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, params.width, item.presentationData, "\(stats.boosts)", item.presentationData.strings.Stats_Boosts_ExistingBoosts, + nil, nil ) @@ -424,10 +484,12 @@ class StatsOverviewItemNode: ListViewItemNode { boostsLeft = 0 } middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( + item.context, params.width, item.presentationData, "\(boostsLeft)", item.presentationData.strings.Stats_Boosts_BoostsToLevelUp, + nil, nil ) @@ -447,11 +509,13 @@ class StatsOverviewItemNode: ListViewItemNode { let followersDelta = deltaText(stats.followers) topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(Int(stats.followers.current)), item.presentationData.strings.Stats_Followers, - (followersDelta.text, followersDelta.positive ? .positive : .negative) + (followersDelta.text, followersDelta.positive ? .positive : .negative), + nil ) var enabledNotifications: Double = 0.0 @@ -459,10 +523,12 @@ class StatsOverviewItemNode: ListViewItemNode { enabledNotifications = stats.enabledNotifications.value / stats.enabledNotifications.total } topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, params.width, item.presentationData, String(format: "%.02f%%", enabledNotifications * 100.0), item.presentationData.strings.Stats_EnabledNotifications, + nil, nil ) @@ -516,56 +582,68 @@ class StatsOverviewItemNode: ListViewItemNode { if let (value, title, delta) = items[0] { middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, params.width, item.presentationData, value, title, - delta + delta, + nil ) } if let (value, title, delta) = items[1] { middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( + item.context, params.width, item.presentationData, value, title, - delta + delta, + nil ) } if let (value, title, delta) = items[2] { middle2LeftItemLayoutAndApply = makeMiddle2LeftItemLayout( + item.context, params.width, item.presentationData, value, title, - delta + delta, + nil ) } if let (value, title, delta) = items[3] { middle2RightItemLayoutAndApply = makeMiddle2RightItemLayout( + item.context, params.width, item.presentationData, value, title, - delta + delta, + nil ) } if let (value, title, delta) = items[4] { bottomLeftItemLayoutAndApply = makeBottomLeftItemLayout( + item.context, params.width, item.presentationData, value, title, - delta + delta, + nil ) } if let (value, title, delta) = items[5] { bottomRightItemLayoutAndApply = makeBottomRightItemLayout( + item.context, params.width, item.presentationData, value, title, - delta + delta, + nil ) } @@ -583,37 +661,45 @@ class StatsOverviewItemNode: ListViewItemNode { let membersDelta = deltaText(stats.members) topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(Int(stats.members.current)), item.presentationData.strings.Stats_GroupMembers, - (membersDelta.text, membersDelta.positive ? .positive : .negative) + (membersDelta.text, membersDelta.positive ? .positive : .negative), + nil ) let messagesDelta = deltaText(stats.messages) topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(Int(stats.messages.current)), item.presentationData.strings.Stats_GroupMessages, - (messagesDelta.text, messagesDelta.positive ? .positive : .negative) + (messagesDelta.text, messagesDelta.positive ? .positive : .negative), + nil ) if displayBottomRow { middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(Int(stats.viewers.current)), item.presentationData.strings.Stats_GroupViewers, - (viewersDelta.text, viewersDelta.positive ? .positive : .negative) + (viewersDelta.text, viewersDelta.positive ? .positive : .negative), + nil ) middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout( + item.context, params.width, item.presentationData, compactNumericCountString(Int(stats.posters.current)), item.presentationData.strings.Stats_GroupPosters, - (postersDelta.text, postersDelta.positive ? .positive : .negative) + (postersDelta.text, postersDelta.positive ? .positive : .negative), + nil ) } @@ -622,6 +708,40 @@ class StatsOverviewItemNode: ListViewItemNode { } else { height += topLeftItemLayoutAndApply!.0.height * 4.0 + verticalSpacing * 3.0 } + } else if let _ = item.stats as? MonetizationStats { + twoColumnLayout = false + + topLeftItemLayoutAndApply = makeTopLeftItemLayout( + item.context, + params.width, + item.presentationData, + "54.12", + "Balance Available to Withdraw", + ("≈$123", .generic), + item.animatedEmoji + ) + + topRightItemLayoutAndApply = makeTopRightItemLayout( + item.context, + params.width, + item.presentationData, + "84.52", + "Proceeds Since Last Withdrawal", + ("≈$226", .generic), + item.animatedEmoji + ) + + middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout( + item.context, + params.width, + item.presentationData, + "692.52", + "Total Lifetime Proceeds", + ("≈$1858", .generic), + item.animatedEmoji + ) + + height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0 } let contentSize = CGSize(width: params.width, height: height) diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift new file mode 100644 index 0000000000..2119b46f74 --- /dev/null +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -0,0 +1,475 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BundleIconComponent +import BalancedTextComponent +import MultilineTextComponent +import SolidRoundedButtonComponent +import LottieComponent +import AccountContext +import TelegramStringFormatting +import PremiumPeerShortcutComponent + +enum MonetizationTransaction: Equatable { + case incoming(amount: Int64, fromTimestamp: Int32, toTimestamp: Int32) + case outgoing(amount: Int64, timestamp: Int32, address: String, explorerUrl: String) + + var amount: Int64 { + switch self { + case let .incoming(amount, _, _), let .outgoing(amount, _, _, _): + return amount + } + } +} + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peer: EnginePeer + let transaction: MonetizationTransaction + let openExplorer: (String) -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + peer: EnginePeer, + transaction: MonetizationTransaction, + openExplorer: @escaping (String) -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.peer = peer + self.transaction = transaction + self.openExplorer = openExplorer + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.transaction != rhs.transaction { + return false + } + return true + } + + final class State: ComponentState { + var cachedCloseImage: (UIImage, PresentationTheme)? + + let playOnce = ActionSlot() + private var didPlayAnimation = false + + func playAnimationIfNeeded() { + guard !self.didPlayAnimation else { + return + } + self.didPlayAnimation = true + self.playOnce.invoke(Void()) + } + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let closeButton = Child(Button.self) + + let amount = Child(MultilineTextComponent.self) + let title = Child(MultilineTextComponent.self) + let date = Child(MultilineTextComponent.self) + let address = Child(MultilineTextComponent.self) + let peerShortcut = Child(PremiumPeerShortcutComponent.self) + + let actionButton = Child(SolidRoundedButtonComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let theme = environment.theme + let strings = environment.strings + let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 + environment.safeInsets.left + + let titleFont = Font.semibold(17.0) + let textFont = Font.regular(17.0) + let fixedFont = Font.monospace(17.0) + + let textColor = theme.actionSheet.primaryTextColor + let secondaryTextColor = theme.actionSheet.secondaryTextColor + + var contentSize = CGSize(width: context.availableSize.width, height: 45.0) + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { [weak component] in + component?.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + ) + + let amountString: NSMutableAttributedString + let dateString: String + let titleString: String + let subtitleString: String + let buttonTitle: String + let explorerUrl: String? + + let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold) + let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold) + + //TODO:localize + switch component.transaction { + case let .incoming(amount, fromTimestamp, toTimestamp): + amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: theme.list.itemDisclosureActions.constructive.fillColor).mutableCopy() as! NSMutableAttributedString + amountString.append(NSAttributedString(string: " TON", font: fractionalFont, textColor: theme.list.itemDisclosureActions.constructive.fillColor)) + dateString = "\(stringForFullDate(timestamp: fromTimestamp, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForFullDate(timestamp: toTimestamp, strings: strings, dateTimeFormat: dateTimeFormat))" + titleString = "Proceeds from Ads displayed in" + subtitleString = "" + buttonTitle = strings.Common_OK + explorerUrl = nil + case let .outgoing(amount, timestamp, address, explorerUrlValue): + amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: theme.list.itemDestructiveColor).mutableCopy() as! NSMutableAttributedString + amountString.append(NSAttributedString(string: " TON", font: fractionalFont, textColor: theme.list.itemDestructiveColor)) + dateString = stringForFullDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat) + titleString = "Balance Withdrawal to" + subtitleString = formatAddress(address) + buttonTitle = "View in Blockchain Explorer" + explorerUrl = explorerUrlValue + } + + let amount = amount.update( + component: MultilineTextComponent( + text: .plain(amountString), + horizontalAlignment: .center, + maximumNumberOfLines: 1, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(amount + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amount.size.height / 2.0)) + ) + contentSize.height += amount.size.height + contentSize.height += -5.0 + + let date = date.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: dateString, font: textFont, textColor: secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(date + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + date.size.height / 2.0)) + ) + contentSize.height += date.size.height + contentSize.height += 32.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: titleFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 3.0 + + if !subtitleString.isEmpty { + let address = address.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleString, font: fixedFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(address + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + address.size.height / 2.0)) + ) + contentSize.height += address.size.height + contentSize.height += 50.0 + } else { + contentSize.height += 5.0 + let peerShortcut = peerShortcut.update( + component: PremiumPeerShortcutComponent( + context: component.context, + theme: theme, + peer: component.peer + + ), + availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(peerShortcut + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) + ) + contentSize.height += peerShortcut.size.height + contentSize.height += 50.0 + } + + let actionButton = actionButton.update( + component: SolidRoundedButtonComponent( + title: buttonTitle, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: theme.list.itemCheckColors.fillColor, + backgroundColors: [], + foregroundColor: theme.list.itemCheckColors.foregroundColor + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + action: { + component.dismiss() + if let explorerUrl { + component.openExplorer(explorerUrl) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + context.add(actionButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) + ) + contentSize.height += actionButton.size.height + contentSize.height += 22.0 + + contentSize.height += environment.safeInsets.bottom + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peer: EnginePeer + let transaction: MonetizationTransaction + let openExplorer: (String) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + transaction: MonetizationTransaction, + openExplorer: @escaping (String) -> Void + ) { + self.context = context + self.peer = peer + self.transaction = transaction + self.openExplorer = openExplorer + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.transaction != rhs.transaction { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + peer: context.component.peer, + transaction: context.component.transaction, + openExplorer: context.component.openExplorer, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + externalState: sheetExternalState, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + + +final class TransactionInfoScreen: ViewControllerComponentContainer { + private let context: AccountContext + + init( + context: AccountContext, + peer: EnginePeer, + transaction: MonetizationTransaction, + openExplorer: @escaping (String) -> Void + ) { + self.context = context + + super.init( + context: context, + component: SheetContainerComponent( + context: context, + peer: peer, + transaction: transaction, + openExplorer: openExplorer + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index cd47fab563..4cde209664 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -40,7 +40,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/StickerPeekUI:StickerPeekUI", "//submodules/Pasteboard:Pasteboard", - "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController" + "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 9d23025fd1..a2058cde6a 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -23,6 +23,7 @@ import AnimationCache import MultiAnimationRenderer import Pasteboard import StickerPackEditTitleController +import EntityKeyboard private enum StickerPackPreviewGridEntry: Comparable, Identifiable { case sticker(index: Int, stableId: Int, stickerItem: StickerPackItem?, isEmpty: Bool, isPremium: Bool, isLocked: Bool, isEditing: Bool, isAdd: Bool) @@ -537,8 +538,8 @@ private final class StickerPackContainer: ASDisplayNode { if canEdit { menuItems.append(.action(ContextMenuActionItem(text: "Edit Sticker", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - if let _ = self { - + if let self { + self.openEditSticker(item.file) } }))) if !strongSelf.isEditing { @@ -1137,6 +1138,7 @@ private final class StickerPackContainer: ASDisplayNode { self.presentInGlobalOverlay(contextController, nil) } + private let stickerPickerInputData = Promise() private func presentAddStickerOptions() { let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] @@ -1155,9 +1157,8 @@ private final class StickerPackContainer: ASDisplayNode { guard let self, let controller = self.controller else { return } - controller.controllerNode.dismiss() - self.presentAddExistingSticker() + controller.controllerNode.dismiss() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in @@ -1165,22 +1166,158 @@ private final class StickerPackContainer: ASDisplayNode { }) ])]) self.presentInGlobalOverlay(actionSheet, nil) + + + let emojiItems = EmojiPagerContentComponent.emojiInputData( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + isStandalone: false, + subject: .emoji, + hasTrending: true, + topReactionItems: [], + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: true, + chatPeerId: self.context.account.peerId, + hasSearch: true, + forceHasPremium: true + ) + + let stickerItems = EmojiPagerContentComponent.stickerInputData( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], + stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], + chatPeerId: self.context.account.peerId, + hasSearch: true, + hasTrending: true, + forceHasPremium: true + ) + + let signal = combineLatest( + queue: .mainQueue(), + emojiItems, + stickerItems + ) |> map { emoji, stickers -> StickerPickerInput in + return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil) + } + + self.stickerPickerInputData.set(signal) } private func presentCreateSticker() { - let controller = self.context.sharedContext.makeStickerMediaPickerScreen( - context: self.context, + guard let (info, _, _) = self.currentStickerPack else { + return + } + let context = self.context + let presentationData = self.presentationData + let updatedPresentationData = self.controller?.updatedPresentationData + let navigationController = self.controller?.parentNavigationController as? NavigationController + + var dismissImpl: (() -> Void)? + let mainController = context.sharedContext.makeStickerMediaPickerScreen( + context: context, getSourceRect: { return .zero }, completion: { result, transitionView, transitionRect, transitionImage, completion, dismissed in - + let editorController = context.sharedContext.makeStickerEditorScreen( + context: context, + source: result, + transitionArguments: (transitionView, transitionRect, transitionImage), + completion: { file in + dismissImpl?() + let sticker = ImportSticker( + resource: file.resource, + emojis: ["😀"], + dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), + mimeType: file.mimeType, + keywords: "" + ) + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + let _ = (context.engine.stickers.addStickerToStickerSet(packReference: packReference, sticker: sticker) + |> deliverOnMainQueue).start(completed: { + let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil) + (navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root)) + + Queue.mainQueue().after(0.1) { + packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + } + }) + } + ) + navigationController?.pushViewController(editorController) }, dismissed: {} ) - self.controller?.parentNavigationController?.pushViewController(controller) + dismissImpl = { [weak mainController] in + mainController?.dismiss() + } + navigationController?.pushViewController(mainController) } private func presentAddExistingSticker() { + guard let (info, _, _) = self.currentStickerPack else { + return + } + let presentationData = self.presentationData + let updatedPresentationData = self.controller?.updatedPresentationData + let navigationController = self.controller?.parentNavigationController as? NavigationController + let context = self.context + let controller = self.context.sharedContext.makeStickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData, completion: { file in + let sticker = ImportSticker( + resource: file.resource, + emojis: ["😀"], + dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), + mimeType: file.mimeType, + keywords: "" + ) + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + let _ = (context.engine.stickers.addStickerToStickerSet(packReference: packReference, sticker: sticker) + |> deliverOnMainQueue).start(completed: { + let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil) + (navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root)) + + Queue.mainQueue().after(0.1) { + packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + } + }) + }) + navigationController?.pushViewController(controller) + } + + private func openEditSticker(_ initialFile: TelegramMediaFile) { + guard let (info, _, _) = self.currentStickerPack else { + return + } + let context = self.context + let updatedPresentationData = self.controller?.updatedPresentationData + let navigationController = self.controller?.parentNavigationController as? NavigationController + + self.controller?.dismiss() + + let controller = context.sharedContext.makeStickerEditorScreen( + context: context, + source: initialFile, + transitionArguments: nil, + completion: { file in + let sticker = ImportSticker( + resource: file.resource, + emojis: ["😀"], + dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), + mimeType: file.mimeType, + keywords: "" + ) + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + + let _ = (context.engine.stickers.replaceSticker(previousSticker: .stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: initialFile), sticker: sticker) + |> deliverOnMainQueue).start(completed: { + let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil) + (navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root)) + }) + } + ) + navigationController?.pushViewController(controller) } private func presentEditPackTitle() { @@ -2408,7 +2545,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { } } -public final class StickerPackScreenImpl: ViewController { +public final class StickerPackScreenImpl: ViewController, StickerPackScreen { private let context: AccountContext fileprivate var presentationData: PresentationData fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 0c4dc8f3b3..389fc5dc23 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -526,7 +526,7 @@ if "".isEmpty { return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil))) } - return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds))) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { @@ -924,7 +924,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili |> mapError { _ -> PendingMessageUploadError in } |> mapToSignal { inputPeer -> Signal in if let inputPeer = inputPeer { - return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, ttlSeconds: ttlSeconds))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, ttlSeconds: ttlSeconds))) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift index 10c2105e85..0479620b2d 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift @@ -64,7 +64,7 @@ public func standaloneUploadedImage(postbox: Postbox, network: Network, peerId: |> mapError { _ -> StandaloneUploadMediaError in } |> mapToSignal { inputPeer -> Signal in if let inputPeer = inputPeer { - return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil))) |> mapError { _ -> StandaloneUploadMediaError in return .generic } |> mapToSignal { media -> Signal in switch media { @@ -158,7 +158,7 @@ public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: P if let _ = thumbnailFile { flags |= 1 << 2 } - return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, ttlSeconds: nil))) + return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, ttlSeconds: nil))) |> mapError { _ -> StandaloneUploadMediaError in return .generic } |> mapToSignal { media -> Signal in switch media { diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index cfb0b59954..99d113350c 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -96,7 +96,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var updatedTimestamp: Int32? if let apiMessage = apiMessage { switch apiMessage { - case let .message(_, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _): updatedTimestamp = date case .messageEmpty: break diff --git a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift index 2976fef9ca..117c3ea820 100644 --- a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift +++ b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift @@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService { self.putNext(groups) } case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod): - let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil) + let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { @@ -74,7 +74,7 @@ class UpdateMessageService: NSObject, MTMessageService { let generatedPeerId = Api.Peer.peerUser(userId: userId) - let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil) + let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index a5f7b9db77..93b58ad893 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -104,7 +104,7 @@ extension Api.MessageMedia { extension Api.Message { var rawId: Int32 { switch self { - case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return id case let .messageEmpty(_, id, _): return id @@ -115,7 +115,7 @@ extension Api.Message { func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? { switch self { - case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = messagePeerId.peerId return MessageId(peerId: peerId, namespace: namespace, id: id) case let .messageEmpty(_, id, peerId): @@ -132,7 +132,7 @@ extension Api.Message { var peerId: PeerId? { switch self { - case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = messagePeerId.peerId return peerId case let .messageEmpty(_, _, peerId): @@ -145,7 +145,7 @@ extension Api.Message { var timestamp: Int32? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return date case let .messageService(_, _, _, _, _, date, _, _): return date @@ -156,7 +156,7 @@ extension Api.Message { var preCachedResources: [(MediaResource, Data)]? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _): return media?.preCachedResources default: return nil @@ -165,7 +165,7 @@ extension Api.Message { var preCachedStories: [StoryId: Api.StoryItem]? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _): return media?.preCachedStories default: return nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 7672673002..a6fb320ca7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -52,7 +52,7 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour var attributes: [Api.DocumentAttribute] = [] attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil)) attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) - return account.network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) + return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) |> mapError { _ -> UploadStickerError in return .generic } |> mapToSignal { media -> Signal in switch media { @@ -384,6 +384,60 @@ func _internal_deleteStickerFromStickerSet(account: Account, sticker: FileMediaR } } +public enum ReplaceStickerError { + case generic +} + +func _internal_replaceSticker(account: Account, previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal { + guard let previousResource = previousSticker.media.resource as? CloudDocumentMediaResource else { + return .fail(.generic) + } + + let uploadSticker: Signal + if let resource = sticker.resource as? CloudDocumentMediaResource { + uploadSticker = .single(.complete(resource, sticker.mimeType)) + } else { + uploadSticker = account.postbox.loadedPeerWithId(account.peerId) + |> castError(ReplaceStickerError.self) + |> mapToSignal { peer in + return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) + |> mapError { _ -> ReplaceStickerError in + return .generic + } + } + } + return uploadSticker + |> mapToSignal { uploadedSticker in + guard case let .complete(resource, _) = uploadedSticker else { + return .complete() + } + + var flags: Int32 = 0 + if sticker.keywords.count > 0 { + flags |= (1 << 1) + } + let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.first ?? "", maskCoords: nil, keywords: sticker.keywords) + + return account.network.request(Api.functions.stickers.replaceSticker(sticker: .inputDocument(id: previousResource.fileId, accessHash: previousResource.accessHash, fileReference: Buffer(data: previousResource.fileReference ?? Data())), newSticker: inputSticker)) + |> mapError { error -> ReplaceStickerError in + return .generic + } + |> mapToSignal { result -> Signal in + guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else { + return .complete() + } + return account.postbox.transaction { transaction -> Void in + if transaction.getItemCollectionInfo(collectionId: info.id) != nil { + transaction.replaceItemCollectionItems(collectionId: info.id, items: items) + } + cacheStickerPack(transaction: transaction, info: info, items: items) + } + |> castError(ReplaceStickerError.self) + |> ignoreValues + } + } +} + func _internal_getMyStickerSets(account: Account) -> Signal<[(StickerPackCollectionInfo, StickerPackItem?)], NoError> { return account.network.request(Api.functions.messages.getMyStickers(offsetId: 0, limit: 100)) |> map(Optional.init) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 862d38351e..01db7419d8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -106,6 +106,10 @@ public extension TelegramEngine { return _internal_deleteStickerFromStickerSet(account: self.account, sticker: sticker) } + public func replaceSticker(previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal { + return _internal_replaceSticker(account: self.account, previousSticker: previousSticker, sticker: sticker) + } + public func getMyStickerSets() -> Signal<[(StickerPackCollectionInfo, StickerPackItem?)], NoError> { return _internal_getMyStickerSets(account: self.account) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 3ff80453f1..5892c6150a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -437,7 +437,8 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen", "//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen", "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", - "//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen" + "//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen", + "//submodules/TelegramUI/Components/StickerPickerScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 16c144e196..950d8f0a0d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1615,6 +1615,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break } } + maximumContentWidth = max(0.0, maximumContentWidth) var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, BubbleItemAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 61fe835f66..58d86e83ba 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -244,7 +244,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { bubbleInsets = layoutConstants.image.bubbleInsets } - sizeCalculation = .constrained(CGSize(width: constrainedSize.width - bubbleInsets.left - bubbleInsets.right, height: constrainedSize.height)) + sizeCalculation = .constrained(CGSize(width: max(0.0, constrainedSize.width - bubbleInsets.left - bubbleInsets.right), height: constrainedSize.height)) case .mosaic: bubbleInsets = UIEdgeInsets() sizeCalculation = .unconstrained diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 90da47d24d..ffca635585 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -7538,3 +7538,19 @@ private final class FadingMaskLayer: SimpleLayer { self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: gradientHeight), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight)) } } + +public struct StickerPickerInputData: StickerPickerInput, Equatable { + public var emoji: EmojiPagerContentComponent + public var stickers: EmojiPagerContentComponent? + public var gifs: GifPagerContentComponent? + + public init( + emoji: EmojiPagerContentComponent, + stickers: EmojiPagerContentComponent?, + gifs: GifPagerContentComponent? + ) { + self.emoji = emoji + self.stickers = stickers + self.gifs = gifs + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 8d0bee5074..4cf696489b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -106,6 +106,7 @@ public final class MediaEditor { case asset(PHAsset) case draft(MediaEditorDraft) case message(MessageId) + case sticker(TelegramMediaFile) var dimensions: PixelDimensions { switch self { @@ -117,6 +118,8 @@ public final class MediaEditor { return draft.dimensions case .message: return PixelDimensions(width: 1080, height: 1920) + case .sticker: + return PixelDimensions(width: 1080, height: 1920) } } } @@ -654,6 +657,19 @@ public final class MediaEditor { ) } } + case .sticker: + let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + }, opaque: false, scale: 1.0) + textureSource = .single( + TextureSourceResult( + image: image, + nightImage: nil, + player: nil, + playerIsReference: false, + gradientColors: GradientColors(top: .clear, bottom: .clear) + ) + ) } self.textureSourceDisposable = (textureSource diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index b210d78e82..c78b21af71 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -55,7 +55,8 @@ swift_library( "//submodules/WebPBinding", "//submodules/StickerResources", "//submodules/StickerPeekUI", - "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController" + "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController", + "//submodules/TelegramUI/Components/StickerPickerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index ac834f1ed8..e1318c1b3d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -193,6 +193,8 @@ extension MediaEditorScreen { if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) { innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) } + case .sticker: + break } if case let .draft(draft, _) = actualSubject { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index e8adab8d0f..de6ec17e31 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -45,6 +45,7 @@ import WebPBinding import StickerResources import StickerPeekUI import StickerPackEditTitleController +import StickerPickerScreen private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -2054,8 +2055,14 @@ let storyMaxVideoDuration: Double = 60.0 public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate { public enum Mode { + public enum StickerEditorMode { + case generic + case addingToPack + case editing + } + case storyEditor - case stickerEditor + case stickerEditor(mode: StickerEditorMode) } public enum TransitionIn { @@ -2150,7 +2157,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let ciContext = CIContext(options: [.workingColorSpace : NSNull()]) - private let stickerPickerInputData = Promise() + private let stickerPickerInputData = Promise() fileprivate var availableReactions: [ReactionItem] = [] private var availableReactionsDisposable: Disposable? @@ -2211,8 +2218,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.stickerTransparentView = UIImageView() self.stickerTransparentView.clipsToBounds = true + var isStickerEditor = false + if case .stickerEditor = controller.mode { + isStickerEditor = true + } + self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions)) - self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true, isStickerEditor: controller.mode == .stickerEditor) + self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true, isStickerEditor: isStickerEditor) self.entitiesView.getEntityCenterPosition = { return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) } @@ -2316,7 +2328,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate queue: .mainQueue(), emojiItems, stickerItems - ) |> map { emoji, stickers -> StickerPickerInputData in + ) |> map { emoji, stickers -> StickerPickerInput in return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil) } |> afterNext { [weak self] _ in if let self { @@ -2495,7 +2507,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let mediaEditor = MediaEditor(context: self.context, mode: controller.mode == .stickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) + var isStickerEditor = false + if case .stickerEditor = controller.mode { + isStickerEditor = true + } + + let mediaEditor = MediaEditor(context: self.context, mode: isStickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = controller.initialVideoPosition { mediaEditor.seek(initialVideoPosition, andPlay: true) } @@ -2630,6 +2647,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.readyValue.set(.single(true)) }) }) + } else if case let .sticker(sticker) = effectiveSubject { + let stickerEntity = DrawingStickerEntity(content: .file(sticker, .sticker)) + stickerEntity.referenceDrawingSize = storyDimensions + stickerEntity.scale = 4.0 + stickerEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + self.entitiesView.add(stickerEntity, announce: false) } self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in @@ -4066,7 +4089,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let controller = self.controller, case .stickerEditor = controller.mode { hasInteractiveStickers = false } - let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji, hasGifs: hasInteractiveStickers, hasInteractiveStickers: hasInteractiveStickers) + let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: hasInteractiveStickers, hasInteractiveStickers: hasInteractiveStickers) controller.completion = { [weak self] content in if let self { if let content { @@ -4429,6 +4452,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case asset(PHAsset) case draft(MediaEditorDraft, Int64?) case message([MessageId]) + case sticker(TelegramMediaFile) var dimensions: PixelDimensions { switch self { @@ -4440,6 +4464,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return draft.dimensions case .message: return PixelDimensions(width: 1080, height: 1920) + case .sticker: + return PixelDimensions(width: 1080, height: 1920) } } @@ -4455,6 +4481,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return .draft(draft) case let .message(messageIds): return .message(messageIds.first!) + case let .sticker(sticker): + return .sticker(sticker) } } @@ -4474,6 +4502,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return draft.isVideo case .message: return false + case .sticker: + return false } } } @@ -5558,6 +5588,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return (image, nil) } duration = 5.0 + case .sticker: + let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + }, opaque: false, scale: 1.0) + let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png" + if let data = image?.pngData() { + try? data.write(to: URL(fileURLWithPath: tempImagePath)) + } + videoResult = .single(.imageFile(path: tempImagePath)) + duration = 3.0 + + firstFrame = .single((image, nil)) } let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult) @@ -5672,10 +5714,133 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) } } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let file = stickerFile(resource: resource, size: Int64(0), dimensions: PixelDimensions(image.size)) - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + var menuItems: [ContextMenuItem] = [] + if case let .stickerEditor(mode) = self.mode { + switch mode { + case .generic: + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + self.completion(MediaEditorScreen.Result( + media: .sticker(file: file), + mediaAreas: [], + caption: NSAttributedString(), + options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), + stickers: [], + randomId: 0 + ), { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) + }))) + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + self.uploadSticker(file, action: .addToFavorites) + }))) + menuItems.append(.action(ContextMenuActionItem(text: "Add to Sticker Set", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in + guard let self else { + return + } + + var contextItems: [ContextMenuItem] = [] + contextItems.append(.action(ContextMenuActionItem(text: "Back", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c, _ in + c.popItems() + }))) + + contextItems.append(.separator) + + contextItems.append(.action(ContextMenuActionItem(text: "New Sticker Set", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] _, f in + if let self { + self.presentCreateStickerPack(file: file, completion: { + f(.default) + }) + } + }))) + + let thumbSize = CGSize(width: 24.0, height: 24.0) + for (pack, firstItem) in self.myStickerPacks { + let thumbnailResource = pack.thumbnail?.resource ?? firstItem?.file.resource + let thumbnailIconSource: ContextMenuActionItemIconSource? + if let thumbnailResource { + var resourceId: Int64 = 0 + if let resource = thumbnailResource as? CloudDocumentMediaResource { + resourceId = resource.fileId + } + let thumbnailFile = firstItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: []) + + let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .stickerPack(stickerPack: .id(id: pack.id.id, accessHash: pack.accessHash), media: thumbnailFile)).start() + thumbnailIconSource = ContextMenuActionItemIconSource( + size: thumbSize, + signal: chatMessageStickerPackThumbnail(postbox: self.context.account.postbox, resource: thumbnailResource) + |> map { generator -> UIImage? in + return generator(TransformImageArguments(corners: ImageCorners(), imageSize: thumbSize, boundingSize: thumbSize, intrinsicInsets: .zero))?.generateImage() + } + ) + } else { + thumbnailIconSource = nil + } + contextItems.append(.action(ContextMenuActionItem(text: pack.title, icon: { _ in return nil }, iconSource: thumbnailIconSource, iconPosition: .left, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + self.uploadSticker(file, action: .addToStickerPack(pack: .id(id: pack.id.id, accessHash: pack.accessHash), title: pack.title)) + }))) + } + + let items = ContextController.Items( + id: 1, + content: .list(contextItems), + context: nil, + reactionItems: [], + selectedReactionItems: Set(), + reactionsTitle: nil, + reactionsLocked: false, + animationCache: nil, + alwaysAllowPremiumReactions: false, + allPresetReactionsAreAvailable: false, + getEmojiContent: nil, + disablePositionLock: false, + tip: nil, + tipSignal: nil, + dismissed: nil + ) + c.pushItems(items: .single(items)) + }))) + case .editing: + menuItems.append(.action(ContextMenuActionItem(text: "Replace Sticker", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + self.uploadSticker(file, action: .upload) + }))) + case .addingToPack: + menuItems.append(.action(ContextMenuActionItem(text: "Add to Sticker Set", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in + guard let self else { + return + } + f(.default) + self.uploadSticker(file, action: .upload) + }))) + } + } + let peekController = PeekController( presentationData: presentationData, content: StickerPreviewPeekContent( @@ -5684,108 +5849,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate strings: presentationData.strings, item: .image(image), isCreating: true, - menu: [ - .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in - f(.default) - guard let self else { - return - } - self.completion(MediaEditorScreen.Result( - media: .sticker(file: file), - mediaAreas: [], - caption: NSAttributedString(), - options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), - stickers: [], - randomId: 0 - ), { [weak self] finished in - self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in - self?.dismiss() - Queue.mainQueue().justDispatch { - finished() - } - }) - }) - })), - .action(ContextMenuActionItem(text: presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in - f(.default) - guard let self else { - return - } - self.uploadSticker(file, action: .addToFavorites) - })), - .action(ContextMenuActionItem(text: "Add to Sticker Set", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in - guard let self else { - return - } - - var contextItems: [ContextMenuItem] = [] - contextItems.append(.action(ContextMenuActionItem(text: "Back", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { c, _ in - c.popItems() - }))) - - contextItems.append(.separator) - - contextItems.append(.action(ContextMenuActionItem(text: "New Sticker Set", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] _, f in - if let self { - self.presentCreateStickerPack(file: file, completion: { - f(.default) - }) - } - }))) - - let thumbSize = CGSize(width: 24.0, height: 24.0) - for (pack, firstItem) in self.myStickerPacks { - let thumbnailResource = pack.thumbnail?.resource ?? firstItem?.file.resource - let thumbnailIconSource: ContextMenuActionItemIconSource? - if let thumbnailResource { - var resourceId: Int64 = 0 - if let resource = thumbnailResource as? CloudDocumentMediaResource { - resourceId = resource.fileId - } - let thumbnailFile = firstItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: []) - - let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .stickerPack(stickerPack: .id(id: pack.id.id, accessHash: pack.accessHash), media: thumbnailFile)).start() - thumbnailIconSource = ContextMenuActionItemIconSource( - size: thumbSize, - signal: chatMessageStickerPackThumbnail(postbox: self.context.account.postbox, resource: thumbnailResource) - |> map { generator -> UIImage? in - return generator(TransformImageArguments(corners: ImageCorners(), imageSize: thumbSize, boundingSize: thumbSize, intrinsicInsets: .zero))?.generateImage() - } - ) - } else { - thumbnailIconSource = nil - } - contextItems.append(.action(ContextMenuActionItem(text: pack.title, icon: { _ in return nil }, iconSource: thumbnailIconSource, iconPosition: .left, action: { [weak self] _, f in - guard let self else { - return - } - f(.default) - self.uploadSticker(file, action: .addToStickerPack(pack: .id(id: pack.id.id, accessHash: pack.accessHash), title: pack.title)) - }))) - } - - let items = ContextController.Items( - id: 1, - content: .list(contextItems), - context: nil, - reactionItems: [], - selectedReactionItems: Set(), - reactionsTitle: nil, - reactionsLocked: false, - animationCache: nil, - alwaysAllowPremiumReactions: false, - allPresetReactionsAreAvailable: false, - getEmojiContent: nil, - disablePositionLock: false, - tip: nil, - tipSignal: nil, - dismissed: nil - ) - c.pushItems(items: .single(items)) - })) - ], + menu: menuItems, reactionItems: self.node.availableReactions, openPremiumIntro: {} ), @@ -5806,6 +5870,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case addToFavorites case createStickerPack(title: String) case addToStickerPack(pack: StickerPackReference, title: String) + case upload } private func presentCreateStickerPack(file: TelegramMediaFile, completion: @escaping () -> Void) { @@ -5938,6 +6003,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate |> map { _ in return status } + case .upload: + return .single(status) } } } @@ -5953,9 +6020,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.updateEditProgress(progress, cancel: { [weak self] in self?.stickerUploadDisposable.set(nil) }) - case .complete: + case let .complete(resource, _): let navigationController = self.navigationController as? NavigationController - self.completion(MediaEditorScreen.Result(), { [weak self] finished in + + let result: MediaEditorScreen.Result + if case .upload = action { + let file = stickerFile(resource: resource, size: resource.size ?? 0, dimensions: dimensions) + result = MediaEditorScreen.Result( + media: .sticker(file: file), + mediaAreas: [], + caption: NSAttributedString(), + options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), + stickers: [], + randomId: 0 + ) + } else { + result = MediaEditorScreen.Result() + } + + self.completion(result, { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in guard let self else { return @@ -5972,7 +6055,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } case let .addToStickerPack(packReference, title): let navigationController = self.navigationController as? NavigationController - self.dismiss() if let navigationController { Queue.mainQueue().after(0.2) { let controller = self.context.sharedContext.makeStickerPackScreen(context: self.context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, parentNavigationController: nil, sendSticker: nil) @@ -6102,6 +6184,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return image.flatMap({ .single(.image(image: $0)) }) ?? .complete() } } + case .sticker: + let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + }, opaque: false, scale: 1.0)! + exportSubject = .single(.image(image: image)) } let _ = exportSubject.start(next: { [weak self] exportSubject in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 4b28227d77..62f73db0c4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -8792,7 +8792,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen { previousController.dismiss() } - let controller = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: peer.id, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in + let controller = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: peer.id, subject: .stories, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in if let self { self.openStats(boosts: true, boostStatus: boostStatus) } diff --git a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/BUILD b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/BUILD new file mode 100644 index 0000000000..b59ef6283b --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PremiumPeerShortcutComponent", + module_name = "PremiumPeerShortcutComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/Components/MultilineTextComponent", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift new file mode 100644 index 0000000000..f65f1d3060 --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift @@ -0,0 +1,106 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import AccountContext +import AvatarNode +import MultilineTextComponent + +public final class PremiumPeerShortcutComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let peer: EnginePeer + + public init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { + self.context = context + self.theme = theme + self.peer = peer + } + + public static func ==(lhs: PremiumPeerShortcutComponent, rhs: PremiumPeerShortcutComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView = UIView() + private let avatarNode: AvatarNode + private let text = ComponentView() + + private var component: PremiumPeerShortcutComponent? + private weak var state: EmptyComponentState? + + public override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0)) + + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + self.backgroundView.layer.cornerRadius = 16.0 + + self.addSubview(self.backgroundView) + self.addSubnode(self.avatarNode) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(component: PremiumPeerShortcutComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + self.backgroundView.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3) + + self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0)) + self.avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true + ) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 50.0, height: availableSize.height) + ) + + let size = CGSize(width: 30.0 + textSize.width + 20.0, height: 32.0) + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: 38.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + view.frame = textFrame + } + + self.backgroundView.frame = CGRect(origin: .zero, size: size) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 37f1d5c338..e1a3c4f228 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1111,7 +1111,7 @@ final class ShareWithPeersScreenComponent: Component { self.hapticFeedback.impact(.light) } else { self.postingAvailabilityDisposable.set((component.context.engine.messages.checkStoriesUploadAvailability(target: .peer(peer.id)) - |> deliverOnMainQueue).start(next: { [weak self] status in + |> deliverOnMainQueue).start(next: { [weak self] status in guard let self, let component = self.component else { return } @@ -1134,7 +1134,7 @@ final class ShareWithPeersScreenComponent: Component { if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen { previousController.dismiss() } - let controller = component.context.sharedContext.makePremiumBoostLevelsController(context: component.context, peerId: peer.id, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: true, openStats: nil) + let controller = component.context.sharedContext.makePremiumBoostLevelsController(context: component.context, peerId: peer.id, subject: .stories, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: true, openStats: nil) navigationController.pushViewController(controller) } self.hapticFeedback.impact(.light) diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/BUILD b/submodules/TelegramUI/Components/StickerPickerScreen/BUILD new file mode 100644 index 0000000000..1de601b336 --- /dev/null +++ b/submodules/TelegramUI/Components/StickerPickerScreen/BUILD @@ -0,0 +1,46 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StickerPickerScreen", + module_name = "StickerPickerScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/TelegramNotices", + "//submodules/FeaturedStickersScreen", + "//submodules/Components/PagerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/EntityKeyboardGifContent", + "//submodules/TelegramUI/Components/EntityKeyboard", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/MediaEditor", + "//submodules/TelegramUI/Components/CameraButtonComponent", + "//submodules/ContextUI", + "//submodules/UndoUI", + "//submodules/GalleryUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift similarity index 97% rename from submodules/DrawingUI/Sources/StickerPickerScreen.swift rename to submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index a7c1e190ad..43c3a1a738 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -17,28 +17,11 @@ import ChatEntityKeyboardInputNode import ContextUI import ChatPresentationInterfaceState import MediaEditor -import StickerPackPreviewUI import EntityKeyboardGifContent -import GalleryUI -import UndoUI import CameraButtonComponent import BundleIconComponent - -public struct StickerPickerInputData: Equatable { - var emoji: EmojiPagerContentComponent - var stickers: EmojiPagerContentComponent? - var gifs: GifPagerContentComponent? - - public init( - emoji: EmojiPagerContentComponent, - stickers: EmojiPagerContentComponent?, - gifs: GifPagerContentComponent? - ) { - self.emoji = emoji - self.stickers = stickers - self.gifs = gifs - } -} +import UndoUI +import GalleryUI private final class StickerSelectionComponent: Component { typealias EnvironmentType = Empty @@ -148,13 +131,13 @@ private final class StickerSelectionComponent: Component { sendSticker: { [weak self] file, silent, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, _ in if let self, let controller = self.component?.getController() { controller.forEachController { c in - if let c = c as? StickerPackScreenImpl { + if let c = c as? (ViewController & StickerPackScreen) { c.dismiss(animated: true) } return true } controller.window?.forEachController({ c in - if let c = c as? StickerPackScreenImpl { + if let c = c as? (ViewController & StickerPackScreen) { c.dismiss(animated: true) } }) @@ -259,7 +242,8 @@ private final class StickerSelectionComponent: Component { let topPanelHeight: CGFloat = 42.0 - let defaultToEmoji = component.getController()?.defaultToEmoji ?? false + let controller = component.getController() + let defaultToEmoji = controller?.defaultToEmoji ?? false let context = component.context let stickerPeekBehavior = EmojiContentPeekBehaviorImpl( @@ -360,7 +344,7 @@ private final class StickerSelectionComponent: Component { deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, inputHeight: 0.0, - displayBottomPanel: true, + displayBottomPanel: controller?.isFullscreen == false, isExpanded: true, clipContentToTopPanel: false, useExternalSearchContainer: false @@ -523,7 +507,9 @@ public class StickerPickerScreen: ViewController { self.containerView.clipsToBounds = true self.containerView.backgroundColor = .clear - self.addSubnode(self.dim) + if !controller.isFullscreen { + self.addSubnode(self.dim) + } self.view.addSubview(self.wrappingView) self.wrappingView.addSubview(self.containerView) @@ -646,12 +632,14 @@ public class StickerPickerScreen: ViewController { self.stickerSearchState.get(), self.emojiSearchState.get() ) - + self.contentDisposable.set(data.start(next: { [weak self] inputData, gifData, stickerSearchState, emojiSearchState in if let strongSelf = self { let presentationData = strongSelf.presentationData - var inputData = inputData - + guard var inputData = inputData as? StickerPickerInputData else { + return + } + inputData.gifs = gifData?.component let emoji = inputData.emoji @@ -1544,7 +1532,9 @@ public class StickerPickerScreen: ViewController { self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + if let controller = self.controller, !controller.isFullscreen { + controller.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { @@ -1555,6 +1545,9 @@ public class StickerPickerScreen: ViewController { } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let controller = self.controller, !controller.isFullscreen else { + return false + } if let (layout, _) = self.currentLayout { if layout.metrics.isTablet { return false @@ -1579,6 +1572,9 @@ public class StickerPickerScreen: ViewController { private var isDismissing = false func animateIn() { + guard let controller = self.controller, !controller.isFullscreen else { + return + } ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) let targetPosition = self.containerView.center @@ -1643,7 +1639,10 @@ public class StickerPickerScreen: ViewController { let clipFrame: CGRect let contentFrame: CGRect - if layout.metrics.widthClass == .compact { + if controller.isFullscreen { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + contentFrame = clipFrame + } else if layout.metrics.widthClass == .compact { self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) if isLandscape { self.containerView.layer.cornerRadius = 0.0 @@ -1753,9 +1752,14 @@ public class StickerPickerScreen: ViewController { } private var defaultTopInset: CGFloat { - guard let (layout, _) = self.currentLayout else{ + guard let (layout, navigationBarHeight) = self.currentLayout else { return 210.0 } + + if let controller = self.controller, controller.isFullscreen { + return navigationBarHeight + } + if case .compact = layout.metrics.widthClass { var factor: CGFloat = 0.2488 if layout.size.width <= 320.0 { @@ -1789,6 +1793,10 @@ public class StickerPickerScreen: ViewController { return } + guard let controller = self.controller, !controller.isFullscreen else { + return + } + let isLandscape = layout.orientation == .landscape let edgeTopInset = isLandscape ? 0.0 : defaultTopInset @@ -1970,8 +1978,10 @@ public class StickerPickerScreen: ViewController { private let context: AccountContext private let theme: PresentationTheme - private let inputData: Signal + private let inputData: Signal fileprivate let defaultToEmoji: Bool + let isFullscreen: Bool + let hasEmoji: Bool let hasGifs: Bool let hasInteractiveStickers: Bool @@ -1988,17 +1998,26 @@ public class StickerPickerScreen: ViewController { public var addReaction: () -> Void = { } public var addCamera: () -> Void = { } - public init(context: AccountContext, inputData: Signal, defaultToEmoji: Bool = false, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) { + public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) { self.context = context - self.theme = defaultDarkColorPresentationTheme + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.theme = forceDark ? defaultDarkColorPresentationTheme : presentationData.theme self.inputData = inputData + self.isFullscreen = expanded self.defaultToEmoji = defaultToEmoji + self.hasEmoji = hasEmoji self.hasGifs = hasGifs self.hasInteractiveStickers = hasInteractiveStickers - super.init(navigationBarPresentationData: nil) + super.init(navigationBarPresentationData: expanded ? NavigationBarPresentationData(presentationData: presentationData) : nil) self.statusBar.statusBarStyle = .Ignore + + if expanded { + //TODO:localize + self.title = "Choose Sticker" + self.navigationPresentation = .modal + } } required init(coder aDecoder: NSCoder) { @@ -2014,14 +2033,18 @@ public class StickerPickerScreen: ViewController { public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { self.view.endEditing(true) - if flag { - self.node.animateOut(completion: { + if self.isFullscreen { + super.dismiss(animated: flag, completion: completion) + } else { + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { super.dismiss(animated: false, completion: {}) completion?() - }) - } else { - super.dismiss(animated: false, completion: {}) - completion?() + } } } diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index d8d5e02e03..e881f3ad6a 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -21,10 +21,12 @@ public final class TabSelectorComponent: Component { public struct CustomLayout: Equatable { public var font: UIFont public var spacing: CGFloat + public var lineSelection: Bool - public init(font: UIFont, spacing: CGFloat) { + public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false) { self.font = font self.spacing = spacing + self.lineSelection = lineSelection } } @@ -107,6 +109,8 @@ public final class TabSelectorComponent: Component { } func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let selectionColorUpdated = component.colors.selection != self.component?.colors.selection + self.component = component self.state = state @@ -115,14 +119,27 @@ public final class TabSelectorComponent: Component { let spacing: CGFloat = component.customLayout?.spacing ?? 2.0 let itemFont: UIFont + var isLineSelection = false if let customLayout = component.customLayout { itemFont = customLayout.font + isLineSelection = customLayout.lineSelection } else { itemFont = Font.semibold(14.0) } - if self.selectionView.image == nil { - self.selectionView.image = generateStretchableFilledCircleImage(diameter: baseHeight, color: component.colors.selection) + if selectionColorUpdated { + if isLineSelection { + self.selectionView.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(component.colors.selection.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: 0.0), size: CGSize(width: 4.0, height: 4.0))) + context.fill(CGRect(x: 2.0, y: 0.0, width: size.width - 4.0, height: 4.0)) + context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0)) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 3.0, left: 3.0, bottom: 0.0, right: 3.0), resizingMode: .stretch) + } else { + self.selectionView.image = generateStretchableFilledCircleImage(diameter: baseHeight, color: component.colors.selection) + } } var contentWidth: CGFloat = 0.0 @@ -146,7 +163,11 @@ public final class TabSelectorComponent: Component { let itemSize = itemView.title.update( transition: .immediate, component: AnyComponent(PlainButtonComponent( - content: AnyComponent(Text(text: item.title, font: itemFont, color: component.colors.foreground)), + content: AnyComponent(Text( + text: item.title, + font: itemFont, + color: item.id == component.selectedId && isLineSelection ? component.colors.selection : component.colors.foreground + )), effectAlignment: .center, minSize: nil, action: { [weak self] in @@ -154,7 +175,8 @@ public final class TabSelectorComponent: Component { return } component.setSelectedId(itemId) - } + }, + animateScale: !isLineSelection )), environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) @@ -178,7 +200,7 @@ public final class TabSelectorComponent: Component { } itemTransition.setPosition(view: itemTitleView, position: itemTitleFrame.origin) itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size)) - itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId ? 1.0 : 0.4) + itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection ? 1.0 : 0.4) } } @@ -195,7 +217,15 @@ public final class TabSelectorComponent: Component { if let selectedBackgroundRect { self.selectionView.alpha = 1.0 - transition.setFrame(view: self.selectionView, frame: selectedBackgroundRect) + + if isLineSelection { + var mappedSelectionFrame = selectedBackgroundRect.insetBy(dx: 12.0, dy: 0.0) + mappedSelectionFrame.origin.y = mappedSelectionFrame.maxY + 6.0 + mappedSelectionFrame.size.height = 3.0 + transition.setFrame(view: self.selectionView, frame: mappedSelectionFrame) + } else { + transition.setFrame(view: self.selectionView, frame: selectedBackgroundRect) + } } else { self.selectionView.alpha = 0.0 } diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/Contents.json new file mode 100644 index 0000000000..6ceca31f32 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ads_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/ads_30.pdf b/submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/ads_30.pdf new file mode 100644 index 0000000000..8f476444c6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Ads.imageset/ads_30.pdf @@ -0,0 +1,99 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.334961 4.678772 cm +0.000000 0.000000 0.000000 scn +18.330002 19.142712 m +18.330002 20.972393 16.201080 21.977081 14.788693 20.813940 c +8.926420 15.986186 l +4.665000 15.986186 l +2.088591 15.986186 0.000000 13.897594 0.000000 11.321186 c +0.000000 8.970550 1.738582 7.025980 4.000000 6.703224 c +4.000000 3.821186 l +4.000000 2.349346 5.193161 1.156185 6.665000 1.156185 c +8.136839 1.156185 9.330000 2.349346 9.330000 3.821186 c +9.330000 6.323827 l +14.788692 1.828432 l +16.201078 0.665291 18.330002 1.669977 18.330002 3.499660 c +18.330002 19.142712 l +h +8.000000 6.656186 m +5.330000 6.656186 l +5.330000 3.821186 l +5.330000 3.083885 5.927700 2.486187 6.665000 2.486187 c +7.402300 2.486187 8.000000 3.083885 8.000000 3.821186 c +8.000000 6.656186 l +h +15.634185 19.787273 m +16.178917 20.235874 17.000000 19.848385 17.000000 19.142712 c +17.000000 3.499660 l +17.000000 2.793987 16.178915 2.406498 15.634184 2.855101 c +9.623762 7.804859 l +9.481418 7.922084 9.302749 7.986186 9.118342 7.986186 c +4.665000 7.986186 l +2.823130 7.986186 1.330000 9.479317 1.330000 11.321186 c +1.330000 13.163055 2.823130 14.656186 4.665000 14.656186 c +9.118342 14.656186 l +9.302745 14.656186 9.481414 14.720285 9.623762 14.837513 c +15.634185 19.787273 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1297 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001387 00000 n +0000001410 00000 n +0000001583 00000 n +0000001657 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1716 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/Contents.json new file mode 100644 index 0000000000..cd3e839d6c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "monetization_90.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/monetization_90.pdf b/submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/monetization_90.pdf new file mode 100644 index 0000000000..941efbfa3e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Monetization.imageset/monetization_90.pdf @@ -0,0 +1,188 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 23.000000 17.500000 cm +0.000000 0.000000 0.000000 scn +37.500000 41.500000 m +37.500000 33.215729 30.784271 26.500000 22.500000 26.500000 c +14.215729 26.500000 7.500000 33.215729 7.500000 41.500000 c +7.500000 49.784271 14.215729 56.500000 22.500000 56.500000 c +30.784271 56.500000 37.500000 49.784271 37.500000 41.500000 c +h +16.055553 45.507233 m +15.908934 45.773811 16.101799 46.099998 16.406040 46.099998 c +21.299994 46.099998 l +21.299994 35.971886 l +16.055553 45.507233 l +h +23.699993 35.971840 m +28.944456 45.507229 l +29.091078 45.773815 28.898209 46.099998 28.593971 46.099998 c +23.699993 46.099998 l +23.699993 35.971840 l +h +16.406040 48.500000 m +14.276352 48.500000 12.926297 46.216698 13.952635 44.350628 c +20.046600 33.270691 l +21.110390 31.336523 23.889614 31.336515 24.953409 33.270691 c +31.047375 44.350624 l +32.073708 46.216690 30.723663 48.500000 28.593971 48.500000 c +16.406040 48.500000 l +h +4.837455 37.573509 m +5.430337 36.994904 5.441912 36.045227 4.863308 35.452347 c +3.594926 34.152664 3.000000 32.810127 3.000000 31.500000 c +3.000000 29.437628 4.521029 27.238575 7.690794 25.410986 c +7.902112 25.289146 8.120757 25.168959 8.346768 25.050571 c +11.891893 23.193604 16.893353 22.000000 22.500000 22.000000 c +28.106646 22.000000 33.108109 23.193604 36.653233 25.050571 c +40.269405 26.944759 42.000000 29.300137 42.000000 31.500000 c +42.000000 32.810127 41.405075 34.152664 40.136692 35.452347 c +39.558090 36.045227 39.569664 36.994904 40.162544 37.573509 c +40.755428 38.152111 41.705105 38.140537 42.283710 37.547653 c +43.943344 35.847065 45.000000 33.790764 45.000000 31.500000 c +45.000000 22.500000 l +45.000000 21.500000 l +45.000000 12.500000 l +45.000000 8.624733 42.029583 5.480110 38.045254 3.393078 c +33.989872 1.268829 28.491333 0.000000 22.500000 0.000000 c +16.508667 0.000000 11.010127 1.268829 6.954747 3.393078 c +2.970417 5.480110 0.000000 8.624733 0.000000 12.500000 c +0.000000 21.500000 l +0.000000 22.500000 l +0.000000 31.500000 l +0.000000 33.790764 1.056656 35.847065 2.716292 37.547653 c +3.294897 38.140537 4.244574 38.152111 4.837455 37.573509 c +h +38.045254 22.393078 m +39.540852 23.176487 40.893585 24.108913 41.999977 25.173065 c +42.000000 25.173086 l +42.000000 22.500000 l +42.000000 21.500000 l +42.000000 19.371666 40.380116 17.097767 37.000000 15.236786 c +37.000000 21.879204 l +37.358109 22.044373 37.706707 22.215744 38.045254 22.393078 c +h +34.000000 20.713173 m +32.442459 20.208160 30.765238 19.804638 29.000000 19.516380 c +29.000000 12.559616 l +30.805933 12.881241 32.486198 13.331841 34.000000 13.885990 c +34.000000 20.713173 l +h +22.500000 19.000000 m +23.688532 19.000000 24.857672 19.049931 26.000000 19.146568 c +26.000000 12.158058 l +24.866440 12.054405 23.697014 12.000000 22.500000 12.000000 c +21.651834 12.000000 20.817518 12.027317 20.000000 12.080112 c +20.000000 19.074383 l +20.821783 19.025181 21.656002 19.000000 22.500000 19.000000 c +h +22.500000 9.000000 m +23.688532 9.000000 24.857672 9.049931 26.000000 9.146568 c +26.000000 3.158058 l +24.866440 3.054405 23.697014 3.000000 22.500000 3.000000 c +21.651834 3.000000 20.817518 3.027317 20.000000 3.080112 c +20.000000 9.074383 l +20.821783 9.025181 21.656002 9.000000 22.500000 9.000000 c +h +34.000000 4.885990 m +32.486198 4.331841 30.805933 3.881241 29.000000 3.559616 c +29.000000 9.516380 l +30.765238 9.804638 32.442459 10.208160 34.000000 10.713173 c +34.000000 4.885990 l +h +17.000000 12.396725 m +17.000000 19.366760 l +14.866652 19.657177 12.848998 20.113659 11.000000 20.713173 c +11.000000 13.885990 l +12.789782 13.230812 14.812259 12.720390 17.000000 12.396725 c +h +17.000000 9.366760 m +17.000000 3.396725 l +14.812259 3.720390 12.789782 4.230812 11.000000 4.885990 c +11.000000 10.713173 l +12.848998 10.113659 14.866652 9.657177 17.000000 9.366760 c +h +6.954747 22.393078 m +7.293292 22.215744 7.641894 22.044373 8.000000 21.879204 c +8.000000 15.236786 l +4.619881 17.097767 3.000000 19.371666 3.000000 21.500000 c +3.000000 22.500000 l +3.000000 25.173086 l +3.003295 25.169918 l +4.109081 24.107075 5.460623 23.175716 6.954747 22.393078 c +h +6.954747 12.393078 m +7.293292 12.215744 7.641894 12.044373 8.000000 11.879204 c +8.000000 6.236786 l +4.619881 8.097767 3.000000 10.371666 3.000000 12.500000 c +3.000000 15.173088 l +4.106395 14.108929 5.459139 13.176491 6.954747 12.393078 c +h +37.000000 6.236786 m +40.380116 8.097767 42.000000 10.371666 42.000000 12.500000 c +42.000000 15.173088 l +40.893604 14.108929 39.540863 13.176491 38.045254 12.393078 c +37.706707 12.215744 37.358109 12.044373 37.000000 11.879204 c +37.000000 6.236786 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 4594 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 90.000000 90.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004684 00000 n +0000004707 00000 n +0000004880 00000 n +0000004954 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5013 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/Contents.json new file mode 100644 index 0000000000..2c8cf2f934 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "split_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/split_30.pdf b/submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/split_30.pdf new file mode 100644 index 0000000000..6b78dc2565 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Split.imageset/split_30.pdf @@ -0,0 +1,416 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +0.923880 -0.382683 0.382683 0.923880 3.005669 4.533266 cm +0.000000 0.000000 0.000000 scn +16.543879 16.094051 m +16.482773 16.456203 16.139654 16.700247 15.777505 16.639141 c +15.415355 16.578033 15.171310 16.234917 15.232417 15.872766 c +16.543879 16.094051 l +h +13.983027 18.887110 m +14.195807 18.587759 14.610972 18.517580 14.910323 18.730362 c +15.209674 18.943142 15.279853 19.358307 15.067072 19.657660 c +13.983027 18.887110 l +h +13.014956 21.709774 m +12.715606 21.922554 12.300441 21.852375 12.087660 21.553024 c +11.874879 21.253674 11.945057 20.838509 12.244409 20.625727 c +13.014956 21.709774 l +h +9.230065 21.875116 m +9.592216 21.814011 9.935332 22.058054 9.996439 22.420204 c +10.057546 22.782354 9.813501 23.125473 9.451351 23.186579 c +9.230065 21.875116 l +h +6.548648 23.186579 m +6.186498 23.125473 5.942453 22.782354 6.003560 22.420204 c +6.064667 22.058054 6.407784 21.814011 6.769934 21.875116 c +6.548648 23.186579 l +h +3.755590 20.625727 m +4.054941 20.838507 4.125120 21.253672 3.912338 21.553024 c +3.699557 21.852375 3.284392 21.922554 2.985041 21.709772 c +3.755590 20.625727 l +h +0.932927 19.657658 m +0.720146 19.358307 0.790325 18.943140 1.089676 18.730360 c +1.389027 18.517578 1.804192 18.587757 2.016973 18.887108 c +0.932927 19.657658 l +h +0.767583 15.872766 m +0.828690 16.234917 0.584645 16.578033 0.222495 16.639139 c +-0.139655 16.700245 -0.482772 16.456202 -0.543879 16.094051 c +0.767583 15.872766 l +h +-0.543879 13.191348 m +-0.482772 12.829198 -0.139655 12.585154 0.222495 12.646260 c +0.584646 12.707367 0.828690 13.050484 0.767583 13.412634 c +-0.543879 13.191348 l +h +2.016974 10.398291 m +1.804193 10.697641 1.389028 10.767819 1.089677 10.555038 c +0.790326 10.342257 0.720147 9.927093 0.932928 9.627742 c +2.016974 10.398291 l +h +2.985043 7.575627 m +3.284394 7.362846 3.699559 7.433024 3.912340 7.732376 c +4.125122 8.031727 4.054943 8.446892 3.755592 8.659673 c +2.985043 7.575627 l +h +6.769935 7.410283 m +6.407784 7.471390 6.064667 7.227345 6.003561 6.865195 c +5.942454 6.503046 6.186498 6.159927 6.548648 6.098822 c +6.769935 7.410283 l +h +9.451352 6.098822 m +9.813502 6.159927 10.057547 6.503046 9.996440 6.865195 c +9.935333 7.227345 9.592216 7.471390 9.230066 7.410283 c +9.451352 6.098822 l +h +12.244410 8.659674 m +11.945059 8.446893 11.874881 8.031728 12.087662 7.732377 c +12.300443 7.433026 12.715608 7.362847 13.014958 7.575628 c +12.244410 8.659674 l +h +15.067073 9.627744 m +15.279854 9.927094 15.209676 10.342259 14.910324 10.555040 c +14.610973 10.767821 14.195808 10.697643 13.983027 10.398292 c +15.067073 9.627744 l +h +15.232417 13.412635 m +15.171310 13.050485 15.415355 12.707368 15.777505 12.646261 c +16.139654 12.585155 16.482773 12.829199 16.543879 13.191349 c +15.232417 13.412635 l +h +16.665001 14.642700 m +16.665001 15.136786 16.623579 15.621709 16.543879 16.094051 c +15.232417 15.872766 l +15.299830 15.473239 15.335000 15.062332 15.335000 14.642700 c +16.665001 14.642700 l +h +15.067072 19.657660 m +14.502963 20.451275 13.808573 21.145664 13.014956 21.709774 c +12.244409 20.625727 l +12.916721 20.147842 13.505140 19.559423 13.983027 18.887110 c +15.067072 19.657660 l +h +9.451351 23.186579 m +8.979008 23.266279 8.494085 23.307701 8.000000 23.307701 c +8.000000 21.977699 l +8.419632 21.977699 8.830539 21.942530 9.230065 21.875116 c +9.451351 23.186579 l +h +8.000000 23.307701 m +7.505914 23.307701 7.020991 23.266279 6.548648 23.186579 c +6.769934 21.875116 l +7.169461 21.942530 7.580368 21.977699 8.000000 21.977699 c +8.000000 23.307701 l +h +2.985041 21.709772 m +2.191425 21.145662 1.497036 20.451273 0.932927 19.657658 c +2.016973 18.887108 l +2.494858 19.559422 3.083277 20.147840 3.755590 20.625727 c +2.985041 21.709772 l +h +-0.543879 16.094051 m +-0.623579 15.621708 -0.665000 15.136786 -0.665000 14.642700 c +0.665000 14.642700 l +0.665000 15.062332 0.700170 15.473238 0.767583 15.872766 c +-0.543879 16.094051 l +h +-0.665000 14.642700 m +-0.665000 14.148614 -0.623578 13.663692 -0.543879 13.191348 c +0.767583 13.412634 l +0.700170 13.812161 0.665000 14.223067 0.665000 14.642700 c +-0.665000 14.642700 l +h +0.932928 9.627742 m +1.497037 8.834126 2.191427 8.139736 2.985043 7.575627 c +3.755592 8.659673 l +3.083278 9.137559 2.494860 9.725977 2.016974 10.398291 c +0.932928 9.627742 l +h +6.548648 6.098822 m +7.020992 6.019121 7.505915 5.977699 8.000000 5.977699 c +8.000000 7.307700 l +7.580368 7.307700 7.169462 7.342870 6.769935 7.410283 c +6.548648 6.098822 l +h +8.000000 5.977699 m +8.494086 5.977699 8.979009 6.019121 9.451352 6.098822 c +9.230066 7.410283 l +8.830539 7.342870 8.419633 7.307700 8.000000 7.307700 c +8.000000 5.977699 l +h +13.014958 7.575628 m +13.808575 8.139737 14.502964 8.834127 15.067073 9.627744 c +13.983027 10.398292 l +13.505141 9.725979 12.916723 9.137560 12.244410 8.659674 c +13.014958 7.575628 l +h +16.543879 13.191349 m +16.623579 13.663692 16.665001 14.148615 16.665001 14.642700 c +15.335000 14.642700 l +15.335000 14.223068 15.299830 13.812161 15.232417 13.412635 c +16.543879 13.191349 l +h +f +n +Q + +endstream +endobj + +2 0 obj + 4945 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 16.000000 5.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 20.000000 m +10.000000 20.000000 l +10.000000 0.000000 l +0.000000 0.000000 l +0.000000 20.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 233 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 5.000122 3.579651 cm +0.000000 0.000000 0.000000 scn +9.001400 1.469524 m +8.935791 0.807770 l +9.001400 1.469524 l +h +9.067010 2.131281 m +4.349816 2.598965 0.665000 6.579719 0.665000 11.420288 c +-0.665000 11.420288 l +-0.665000 5.889203 3.544995 1.342237 8.935791 0.807770 c +9.067010 2.131281 l +h +0.665000 11.420288 m +0.665000 16.260859 4.349816 20.241613 9.067010 20.709297 c +8.935791 22.032806 l +3.544996 21.498341 -0.665000 16.951374 -0.665000 11.420288 c +0.665000 11.420288 l +h +9.335000 20.420288 m +9.335000 2.420288 l +10.665000 2.420288 l +10.665000 20.420288 l +9.335000 20.420288 l +h +9.067010 20.709297 m +9.134297 20.715967 9.193896 20.694256 9.243861 20.645805 c +9.296615 20.594650 9.335000 20.515173 9.335000 20.420288 c +10.665000 20.420288 l +10.665000 21.320156 9.920688 22.130455 8.935791 22.032806 c +9.067010 20.709297 l +h +8.935791 0.807770 m +9.920687 0.710121 10.665000 1.520422 10.665000 2.420288 c +9.335000 2.420288 l +9.335000 2.325403 9.296615 2.245926 9.243861 2.194773 c +9.193896 2.146320 9.134296 2.124609 9.067010 2.131281 c +8.935791 0.807770 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.250000 13.790161 cm +0.000000 0.000000 0.000000 scn +9.720226 10.239613 m +9.979925 10.499311 9.979925 10.920366 9.720226 11.180065 c +9.460527 11.439764 9.039473 11.439764 8.779774 11.180065 c +9.720226 10.239613 l +h +-0.470226 1.930065 m +-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c +-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c +-0.470226 1.930065 l +h +8.779774 11.180065 m +-0.470226 1.930065 l +0.470226 0.989613 l +9.720226 10.239613 l +8.779774 11.180065 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.000000 9.540161 cm +0.000000 0.000000 0.000000 scn +9.470226 9.989613 m +9.729925 10.249311 9.729925 10.670366 9.470226 10.930065 c +9.210527 11.189764 8.789473 11.189764 8.529774 10.930065 c +9.470226 9.989613 l +h +-0.470226 1.930065 m +-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c +-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c +-0.470226 1.930065 l +h +8.529774 10.930065 m +-0.470226 1.930065 l +0.470226 0.989613 l +9.470226 9.989613 l +8.529774 10.930065 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 8.500000 7.040161 cm +0.000000 0.000000 0.000000 scn +6.970226 7.489613 m +7.229925 7.749311 7.229925 8.170366 6.970226 8.430065 c +6.710527 8.689764 6.289473 8.689764 6.029774 8.430065 c +6.970226 7.489613 l +h +-0.470226 1.930065 m +-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c +-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c +-0.470226 1.930065 l +h +6.029774 8.430065 m +-0.470226 1.930065 l +0.470226 0.989613 l +6.970226 7.489613 l +6.029774 8.430065 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 11.000000 4.540161 cm +0.000000 0.000000 0.000000 scn +4.470226 4.989613 m +4.729925 5.249311 4.729925 5.670366 4.470226 5.930065 c +4.210527 6.189764 3.789473 6.189764 3.529774 5.930065 c +4.470226 4.989613 l +h +-0.470226 1.930065 m +-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c +-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c +-0.470226 1.930065 l +h +3.529774 5.930065 m +-0.470226 1.930065 l +0.470226 0.989613 l +4.470226 4.989613 l +3.529774 5.930065 l +h +f +n +Q + +endstream +endobj + +7 0 obj + 3228 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000005203 00000 n +0000005226 00000 n +0000005707 00000 n +0000005729 00000 n +0000006027 00000 n +0000009311 00000 n +0000009334 00000 n +0000009507 00000 n +0000009581 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +9641 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/Contents.json new file mode 100644 index 0000000000..3baeb9f565 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ton_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/ton_30.pdf b/submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/ton_30.pdf new file mode 100644 index 0000000000..da81c52597 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chart/Withdrawal.imageset/ton_30.pdf @@ -0,0 +1,117 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 8.343262 7.454346 cm +0.000000 0.000000 0.000000 scn +1.432826 10.484781 m +1.133278 11.041084 1.536197 11.715655 2.168021 11.715654 c +5.991703 11.715646 l +5.991703 2.038759 l +5.966153 2.072763 5.942646 2.109401 5.921499 2.148673 c +1.432826 10.484781 l +h +7.321702 2.038787 m +7.347245 2.072783 7.370744 2.109412 7.391885 2.148674 c +11.880558 10.484761 l +12.180106 11.041063 11.777191 11.715633 11.145367 11.715634 c +7.321702 11.715643 l +7.321702 2.038787 l +h +2.168024 13.045654 m +0.529820 13.045658 -0.514872 11.296619 0.261800 9.854228 c +4.750473 1.518121 l +5.567911 0.000022 7.745473 0.000024 8.562911 1.518120 c +13.051584 9.854208 l +13.828257 11.296596 12.783570 13.045631 11.145370 13.045634 c +2.168024 13.045654 l +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.000000 3.670044 cm +0.000000 0.000000 0.000000 scn +19.334999 11.329956 m +19.334999 6.174378 15.155578 1.994957 10.000000 1.994957 c +10.000000 0.664955 l +15.890117 0.664955 20.665001 5.439839 20.665001 11.329956 c +19.334999 11.329956 l +h +10.000000 1.994957 m +4.844422 1.994957 0.665000 6.174378 0.665000 11.329956 c +-0.665000 11.329956 l +-0.665000 5.439839 4.109883 0.664955 10.000000 0.664955 c +10.000000 1.994957 l +h +0.665000 11.329956 m +0.665000 16.485535 4.844422 20.664955 10.000000 20.664955 c +10.000000 21.994957 l +4.109883 21.994957 -0.665000 17.220074 -0.665000 11.329956 c +0.665000 11.329956 l +h +10.000000 20.664955 m +15.155578 20.664955 19.334999 16.485535 19.334999 11.329956 c +20.665001 11.329956 l +20.665001 17.220074 15.890117 21.994957 10.000000 21.994957 c +10.000000 20.664955 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1634 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001724 00000 n +0000001747 00000 n +0000001920 00000 n +0000001994 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2053 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/Contents.json new file mode 100644 index 0000000000..b661ea2e06 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "noads_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/noads_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/noads_30.pdf new file mode 100644 index 0000000000..655de5e030 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NoAds.imageset/noads_30.pdf @@ -0,0 +1,111 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.334961 3.604797 cm +0.000000 0.000000 0.000000 scn +19.329964 20.216686 m +19.329964 22.046368 17.201042 23.051056 15.788654 21.887915 c +9.926381 17.060162 l +5.940495 17.060162 l +1.135226 21.865429 l +0.875527 22.125128 0.454473 22.125128 0.194774 21.865429 c +-0.064925 21.605730 -0.064925 21.184675 0.194774 20.924976 c +20.194775 0.924976 l +20.454472 0.665277 20.875526 0.665277 21.135227 0.924976 c +21.394924 1.184675 21.394924 1.605730 21.135227 1.865429 c +19.194319 3.806337 l +19.281490 4.040243 19.329964 4.297546 19.329964 4.573635 c +19.329964 20.216686 l +h +17.999962 5.000694 m +17.999962 20.216686 l +17.999962 20.922359 17.178879 21.309849 16.634146 20.861248 c +10.623723 15.911488 l +10.481375 15.794260 10.302706 15.730161 10.118303 15.730161 c +7.270494 15.730161 l +17.999962 5.000694 l +h +0.999961 12.395161 m +0.999961 14.083057 1.896390 15.561582 3.239132 16.380619 c +4.218666 15.401085 l +3.101114 14.862392 2.329961 13.718833 2.329961 12.395161 c +2.329961 10.553291 3.823091 9.060161 5.664961 9.060161 c +10.118303 9.060161 l +10.302710 9.060161 10.481379 8.996058 10.623723 8.878834 c +11.287819 8.331931 l +17.214378 2.405373 l +16.726585 2.391582 16.223637 2.544184 15.788653 2.902407 c +10.329961 7.397801 l +10.329961 4.895161 l +10.329961 3.423321 9.136800 2.230160 7.664961 2.230160 c +6.193122 2.230160 4.999961 3.423321 4.999961 4.895161 c +4.999961 7.777199 l +2.738543 8.099955 0.999961 10.044524 0.999961 12.395161 c +h +8.999961 7.730161 m +6.329961 7.730161 l +6.329961 4.895161 l +6.329961 4.157860 6.927661 3.560162 7.664961 3.560162 c +8.402261 3.560162 8.999961 4.157860 8.999961 4.895161 c +8.999961 7.730161 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1696 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001786 00000 n +0000001809 00000 n +0000001982 00000 n +0000002056 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2115 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/Contents.json new file mode 100644 index 0000000000..5ad5d8a9d6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "intro_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/intro_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/intro_30.pdf new file mode 100644 index 0000000000..3dbdb495b7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Business/Intro.imageset/intro_30.pdf @@ -0,0 +1,226 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 3.620605 cm +0.000000 0.000000 0.000000 scn +7.545889 3.473349 m +7.397222 2.825180 l +7.397222 2.825180 l +7.545889 3.473349 l +h +5.266314 1.841906 m +4.941034 2.421923 l +4.941034 2.421923 l +5.266314 1.841906 l +h +2.471971 1.400564 m +2.214746 0.787325 l +2.471971 1.400564 l +h +3.613619 5.411451 m +3.220785 4.874882 l +3.613619 5.411451 l +h +10.000000 20.714394 m +15.216641 20.714394 19.334999 16.883762 19.334999 12.288486 c +20.665001 12.288486 l +20.665001 17.734751 15.829054 22.044395 10.000000 22.044395 c +10.000000 20.714394 l +h +19.334999 12.288486 m +19.334999 7.693209 15.216641 3.862577 10.000000 3.862577 c +10.000000 2.532578 l +15.829054 2.532578 20.665001 6.842221 20.665001 12.288486 c +19.334999 12.288486 l +h +10.000000 3.862577 m +9.203434 3.862577 8.431143 3.952570 7.694557 4.121517 c +7.397222 2.825180 l +8.231061 2.633926 9.103088 2.532578 10.000000 2.532578 c +10.000000 3.862577 l +h +7.694557 4.121517 m +7.296880 4.212730 6.988395 3.993872 6.866288 3.902136 c +6.721982 3.793720 6.552248 3.631365 6.405339 3.496567 c +6.085002 3.202635 5.640136 2.813988 4.941034 2.421923 c +5.591593 1.261890 l +6.417546 1.725096 6.948387 2.189802 7.304533 2.516592 c +7.495866 2.692154 7.589569 2.782000 7.665158 2.838789 c +7.762947 2.912256 7.630817 2.771601 7.397222 2.825180 c +7.694557 4.121517 l +h +4.941034 2.421923 m +4.518973 2.185225 3.984706 2.063803 3.502597 2.019852 c +3.266236 1.998304 3.056921 1.996721 2.899934 2.005190 c +2.821182 2.009439 2.761494 2.015905 2.721901 2.022039 c +2.669684 2.030128 2.683973 2.032770 2.729195 2.013802 c +2.214746 0.787325 l +2.322490 0.742132 2.437665 0.720207 2.518294 0.707716 c +2.611548 0.693270 2.716669 0.683144 2.828285 0.677122 c +3.052035 0.665051 3.325697 0.668209 3.623343 0.695343 c +4.209247 0.748756 4.947107 0.900455 5.591593 1.261890 c +4.941034 2.421923 l +h +2.729195 2.013802 m +2.749930 2.005104 2.882758 1.946646 2.968039 1.777298 c +3.061458 1.591789 3.022986 1.426357 3.001172 1.364456 c +2.982594 1.311743 2.964163 1.295038 2.989081 1.328539 c +3.009508 1.356001 3.043106 1.396257 3.097501 1.456873 c +3.303000 1.685873 3.720195 2.103415 4.080953 2.656437 c +2.967016 3.383102 l +2.678986 2.941568 2.367350 2.634583 2.107628 2.345158 c +2.045717 2.276167 1.979485 2.199697 1.921927 2.122316 c +1.868860 2.050974 1.794289 1.941309 1.746787 1.806519 c +1.696047 1.662540 1.655866 1.425915 1.780159 1.179100 c +1.896313 0.948444 2.092599 0.838560 2.214746 0.787325 c +2.729195 2.013802 l +h +4.080953 2.656437 m +4.494962 3.291088 4.613441 3.980347 4.585420 4.541803 c +4.571406 4.822596 4.520174 5.086733 4.439862 5.311483 c +4.369761 5.507658 4.241846 5.775683 4.006453 5.948020 c +3.220785 4.874882 l +3.130925 4.940670 3.145670 4.980782 3.187423 4.863937 c +3.218965 4.775669 3.248780 4.641682 3.257073 4.475510 c +3.273666 4.143038 3.201793 3.743004 2.967016 3.383102 c +4.080953 2.656437 l +h +4.006453 5.948020 m +1.882439 7.503058 0.665000 9.659539 0.665000 12.288486 c +-0.665000 12.288486 l +-0.665000 9.190428 0.792707 6.652533 3.220785 4.874882 c +4.006453 5.948020 l +h +0.665000 12.288486 m +0.665000 16.883762 4.783359 20.714394 10.000000 20.714394 c +10.000000 22.044395 l +4.170946 22.044395 -0.665000 17.734751 -0.665000 12.288486 c +0.665000 12.288486 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 9.669922 cm +0.000000 0.000000 0.000000 scn +0.000000 1.995078 m +-0.367269 1.995078 -0.665000 1.697348 -0.665000 1.330078 c +-0.665000 0.962809 -0.367269 0.665078 0.000000 0.665078 c +0.000000 1.995078 l +h +3.000000 0.665078 m +3.367269 0.665078 3.665000 0.962809 3.665000 1.330078 c +3.665000 1.697348 3.367269 1.995078 3.000000 1.995078 c +3.000000 0.665078 l +h +0.000000 0.665078 m +3.000000 0.665078 l +3.000000 1.995078 l +0.000000 1.995078 l +0.000000 0.665078 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 13.500000 9.669922 cm +0.000000 0.000000 0.000000 scn +1.335000 1.330078 m +1.335000 0.962809 1.632731 0.665078 2.000000 0.665078 c +2.367269 0.665078 2.665000 0.962809 2.665000 1.330078 c +1.335000 1.330078 l +h +2.000000 7.830078 m +2.665000 7.830078 l +2.665000 8.197348 2.367269 8.495078 2.000000 8.495078 c +2.000000 7.830078 l +h +0.000000 8.495078 m +-0.367269 8.495078 -0.665000 8.197348 -0.665000 7.830078 c +-0.665000 7.462809 -0.367269 7.165078 0.000000 7.165078 c +0.000000 8.495078 l +h +2.665000 1.330078 m +2.665000 7.830078 l +1.335000 7.830078 l +1.335000 1.330078 l +2.665000 1.330078 l +h +2.000000 8.495078 m +0.000000 8.495078 l +0.000000 7.165078 l +2.000000 7.165078 l +2.000000 8.495078 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 13.750000 19.750000 cm +0.000000 0.000000 0.000000 scn +1.250000 0.000000 m +1.940356 0.000000 2.500000 0.559644 2.500000 1.250000 c +2.500000 1.940356 1.940356 2.500000 1.250000 2.500000 c +0.559644 2.500000 0.000000 1.940356 0.000000 1.250000 c +0.000000 0.559644 0.559644 0.000000 1.250000 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 4852 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004942 00000 n +0000004965 00000 n +0000005138 00000 n +0000005212 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5271 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/Contents.json new file mode 100644 index 0000000000..ee672ca436 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "introsetting_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/introsetting_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/introsetting_30.pdf new file mode 100644 index 0000000000..4d8c9f9751 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BusinessPerk/Intro.imageset/introsetting_30.pdf @@ -0,0 +1,182 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 4.484375 cm +0.000000 0.000000 0.000000 scn +10.000000 20.135742 m +15.522848 20.135742 20.000000 16.065603 20.000000 11.044833 c +20.000000 6.024063 15.522848 1.953924 10.000000 1.953924 c +9.153261 1.953924 8.331102 2.049595 7.545889 2.229696 c +7.397357 2.263765 7.228708 2.107983 6.947171 1.847927 c +6.606689 1.533424 6.101101 1.066412 5.266314 0.598253 c +4.199766 0.000120 2.722059 0.052011 2.471971 0.156912 c +2.232201 0.257484 2.416753 0.457399 2.741760 0.809465 c +2.966608 1.053034 3.258681 1.369423 3.523984 1.776117 c +4.172771 2.770672 3.904685 3.954702 3.613619 4.167799 c +1.337573 5.834144 0.000000 8.181331 0.000000 11.044833 c +0.000000 16.065603 4.477152 20.135742 10.000000 20.135742 c +h +9.999961 15.265625 m +10.690317 15.265625 11.249961 15.825269 11.249961 16.515625 c +11.249961 17.205980 10.690317 17.765625 9.999961 17.765625 c +9.309605 17.765625 8.749961 17.205980 8.749961 16.515625 c +8.749961 15.825269 9.309605 15.265625 9.999961 15.265625 c +h +8.499961 13.680625 m +8.132691 13.680625 7.834961 13.382895 7.834961 13.015625 c +7.834961 12.648355 8.132691 12.350625 8.499961 12.350625 c +9.834961 12.350625 l +9.834961 7.180625 l +8.999961 7.180625 l +8.632691 7.180625 8.334961 6.882895 8.334961 6.515625 c +8.334961 6.148355 8.632691 5.850625 8.999961 5.850625 c +10.499961 5.850625 l +11.999961 5.850625 l +12.367230 5.850625 12.664961 6.148355 12.664961 6.515625 c +12.664961 6.882895 12.367230 7.180625 11.999961 7.180625 c +11.164961 7.180625 l +11.164961 13.015625 l +11.164961 13.382895 10.867230 13.680625 10.499961 13.680625 c +8.499961 13.680625 l +h +f* +n +Q + +endstream +endobj + +2 0 obj + 1644 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 944 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001902 00000 n +0000001925 00000 n +0000003117 00000 n +0000003139 00000 n +0000003437 00000 n +0000003539 00000 n +0000003560 00000 n +0000003733 00000 n +0000003807 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +3867 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c366748421..a10b98639d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -14818,25 +14818,39 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in return currentState.withUpdatedComposeInputState(textInputState) }) - |> deliverOnMainQueue).startStandalone(completed: { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - if let navigationController = strongSelf.effectiveNavigationController { - let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId)) + if let navigationController = strongSelf.effectiveNavigationController { + let chatController: Signal + if let threadId { + chatController = chatControllerForForumThreadImpl(context: strongSelf.context, peerId: peerId, threadId: threadId) + } else { + chatController = .single(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId))) + } + + let _ = (chatController + |> deliverOnMainQueue).start(next: { [weak self, weak navigationController] chatController in + guard let strongSelf = self, let navigationController else { + return + } var viewControllers = navigationController.viewControllers + let lastController = viewControllers.last as! ViewController + viewControllers.remove(at: viewControllers.count - 2) viewControllers.insert(chatController, at: viewControllers.count - 1) + lastController.navigationPresentation = .modal navigationController.setViewControllers(viewControllers, animated: false) strongSelf.controllerNavigationDisposable.set((chatController.ready.get() |> filter { $0 } |> take(1) - |> deliverOnMainQueue).startStrict(next: { _ in - if let strongController = controller { - strongController.dismiss() - } + |> deliverOnMainQueue).startStrict(next: { [weak lastController] _ in + lastController?.dismiss() })) - } + }) } }) } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 1b5ec39d4a..fd8e11c61a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -1725,7 +1725,7 @@ extension ChatControllerImpl { } let editorController = MediaEditorScreen( context: self.context, - mode: .stickerEditor, + mode: .stickerEditor(mode: .generic), subject: .single(.asset(asset)), transitionIn: .gallery( MediaEditorScreen.TransitionIn.GalleryTransitionIn( @@ -1756,8 +1756,6 @@ extension ChatControllerImpl { } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void ) self.push(editorController) - - }, dismissed: {} ) @@ -1768,68 +1766,4 @@ extension ChatControllerImpl { mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.push(mainController) } - -// func openStickerEditor() { -// let mainController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { -// return nil -// }) -//// controller.forceSourceRect = true -//// controller.getSourceRect = getSourceRect -// mainController.requestController = { [weak self, weak mainController] _, present in -// guard let self else { -// return -// } -// let mediaPickerController = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, subject: .assets(nil, .createSticker)) -// mediaPickerController.customSelection = { [weak self, weak mainController] controller, result in -// guard let self else { -// return -// } -// if let result = result as? PHAsset { -// controller.updateHiddenMediaId(result.localIdentifier) -// if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { -// let editorController = MediaEditorScreen( -// context: self.context, -// mode: .stickerEditor, -// subject: .single(.asset(result)), -// transitionIn: .gallery( -// MediaEditorScreen.TransitionIn.GalleryTransitionIn( -// sourceView: transitionView, -// sourceRect: transitionView.bounds, -// sourceImage: controller.transitionImage(for: result.localIdentifier) -// ) -// ), -// transitionOut: { finished, isNew in -// if !finished { -// return MediaEditorScreen.TransitionOut( -// destinationView: transitionView, -// destinationRect: transitionView.bounds, -// destinationCornerRadius: 0.0 -// ) -// } -// return nil -// }, completion: { [weak self, weak mainController] result, commit in -// mainController?.dismiss() -// self?.chatDisplayNode.dismissInput() -// -// Queue.mainQueue().after(0.1) { -// commit({}) -// if case let .sticker(file) = result.media { -// self?.enqueueStickerFile(file) -// } -// } -// } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void -// ) -// editorController.dismissed = { [weak controller] in -// controller?.updateHiddenMediaId(nil) -// } -// self.push(editorController) -// } -// } -// } -// present(mediaPickerController, mediaPickerController.mediaPickerContext) -// } -// mainController.navigationPresentation = .flatModal -// mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) -// self.push(mainController) -// } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 99a8efb455..48b0935967 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -56,6 +56,9 @@ import BusinessLocationSetupScreen import BusinessHoursSetupScreen import AutomaticBusinessMessageSetupScreen import CollectibleItemInfoScreen +import StickerPickerScreen +import MediaEditor +import MediaEditorScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -2264,7 +2267,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return controller } - public func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController { + public func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) var pushImpl: ((ViewController) -> Void)? @@ -2272,7 +2275,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { let controller = PremiumBoostLevelsScreen( context: context, peerId: peerId, - mode: .owner(subject: .stories), + mode: .owner(subject: subject), status: boostStatus, myBoostStatus: myBoostStatus, openStats: openStats, @@ -2306,9 +2309,47 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, isEditing: isEditing, parentNavigationController: parentNavigationController, sendSticker: sendSticker) } -// func makeStickerEditorScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, initialSticker: TelegramMediaFile?, targetStickerPack: StickerPackReference?) -> ViewController { -// -// } + public func makeStickerEditorScreen(context: AccountContext, source: Any, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController { + let subject: MediaEditorScreen.Subject + let mode: MediaEditorScreen.Mode.StickerEditorMode + if let file = source as? TelegramMediaFile { + subject = .sticker(file) + mode = .editing + } else if let asset = source as? PHAsset { + subject = .asset(asset) + mode = .addingToPack + } else { + fatalError() + } + let controller = MediaEditorScreen( + context: context, + mode: .stickerEditor(mode: mode), + subject: .single(subject), + transitionIn: transitionArguments.flatMap { .gallery( + MediaEditorScreen.TransitionIn.GalleryTransitionIn( + sourceView: $0.0, + sourceRect: $0.1, + sourceImage: $0.2 + ) + ) }, + transitionOut: { finished, isNew in + if !finished, let transitionArguments { + return MediaEditorScreen.TransitionOut( + destinationView: transitionArguments.0, + destinationRect: transitionArguments.0.bounds, + destinationCornerRadius: 0.0 + ) + } + return nil + }, completion: { result, commit in + commit({}) + if case let .sticker(file) = result.media { + completion(file) + } + } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void + ) + return controller + } public func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController { return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion) @@ -2321,6 +2362,17 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController { return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed) } + + public func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController { + let controller = StickerPickerScreen(context: context, inputData: inputData.get(), expanded: true, hasGifs: false, hasInteractiveStickers: false) + controller.completion = { content in + if let content, case let .file(file, _) = content { + completion(file) + } + return true + } + return controller + } public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { return proxySettingsController(accountManager: sharedContext.accountManager, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData) diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h index 224d579d42..96934adf5a 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h @@ -29,6 +29,7 @@ UIView * _Nullable getPortalViewSourceView(UIView * _Nonnull portalView); NSObject * _Nullable makeBlurFilter(); NSObject * _Nullable makeLuminanceToAlphaFilter(); +NSObject * _Nullable makeMonochromeFilter(); void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots); void setLayerContentsMaskMode(CALayer * _Nonnull layer, bool maskMode); diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index 717c584044..714d7032bd 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -232,6 +232,11 @@ NSObject * _Nullable makeLuminanceToAlphaFilter() { return [(id)NSClassFromString(@"CAFilter") filterWithName:@"luminanceToAlpha"]; } +NSObject * _Nullable makeMonochromeFilter() { + return [(id)NSClassFromString(@"CAFilter") filterWithName:@"colorMonochrome"]; +} + + void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots) { static UITextField *textField = nil; static UIView *secureView = nil;