diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index f79918f225..20e71ee2d6 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -705,6 +705,8 @@ private final class NotificationServiceHandler { let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) setupSharedLogger(rootPath: logsPath, path: logsPath) + + Logger.shared.log("NotificationService \(episode)", "Started handling notification") initializeAccountManagement() diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b077464c25..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."; @@ -11594,3 +11599,20 @@ Sorry for the inconvenience."; "Chat.QuickReplyMediaMessageLimitReachedText_1" = "There can be at most %d message in this chat."; "Chat.QuickReplyMediaMessageLimitReachedText_any" = "There can be at most %d messages in this chat."; + +"CollectibleItemInfo.StoreName" = "Fragment"; +"CollectibleItemInfo.UsernameTitle" = "%@ is a collectible username that belongs to"; +"CollectibleItemInfo.UsernameText" = "The %1$@ username was acquired on %2$@ on %3$@ for %4$@ (%5$@)."; +"CollectibleItemInfo.PhoneTitle" = "%@ is a collectible phone number that belongs to"; +"CollectibleItemInfo.PhoneText" = "The %1$@ phone number was acquired on %2$@ on %3$@ for %4$@ (%5$@)."; +"CollectibleItemInfo.ButtonOpenInfo" = "Learn More"; +"CollectibleItemInfo.ButtonCopyUsername" = "Copy Link"; +"CollectibleItemInfo.ButtonCopyPhone" = "Copy Phone Number"; +"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/Telegram/WidgetKitWidget/TodayViewController.swift b/Telegram/WidgetKitWidget/TodayViewController.swift index e33a63b225..5247e3caca 100644 --- a/Telegram/WidgetKitWidget/TodayViewController.swift +++ b/Telegram/WidgetKitWidget/TodayViewController.swift @@ -729,9 +729,22 @@ struct WidgetView: View { chatUpdateView(size: geometry.size) }) }) - .background(Rectangle().foregroundColor(getBackgroundColor())) .padding(0.0) .unredacted() + .widgetBackground(Rectangle().foregroundColor(getBackgroundColor())) + } +} + +@available(iOSApplicationExtension 14.0, iOS 14.0, *) +extension View { + func widgetBackground(_ backgroundView: some View) -> some View { + if #available(iOSApplicationExtension 17.0, iOS 17.0, *) { + return containerBackground(for: .widget) { + backgroundView + } + } else { + return background(backgroundView) + } } } @@ -766,6 +779,17 @@ struct AvatarsWidgetView: View { } } + func getBackgroundColor() -> Color { + switch colorScheme { + case .light: + return .white + case .dark: + return Color(.sRGB, red: 28.0 / 255.0, green: 28.0 / 255.0, blue: 30.0 / 255.0, opacity: 1.0) + @unknown default: + return .secondary + } + } + func itemView(index: Int) -> some View { let peers: ParsedPeers? var isPlaceholder = false @@ -821,6 +845,7 @@ struct AvatarsWidgetView: View { }) .padding(EdgeInsets(top: 10.0, leading: 10.0, bottom: 10.0, trailing: 10.0)) .unredacted() + .widgetBackground(Rectangle().foregroundColor(getBackgroundColor())) } } @@ -854,12 +879,22 @@ struct Static_Widget: Widget { public var body: some WidgetConfiguration { let presentationData = WidgetPresentationData.getForExtension() - return IntentConfiguration(kind: kind, intent: SelectFriendsIntent.self, provider: Provider(), content: { entry in - WidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData) - }) - .supportedFamilies([.systemMedium]) - .configurationDisplayName(presentationData.widgetChatsGalleryTitle) - .description(presentationData.widgetChatsGalleryDescription) + if #available(iOSApplicationExtension 15.0, iOS 15.0, *) { + return IntentConfiguration(kind: kind, intent: SelectFriendsIntent.self, provider: Provider(), content: { entry in + WidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData) + }) + .supportedFamilies([.systemMedium]) + .configurationDisplayName(presentationData.widgetChatsGalleryTitle) + .contentMarginsDisabled() + .description(presentationData.widgetChatsGalleryDescription) + } else { + return IntentConfiguration(kind: kind, intent: SelectFriendsIntent.self, provider: Provider(), content: { entry in + WidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData) + }) + .supportedFamilies([.systemMedium]) + .configurationDisplayName(presentationData.widgetChatsGalleryTitle) + .description(presentationData.widgetChatsGalleryDescription) + } } } @@ -870,12 +905,22 @@ struct Static_AvatarsWidget: Widget { public var body: some WidgetConfiguration { let presentationData = WidgetPresentationData.getForExtension() - return IntentConfiguration(kind: kind, intent: SelectAvatarFriendsIntent.self, provider: AvatarsProvider(), content: { entry in - AvatarsWidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData) - }) - .supportedFamilies([.systemMedium]) - .configurationDisplayName(presentationData.widgetShortcutsGalleryTitle) - .description(presentationData.widgetShortcutsGalleryDescription) + if #available(iOSApplicationExtension 15.0, iOS 15.0, *) { + return IntentConfiguration(kind: kind, intent: SelectAvatarFriendsIntent.self, provider: AvatarsProvider(), content: { entry in + AvatarsWidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData) + }) + .supportedFamilies([.systemMedium]) + .configurationDisplayName(presentationData.widgetShortcutsGalleryTitle) + .contentMarginsDisabled() + .description(presentationData.widgetShortcutsGalleryDescription) + } else { + return IntentConfiguration(kind: kind, intent: SelectAvatarFriendsIntent.self, provider: AvatarsProvider(), content: { entry in + AvatarsWidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData) + }) + .supportedFamilies([.systemMedium]) + .configurationDisplayName(presentationData.widgetShortcutsGalleryTitle) + .description(presentationData.widgetShortcutsGalleryDescription) + } } } diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index bdb918f337..d76ab65342 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -101,7 +101,7 @@ public enum TextLinkItemActionType { case longTap } -public enum TextLinkItem { +public enum TextLinkItem: Equatable { case url(url: String, concealed: Bool) case mention(String) case hashtag(String?, String) @@ -856,6 +856,15 @@ public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject { public protocol ChatbotSetupScreenInitialData: AnyObject { } +public protocol CollectibleItemInfoScreenInitialData: AnyObject { + var collectibleItemInfo: TelegramCollectibleItemInfo { get } +} + +public enum CollectibleItemInfoScreenSubject { + case phoneNumber(String) + case username(String) +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -951,6 +960,8 @@ public protocol SharedAccountContext: AnyObject { func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal + func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController + func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal @@ -980,14 +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], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> 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, 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/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 75406cdcf6..3cc929b696 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -231,17 +231,52 @@ public enum ChatRecordedMediaPreview: Equatable { case video(Video) } +public final class ChatManagingBot: Equatable { + public let bot: EnginePeer + public let isPaused: Bool + public let canReply: Bool + public let settingsUrl: String? + + public init(bot: EnginePeer, isPaused: Bool, canReply: Bool, settingsUrl: String?) { + self.bot = bot + self.isPaused = isPaused + self.canReply = canReply + self.settingsUrl = settingsUrl + } + + public static func ==(lhs: ChatManagingBot, rhs: ChatManagingBot) -> Bool { + if lhs === rhs { + return true + } + if lhs.bot != rhs.bot { + return false + } + if lhs.isPaused != rhs.isPaused { + return false + } + if lhs.canReply != rhs.canReply { + return false + } + if lhs.settingsUrl != rhs.settingsUrl { + return false + } + return true + } +} + public struct ChatContactStatus: Equatable { public var canAddContact: Bool public var canReportIrrelevantLocation: Bool public var peerStatusSettings: PeerStatusSettings? public var invitedBy: Peer? + public var managingBot: ChatManagingBot? - public init(canAddContact: Bool, canReportIrrelevantLocation: Bool, peerStatusSettings: PeerStatusSettings?, invitedBy: Peer?) { + public init(canAddContact: Bool, canReportIrrelevantLocation: Bool, peerStatusSettings: PeerStatusSettings?, invitedBy: Peer?, managingBot: ChatManagingBot?) { self.canAddContact = canAddContact self.canReportIrrelevantLocation = canReportIrrelevantLocation self.peerStatusSettings = peerStatusSettings self.invitedBy = invitedBy + self.managingBot = managingBot } public var isEmpty: Bool { @@ -270,6 +305,9 @@ public struct ChatContactStatus: Equatable { if !arePeersEqual(lhs.invitedBy, rhs.invitedBy) { return false } + if lhs.managingBot != rhs.managingBot { + return false + } return true } } diff --git a/submodules/ContextUI/Sources/PeekController.swift b/submodules/ContextUI/Sources/PeekController.swift index 3d9c459a33..c76409fbee 100644 --- a/submodules/ContextUI/Sources/PeekController.swift +++ b/submodules/ContextUI/Sources/PeekController.swift @@ -45,9 +45,11 @@ public final class PeekController: ViewController, ContextControllerProtocol { } public func pushItems(items: Signal) { + self.controllerNode.pushItems(items: items) } public func popItems() { + self.controllerNode.popItems() } private var controllerNode: PeekControllerNode { @@ -61,6 +63,7 @@ public final class PeekController: ViewController, ContextControllerProtocol { private let presentationData: PresentationData private let content: PeekControllerContent var sourceView: () -> (UIView, CGRect)? + private let activateImmediately: Bool public var visibilityUpdated: ((Bool) -> Void)? @@ -73,10 +76,11 @@ public final class PeekController: ViewController, ContextControllerProtocol { return self._ready } - public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?) { + public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?, activateImmediately: Bool = false) { self.presentationData = presentationData self.content = content self.sourceView = sourceView + self.activateImmediately = activateImmediately super.init(navigationBarPresentationData: nil) @@ -111,6 +115,10 @@ public final class PeekController: ViewController, ContextControllerProtocol { self.controllerNode.animateIn(from: self.getSourceRect()) self.visibilityUpdated?(true) + + if self.activateImmediately { + self.controllerNode.activateMenu() + } } } diff --git a/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift index a766664a2c..26e1025f13 100644 --- a/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift +++ b/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift @@ -22,6 +22,7 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { private let present: (PeekControllerContent, UIView, CGRect) -> ViewController? private let updateContent: (PeekControllerContent?) -> Void private let activateBySingleTap: Bool + public var longPressEnabled = true public var checkSingleTapActivationAtPoint: ((CGPoint) -> Bool)? private var tapLocation: CGPoint? @@ -54,6 +55,9 @@ public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { } private func startLongTapTimer() { + guard self.longPressEnabled else { + return + } self.longTapTimer?.invalidate() let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in self?.longTapTimerFired() diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index f2bbdafc5b..eb5454e783 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import SwiftSignalKit import TelegramPresentationData private let animationDurationFactor: Double = 1.0 @@ -28,11 +29,11 @@ final class PeekControllerNode: ViewControllerTracingNode { private var topAccessoryNode: ASDisplayNode? private var fullScreenAccessoryNode: (PeekControllerAccessoryNode & ASDisplayNode)? - - private var actionsContainerNode: ContextActionsContainerNode + + private var actionsStackNode: ContextControllerActionsStackNode private var hapticFeedback = HapticFeedback() - + private var initialContinueGesturePoint: CGPoint? private var didMoveFromInitialGesturePoint = false private var highlightedActionNode: ContextActionNodeProtocol? @@ -55,12 +56,12 @@ final class PeekControllerNode: ViewControllerTracingNode { self.darkDimNode.isUserInteractionEnabled = false switch content.menuActivation() { - case .drag: - self.dimNode.backgroundColor = nil - self.blurView.alpha = 1.0 - case .press: - self.dimNode.backgroundColor = UIColor(white: self.theme.isDark ? 0.0 : 1.0, alpha: 0.5) - self.blurView.alpha = 0.0 + case .drag: + self.dimNode.backgroundColor = nil + self.blurView.alpha = 1.0 + case .press: + self.dimNode.backgroundColor = UIColor(white: self.theme.isDark ? 0.0 : 1.0, alpha: 0.5) + self.blurView.alpha = 0.0 } self.containerBackgroundNode = ASImageNode() @@ -75,28 +76,50 @@ final class PeekControllerNode: ViewControllerTracingNode { self.fullScreenAccessoryNode = content.fullScreenAccessoryNode(blurView: blurView) self.fullScreenAccessoryNode?.alpha = 0.0 - var feedbackTapImpl: (() -> Void)? var activatedActionImpl: (() -> Void)? - var requestLayoutImpl: (() -> Void)? - self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(content: .list(content.menuItems()), animationCache: nil), getController: { [weak controller] in - return controller - }, actionSelected: { result in - activatedActionImpl?() - }, requestLayout: { - requestLayoutImpl?() - }, feedbackTap: { - feedbackTapImpl?() - }, blurBackground: true) - self.actionsContainerNode.alpha = 0.0 + var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)? - super.init() + self.actionsStackNode = ContextControllerActionsStackNode( + getController: { [weak controller] in + return controller + }, + requestDismiss: { result in + activatedActionImpl?() + }, + requestUpdate: { transition in + requestLayoutImpl?(transition) + } + ) + self.actionsStackNode.alpha = 0.0 - feedbackTapImpl = { [weak self] in - self?.hapticFeedback.tap() + let items = ContextController.Items( + id: 0, + content: .list(content.menuItems()), + 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 + ) + if let item = makeContextControllerActionsStackItem(items: items).first { + self.actionsStackNode.replace( + item: item, + animated: false + ) } - - requestLayoutImpl = { [weak self] in - self?.updateLayout() + + super.init() + + requestLayoutImpl = { [weak self] transition in + self?.updateLayout(transition: transition) } if content.presentation() == .freeform { @@ -112,7 +135,7 @@ final class PeekControllerNode: ViewControllerTracingNode { self.containerNode.addSubnode(self.contentNode) self.addSubnode(self.containerNode) - self.addSubnode(self.actionsContainerNode) + self.addSubnode(self.actionsStackNode) if let fullScreenAccessoryNode = self.fullScreenAccessoryNode { self.fullScreenAccessoryNode?.dismiss = { [weak self] in @@ -139,13 +162,41 @@ final class PeekControllerNode: ViewControllerTracingNode { self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } - - func updateLayout() { + + func updateLayout(transition: ContainedViewLayoutTransition = .immediate) { if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .immediate) + self.containerLayoutUpdated(layout, transition: transition) } } + func replaceItem(items: Signal) { + let _ = (items + |> deliverOnMainQueue).start(next: { [weak self] items in + guard let self else { + return + } + if let item = makeContextControllerActionsStackItem(items: items).first { + self.actionsStackNode.replace(item: item, animated: false) + } + }) + } + + func pushItems(items: Signal) { + let _ = (items + |> deliverOnMainQueue).start(next: { [weak self] items in + guard let self else { + return + } + if let item = makeContextControllerActionsStackItem(items: items).first { + self.actionsStackNode.push(item: item, currentScrollingState: nil, positionLock: nil, animated: true) + } + }) + } + + func popItems() { + self.actionsStackNode.pop() + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout @@ -172,12 +223,18 @@ final class PeekControllerNode: ViewControllerTracingNode { } let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0 - let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: .immediate) + + let actionsSize = self.actionsStackNode.update( + presentationData: self.presentationData, + constrainedSize: CGSize(width: layout.size.width - actionsSideInset * 2.0, height: layout.size.height), + presentation: .inline, + transition: transition + ) let containerFrame: CGRect let actionsFrame: CGRect if layout.size.width > layout.size.height { - if self.actionsContainerNode.alpha.isZero { + if self.actionsStackNode.alpha.isZero { containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) } else { containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 3.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) @@ -194,12 +251,11 @@ final class PeekControllerNode: ViewControllerTracingNode { } transition.updateFrame(node: self.containerNode, frame: containerFrame) - self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize) - transition.updateFrame(node: self.actionsContainerNode, frame: actionsFrame) + transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame) if let fullScreenAccessoryNode = self.fullScreenAccessoryNode { - fullScreenAccessoryNode.updateLayout(size: layout.size, transition: transition) transition.updateFrame(node: fullScreenAccessoryNode, frame: CGRect(origin: .zero, size: layout.size)) + fullScreenAccessoryNode.updateLayout(size: layout.size, transition: transition) } self.contentNodeHasValidLayout = true @@ -244,11 +300,11 @@ final class PeekControllerNode: ViewControllerTracingNode { self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) - if !self.actionsContainerNode.alpha.isZero { - let actionsOffset = CGPoint(x: rect.midX - self.actionsContainerNode.position.x, y: rect.midY - self.actionsContainerNode.position.y) - self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false) - self.actionsContainerNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) - self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: actionsOffset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + if !self.actionsStackNode.alpha.isZero { + let actionsOffset = CGPoint(x: rect.midX - self.actionsStackNode.position.x, y: rect.midY - self.actionsStackNode.position.y) + self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false) + self.actionsStackNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) + self.actionsStackNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: actionsOffset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) } if let fullScreenAccessoryNode = self.fullScreenAccessoryNode, !fullScreenAccessoryNode.alpha.isZero { @@ -289,7 +345,7 @@ final class PeekControllerNode: ViewControllerTracingNode { initialPoint = localPoint self.initialContinueGesturePoint = localPoint } - if !self.actionsContainerNode.alpha.isZero { + if !self.actionsStackNode.alpha.isZero { if !self.didMoveFromInitialGesturePoint { let distance = abs(localPoint.y - initialPoint.y) if distance > 12.0 { @@ -297,16 +353,19 @@ final class PeekControllerNode: ViewControllerTracingNode { } } if self.didMoveFromInitialGesturePoint { - let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view) - let actionNode = self.actionsContainerNode.actionNode(at: actionPoint) - if self.highlightedActionNode !== actionNode { - self.highlightedActionNode?.setIsHighlighted(false) - self.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - self.hapticFeedback.tap() - } - } + let actionPoint = self.view.convert(localPoint, to: self.actionsStackNode.view) + self.actionsStackNode.highlightGestureMoved(location: actionPoint) + } + } + } + + func endDragging(_ location: CGPoint) { + if self.didMoveFromInitialGesturePoint { + self.actionsStackNode.highlightGestureFinished(performAction: true) + } else if self.actionsStackNode.alpha.isZero { + if let fullScreenAccessoryNode = self.fullScreenAccessoryNode, !fullScreenAccessoryNode.alpha.isZero { + } else { + self.requestDismiss() } } } @@ -322,6 +381,11 @@ final class PeekControllerNode: ViewControllerTracingNode { self.blurView.layer.animateAlpha(from: previousBlurAlpha, to: self.blurView.alpha, duration: 0.3) } return + } else { + if let fullScreenAccessoryNode = self.fullScreenAccessoryNode { + fullScreenAccessoryNode.alpha = 1.0 + fullScreenAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } } if case .press = self.content.menuActivation() { self.hapticFeedback.impact() @@ -338,32 +402,20 @@ final class PeekControllerNode: ViewControllerTracingNode { self.darkDimNode.alpha = 1.0 self.darkDimNode.layer.animateAlpha(from: previousDarkDimAlpha, to: 1.0, duration: 0.3) - self.actionsContainerNode.alpha = 1.0 - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) - self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - - let localContentSourceFrame = self.containerNode.frame - self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localContentSourceFrame.center.x - self.actionsContainerNode.position.x, y: localContentSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + Queue.mainQueue().justDispatch { + self.actionsStackNode.alpha = 1.0 + self.actionsStackNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) + self.actionsStackNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + let localContentSourceFrame = self.containerNode.frame + self.actionsStackNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localContentSourceFrame.center.x - self.actionsStackNode.position.x, y: localContentSourceFrame.center.y - self.actionsStackNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + } if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .animated(duration: springDuration, curve: .spring)) } } - func endDragging(_ location: CGPoint) { - if self.didMoveFromInitialGesturePoint { - if let highlightedActionNode = self.highlightedActionNode { - self.highlightedActionNode = nil - highlightedActionNode.performAction() - } - } else if self.actionsContainerNode.alpha.isZero { - if let fullScreenAccessoryNode = self.fullScreenAccessoryNode, !fullScreenAccessoryNode.alpha.isZero { - } else { - self.requestDismiss() - } - } - } - func updateContent(content: PeekControllerContent) { let contentNode = self.contentNode contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in @@ -376,19 +428,7 @@ final class PeekControllerNode: ViewControllerTracingNode { self.containerNode.addSubnode(self.contentNode) self.contentNodeHasValidLayout = false - let previousActionsContainerNode = self.actionsContainerNode - self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: ContextController.Items(content: .list(content.menuItems()), animationCache: nil), getController: { [weak self] in - return self?.controller - }, actionSelected: { [weak self] result in - self?.requestDismiss() - }, requestLayout: { [weak self] in - self?.updateLayout() - }, feedbackTap: { [weak self] in - self?.hapticFeedback.tap() - }, blurBackground: true) - self.actionsContainerNode.alpha = 0.0 - self.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) - previousActionsContainerNode.removeFromSupernode() + self.replaceItem(items: .single(ContextController.Items(content: .list(content.menuItems())))) self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index a6d3392043..4e71fb78ab 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -5,7 +5,7 @@ import SwiftSignalKit private var backArrowImageCache: [Int32: UIImage] = [:] open class SparseNode: ASDisplayNode { - override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.alpha.isZero { return nil } 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/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 34253dde77..7d50682a93 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -145,7 +145,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.angleLayer.opacity = 0.0 self.angleLayer.lineDashPattern = [12, 12] as [NSNumber] - self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.6).cgColor + self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.7).cgColor self.stickerFrameLayer.fillColor = UIColor.clear.cgColor self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor diff --git a/submodules/DrawingUI/Sources/DrawingReactionView.swift b/submodules/DrawingUI/Sources/DrawingReactionView.swift index 7a3b597a52..a86449a87e 100644 --- a/submodules/DrawingUI/Sources/DrawingReactionView.swift +++ b/submodules/DrawingUI/Sources/DrawingReactionView.swift @@ -177,7 +177,7 @@ public class DrawingReactionEntityView: DrawingStickerEntityView { reactionContextNode.forceTailToRight = true reactionContextNode.forceDark = true self.reactionContextNode = reactionContextNode - + reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in guard let self else { return diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 8ce1998d79..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()) + 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/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift index 3a25687876..be09ed33a2 100644 --- a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift +++ b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift @@ -496,8 +496,8 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } if let searchNode = strongSelf.searchNode, searchNode.isActive { if let (itemNode, item) = searchNode.itemAt(point: strongSelf.view.convert(point, to: searchNode.view)) { - if let item = item as? StickerPreviewPeekItem { - return strongSelf.context.engine.stickers.isStickerSaved(id: item.file.fileId) + if let item = item as? StickerPreviewPeekItem, let file = item.file { + return strongSelf.context.engine.stickers.isStickerSaved(id: file.fileId) |> deliverOnMainQueue |> map { isStarred -> (UIView, CGRect, PeekControllerContent)? in if let strongSelf = self { @@ -506,9 +506,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: file), imageNode.view, imageNode.bounds) } } f(.default) @@ -517,11 +517,11 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { f(.default) if let strongSelf = self { - let _ = (strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file, saved: !isStarred) + let _ = (strongSelf.context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil) + strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -530,7 +530,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } else { text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, loop: true, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, loop: true, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) @@ -548,7 +548,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { f(.default) if let strongSelf = self { - loop: for attribute in item.file.attributes { + loop: for attribute in file.attributes { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index 1a64cdec19..6e39581e13 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -14,6 +14,7 @@ import ContextUI import RadialStatusNode import UndoUI import StickerPackPreviewUI +import StickerPackEditTitleController private struct StickerPackPreviewGridEntry: Comparable, Equatable, Identifiable { let index: Int @@ -718,7 +719,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll @objc private func createActionButtonPressed() { var proceedImpl: ((String, String?) -> Void)? - let titleController = importStickerPackTitleController(context: self.context, title: self.presentationData.strings.ImportStickerPack_ChooseName, text: self.presentationData.strings.ImportStickerPack_ChooseNameDescription, placeholder: self.presentationData.strings.ImportStickerPack_NamePlaceholder, value: nil, maxLength: 128, apply: { [weak self] title in + let titleController = stickerPackEditTitleController(context: self.context, title: self.presentationData.strings.ImportStickerPack_ChooseName, text: self.presentationData.strings.ImportStickerPack_ChooseNameDescription, placeholder: self.presentationData.strings.ImportStickerPack_NamePlaceholder, value: nil, maxLength: 128, apply: { [weak self] title in if let strongSelf = self, let title = title { strongSelf.shortNameSuggestionDisposable.set((strongSelf.context.engine.stickers.getStickerSetShortNameSuggestion(title: title) |> deliverOnMainQueue).start(next: { suggestedShortName in 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/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index f6412b6200..756cd33f56 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -91,6 +91,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer } } + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private let avatarNode: AvatarNode @@ -220,6 +221,10 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } @@ -385,6 +390,7 @@ public enum ShareLoadingState { } public final class JoinLinkPreviewLoadingContainerNode: ASDisplayNode, ShareContentContainerNode { + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private var theme: PresentationTheme @@ -408,6 +414,10 @@ public final class JoinLinkPreviewLoadingContainerNode: ASDisplayNode, ShareCont public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + public func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift index 13ebb8e9bd..e4c691aff3 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift @@ -78,6 +78,9 @@ final class LanguageLinkPreviewContentNode: ASDisplayNode, ShareContentContainer func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + func setDidBeginDragging(_ f: (() -> Void)?) { + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 994ddbf39e..88818178fd 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -561,6 +561,9 @@ if (_editingContext != nil) { + if (_timersChangedDisposable) { + [_timersChangedDisposable dispose]; + } _timersChangedDisposable = [[SMetaDisposable alloc] init]; [_timersChangedDisposable setDisposable:[_editingContext.timersUpdatedSignal startStrictWithNext:^(__unused NSNumber *next) { @@ -586,6 +589,7 @@ self.delegate = nil; [_selectionChangedDisposable dispose]; [_tooltipDismissDisposable dispose]; + [_timersChangedDisposable dispose]; [_adjustmentsChangedDisposable dispose]; } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m index 1cc45395ca..7b6117cf92 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m @@ -248,11 +248,11 @@ }]]; - if (!item.asFile) { - [_facesDisposable setDisposable:[[TGPaintFaceDetector detectFacesInItem:item.editableMediaItem editingContext:item.editingContext] startStrictWithNext:nil file:__FILE_NAME__ line:__LINE__]]; - - return; - } +// if (!item.asFile) { +// [_facesDisposable setDisposable:[[TGPaintFaceDetector detectFacesInItem:item.editableMediaItem editingContext:item.editingContext] startStrictWithNext:nil file:__FILE_NAME__ line:__LINE__]]; +// +// return; +// } _fileInfoLabel.text = nil; diff --git a/submodules/LocalAuth/Sources/LocalAuth.swift b/submodules/LocalAuth/Sources/LocalAuth.swift index 2ea1dcd3e6..ad67c038e6 100644 --- a/submodules/LocalAuth/Sources/LocalAuth.swift +++ b/submodules/LocalAuth/Sources/LocalAuth.swift @@ -1,6 +1,7 @@ import Foundation import LocalAuthentication import SwiftSignalKit +import Security public enum LocalAuthBiometricAuthentication { case touchId @@ -8,10 +9,83 @@ public enum LocalAuthBiometricAuthentication { } public struct LocalAuth { - public static let biometricAuthentication: LocalAuthBiometricAuthentication? = { + private static let customKeyIdPrefix = "$#_".data(using: .utf8)! + + public enum DecryptionResult { + public enum Error { + case cancelled + case generic + } + + case result(Data) + case error(Error) + } + + #if targetEnvironment(simulator) + public final class PrivateKey { + public let publicKeyRepresentation: Data + + fileprivate init() { + self.publicKeyRepresentation = Data(count: 32) + } + + public func encrypt(data: Data) -> Data? { + return data + } + + public func decrypt(data: Data) -> DecryptionResult { + return .result(data) + } + } + #else + public final class PrivateKey { + private let privateKey: SecKey + private let publicKey: SecKey + public let publicKeyRepresentation: Data + + fileprivate init(privateKey: SecKey, publicKey: SecKey, publicKeyRepresentation: Data) { + self.privateKey = privateKey + self.publicKey = publicKey + self.publicKeyRepresentation = publicKeyRepresentation + } + + public func encrypt(data: Data) -> Data? { + var error: Unmanaged? + let cipherText = SecKeyCreateEncryptedData(self.publicKey, .eciesEncryptionCofactorVariableIVX963SHA512AESGCM, data as CFData, &error) + if let error { + error.release() + } + guard let cipherText else { + return nil + } + + let result = cipherText as Data + return result + } + + public func decrypt(data: Data) -> DecryptionResult { + var maybeError: Unmanaged? + let plainText = SecKeyCreateDecryptedData(self.privateKey, .eciesEncryptionCofactorVariableIVX963SHA512AESGCM, data as CFData, &maybeError) + let error = maybeError?.takeRetainedValue() + + guard let plainText else { + if let error { + if CFErrorGetCode(error) == -2 { + return .error(.cancelled) + } + } + return .error(.generic) + } + + let result = plainText as Data + return .result(result) + } + } + #endif + + public static var biometricAuthentication: LocalAuthBiometricAuthentication? { let context = LAContext() if context.canEvaluatePolicy(LAPolicy(rawValue: Int(kLAPolicyDeviceOwnerAuthenticationWithBiometrics))!, error: nil) { - #if swift(>=5.9) switch context.biometryType { case .faceID, .opticID: return .faceId @@ -22,22 +96,10 @@ public struct LocalAuth { @unknown default: return nil } - #else - switch context.biometryType { - case .faceID://, .opticID: - return .faceId - case .touchID: - return .touchId - case .none: - return nil - @unknown default: - return nil - } - #endif } else { return nil } - }() + } public static let evaluatedPolicyDomainState: Data? = { let context = LAContext() @@ -78,4 +140,161 @@ public struct LocalAuth { } } } + + private static func bundleSeedId() -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword as String, + kSecAttrAccount as String: "bundleSeedID", + kSecAttrService as String: "", + kSecReturnAttributes as String: true + ] + var result: CFTypeRef? + var status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecItemNotFound { + status = SecItemAdd(query as CFDictionary, &result) + } + if status != errSecSuccess { + return nil + } + guard let result = result else { + return nil + } + if CFGetTypeID(result) != CFDictionaryGetTypeID() { + return nil + } + guard let resultDict = (result as! CFDictionary) as? [String: Any] else { + return nil + } + guard let accessGroup = resultDict[kSecAttrAccessGroup as String] as? String else { + return nil + } + let components = accessGroup.components(separatedBy: ".") + guard let seedId = components.first else { + return nil + } + return seedId; + } + + public static func getOrCreatePrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? { + if let key = self.getPrivateKey(baseAppBundleId: baseAppBundleId, keyId: keyId) { + return key + } else { + return self.addPrivateKey(baseAppBundleId: baseAppBundleId, keyId: keyId) + } + } + + private static func getPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? { + #if targetEnvironment(simulator) + return PrivateKey() + #else + guard let bundleSeedId = self.bundleSeedId() else { + return nil + } + + let applicationTag = customKeyIdPrefix + keyId + let accessGroup = "\(bundleSeedId).\(baseAppBundleId)" + + let query: [String: Any] = [ + kSecClass as String: kSecClassKey as String, + kSecAttrApplicationTag as String: applicationTag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom as String, + kSecAttrAccessGroup as String: accessGroup, + kSecReturnRef as String: true + ] + + var maybePrivateKey: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &maybePrivateKey) + if status != errSecSuccess { + return nil + } + guard let maybePrivateKey else { + return nil + } + if CFGetTypeID(maybePrivateKey) != SecKeyGetTypeID() { + return nil + } + let privateKey = maybePrivateKey as! SecKey + + guard let publicKey = SecKeyCopyPublicKey(privateKey) else { + return nil + } + guard let publicKeyRepresentation = SecKeyCopyExternalRepresentation(publicKey, nil) else { + return nil + } + + let result = PrivateKey(privateKey: privateKey, publicKey: publicKey, publicKeyRepresentation: publicKeyRepresentation as Data) + + return result + #endif + } + + public static func removePrivateKey(baseAppBundleId: String, keyId: Data) -> Bool { + guard let bundleSeedId = self.bundleSeedId() else { + return false + } + + let applicationTag = customKeyIdPrefix + keyId + let accessGroup = "\(bundleSeedId).\(baseAppBundleId)" + + let query: [String: Any] = [ + kSecClass as String: kSecClassKey as String, + kSecAttrApplicationTag as String: applicationTag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom as String, + kSecAttrIsPermanent as String: true, + kSecAttrAccessGroup as String: accessGroup + ] + + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess { + return false + } + return true + } + + private static func addPrivateKey(baseAppBundleId: String, keyId: Data) -> PrivateKey? { + #if targetEnvironment(simulator) + return PrivateKey() + #else + guard let bundleSeedId = self.bundleSeedId() else { + return nil + } + + let applicationTag = customKeyIdPrefix + keyId + let accessGroup = "\(bundleSeedId).\(baseAppBundleId)" + + guard let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, [.userPresence, .privateKeyUsage], nil) else { + return nil + } + + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom as String, + kSecAttrKeySizeInBits as String: 256 as NSNumber, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave as String, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: applicationTag, + kSecAttrAccessControl as String: access, + kSecAttrAccessGroup as String: accessGroup, + ] as [String: Any] + ] + var error: Unmanaged? + let maybePrivateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) + if let error { + error.release() + } + guard let privateKey = maybePrivateKey else { + return nil + } + + guard let publicKey = SecKeyCopyPublicKey(privateKey) else { + return nil + } + guard let publicKeyRepresentation = SecKeyCopyExternalRepresentation(publicKey, nil) else { + return nil + } + + let result = PrivateKey(privateKey: privateKey, publicKey: publicKey, publicKeyRepresentation: publicKeyRepresentation as Data) + return result + #endif + } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 45682f7729..c4859c5482 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -678,7 +678,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if !controller.didSetupGroups { controller.didSetupGroups = true - Queue.concurrentDefaultQueue().after(0.3) { + Queue.concurrentDefaultQueue().after(0.4) { + var isCreateSticker = false + if case .assets(_, .createSticker) = controller.subject { + isCreateSticker = true + } controller.groupsPromise.set( combineLatest( self.mediaAssetsContext.fetchAssetsCollections(.album), @@ -707,6 +711,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { supportedAlbums.append(.smartAlbumDepthEffect) supportedAlbums.append(.smartAlbumLivePhotos) } + + if isCreateSticker { + supportedAlbums = supportedAlbums.filter { type in + if type == .smartAlbumSlomoVideos || type == .smartAlbumTimelapses || type == .smartAlbumVideos { + return false + } + return true + } + } + if supportedAlbums.contains(collection.assetCollectionSubtype) { let result = PHAsset.fetchAssets(in: collection, options: nil) if result.count > 0 { @@ -2590,3 +2604,51 @@ public func storyMediaPickerController( controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller } + +public func stickerMediaPickerController( + context: AccountContext, + getSourceRect: @escaping () -> CGRect, + completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, + dismissed: @escaping () -> Void +) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { + return nil + }) + controller.forceSourceRect = true + controller.getSourceRect = getSourceRect + controller.requestController = { _, present in + let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .createSticker), mainButtonState: nil, mainButtonAction: nil) + mediaPickerController.customSelection = { controller, result in + if let result = result as? PHAsset { + controller.updateHiddenMediaId(result.localIdentifier) + if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + let transitionOut: (Bool?) -> (UIView, CGRect)? = { isNew in + if let isNew { + if isNew { + controller.updateHiddenMediaId(nil) + if let transitionView = controller.defaultTransitionView() { + return (transitionView, transitionView.bounds) + } + } else if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + return (transitionView, transitionView.bounds) + } + } + return nil + } + completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in + controller?.updateHiddenMediaId(nil) + }) + } + } + } + present(mediaPickerController, mediaPickerController.mediaPickerContext) + } + controller.willDismiss = { + dismissed() + } + controller.navigationPresentation = .flatModal + controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + return controller +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 9d05f7ea39..a1c87d391d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -1163,7 +1163,6 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer { } } - private var currentItemNode: ASDisplayNode? override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h index b1e9361e08..c7105cb69c 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTRequestErrorContext.h @@ -8,6 +8,7 @@ @property (nonatomic) NSUInteger internalServerErrorCount; @property (nonatomic) NSUInteger floodWaitSeconds; +@property (nonatomic, strong) NSString *floodWaitErrorText; @property (nonatomic) bool waitingForTokenExport; @property (nonatomic, strong) id waitingForRequestToComplete; diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index 83457c3c61..a81adedb34 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -808,7 +808,7 @@ } restartRequest = true; } - else if (rpcError.errorCode == 420 || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound) { + else if (rpcError.errorCode == 420 || [rpcError.errorDescription rangeOfString:@"FLOOD_WAIT_"].location != NSNotFound || [rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) { if (request.errorContext == nil) request.errorContext = [[MTRequestErrorContext alloc] init]; @@ -821,6 +821,32 @@ if ([scanner scanInt:&errorWaitTime]) { request.errorContext.floodWaitSeconds = errorWaitTime; + request.errorContext.floodWaitErrorText = rpcError.errorDescription; + + if (request.shouldContinueExecutionWithErrorContext != nil) + { + if (request.shouldContinueExecutionWithErrorContext(request.errorContext)) + { + restartRequest = true; + request.errorContext.minimalExecuteTime = MAX(request.errorContext.minimalExecuteTime, MTAbsoluteSystemTime() + (CFAbsoluteTime)errorWaitTime); + } + } + else + { + restartRequest = true; + request.errorContext.minimalExecuteTime = MAX(request.errorContext.minimalExecuteTime, MTAbsoluteSystemTime() + (CFAbsoluteTime)errorWaitTime); + } + } + } else if ([rpcError.errorDescription rangeOfString:@"FLOOD_PREMIUM_WAIT_"].location != NSNotFound) { + int errorWaitTime = 0; + + NSScanner *scanner = [[NSScanner alloc] initWithString:rpcError.errorDescription]; + [scanner scanUpToString:@"FLOOD_PREMIUM_WAIT_" intoString:nil]; + [scanner scanString:@"FLOOD_PREMIUM_WAIT_" intoString:nil]; + if ([scanner scanInt:&errorWaitTime]) + { + request.errorContext.floodWaitSeconds = errorWaitTime; + request.errorContext.floodWaitErrorText = rpcError.errorDescription; if (request.shouldContinueExecutionWithErrorContext != nil) { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 40a62f57d8..5c17964f83 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -2999,6 +2999,16 @@ final class PostboxImpl { let startTime = CFAbsoluteTimeGetCurrent() + #if os(macOS) + #if DEBUG || BETA + var crashDisposable: Disposable? + crashDisposable = (Signal.single(Void()) + |> delay(0.1, queue: .concurrentDefaultQueue())).startStandalone(next: { _ in + preconditionFailure() + }) + #endif + #endif + self.valueBox.begin() let transaction = Transaction(queue: self.queue, postbox: self) self.afterBegin(transaction: transaction) @@ -3013,6 +3023,12 @@ final class PostboxImpl { postboxLog("Postbox transaction took \(transactionDuration * 1000.0) ms, from: \(file), on:\(line)") } + #if os(macOS) + #if DEBUG || BETA + crashDisposable?.dispose() + #endif + #endif + let _ = self.isInTransaction.swap(false) if let currentUpdatedState = self.currentUpdatedState { diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index 1c1a0be7eb..d0b9fddc61 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -19,8 +19,9 @@ public func printOpenFiles() { var flags: Int32 = 0 var fd: Int32 = 0 var buf = Data(count: Int(MAXPATHLEN) + 1) + let maxFd = min(1024, FD_SETSIZE) - while fd < FD_SETSIZE { + while fd < maxFd { errno = 0; flags = fcntl(fd, F_GETFD, 0); if flags == -1 && errno != 0 { 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/Resources/coin b/submodules/PremiumUI/Resources/coin deleted file mode 100644 index dbb4f92289..0000000000 Binary files a/submodules/PremiumUI/Resources/coin and /dev/null differ diff --git a/submodules/PremiumUI/Resources/coin.scn b/submodules/PremiumUI/Resources/coin.scn new file mode 100644 index 0000000000..3754412983 Binary files /dev/null and b/submodules/PremiumUI/Resources/coin.scn differ 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/PremiumCoinComponent.swift b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift index 24a3c5ec50..f5681e1d6c 100644 --- a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift @@ -8,7 +8,7 @@ import GZip import AppBundle import LegacyComponents -private let sceneVersion: Int = 2 +private let sceneVersion: Int = 3 private func deg2rad(_ number: Float) -> Float { return number * .pi / 180 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 ae057ad6c0..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() } @@ -3243,7 +3260,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { for attribute in emojiFile.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { - let controller = accountContext.sharedContext.makeStickerPackScreen(context: accountContext, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedEmojiPack.flatMap { [$0] } ?? [], parentNavigationController: navigationController, sendSticker: { _, _, _ in + let controller = accountContext.sharedContext.makeStickerPackScreen(context: accountContext, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedEmojiPack.flatMap { [$0] } ?? [], isEditing: false, parentNavigationController: navigationController, sendSticker: { _, _, _ in return false }) presentController(controller) 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/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index fff04dc3e8..edaffa43d0 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1595,6 +1595,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) } }, + editAction: { _ in }, pushController: { _ in }, presentController: { _ in @@ -1728,6 +1729,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1777,6 +1779,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: 3, displayPremiumBadges: false, headerItem: nil, @@ -1836,6 +1839,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1868,6 +1872,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -2561,6 +2566,23 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) } + public func collapse() { + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.tap() + + self.longPressRecognizer?.isEnabled = false + + self.animateFromExtensionDistance = 0.0 + self.extensionDistance = 0.0 + self.visibleExtensionDistance = 0.0 + self.contentTopInset = self.titleLabelHeight ?? 0.0 + self.currentContentHeight = 46.0 + self.isExpanded = false + self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) + } + public func highlightGestureMoved(location: CGPoint, hover: Bool) { if self.allPresetReactionsAreAvailable { return diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 8d91ce2463..439be0fa1f 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -40,6 +40,9 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/UndoUI", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareContentContainerNode.swift b/submodules/ShareController/Sources/ShareContentContainerNode.swift index 57179bcf47..fe14148588 100644 --- a/submodules/ShareController/Sources/ShareContentContainerNode.swift +++ b/submodules/ShareController/Sources/ShareContentContainerNode.swift @@ -8,6 +8,7 @@ public protocol ShareContentContainerNode: AnyObject { func activate() func deactivate() func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) + func setDidBeginDragging(_ f: (() -> Void)?) func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) func updateTheme(_ theme: PresentationTheme) diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 8dde82352f..e77387018c 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -450,6 +450,7 @@ public final class ShareController: ViewController { private let immediatePeerId: PeerId? private let segmentedValues: [ShareControllerSegmentedValue]? private let fromForeignApp: Bool + private let collectibleItemInfo: TelegramCollectibleItemInfo? private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer)>() private let peersDisposable = MetaDisposable() @@ -484,7 +485,7 @@ public final class ShareController: ViewController { public var parentNavigationController: NavigationController? - public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false) { + public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) { self.init( environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), currentContext: ShareControllerAppAccountContext(context: context), @@ -503,11 +504,12 @@ public final class ShareController: ViewController { updatedPresentationData: updatedPresentationData, forceTheme: forceTheme, forcedActionTitle: forcedActionTitle, - shareAsLink: shareAsLink + shareAsLink: shareAsLink, + collectibleItemInfo: collectibleItemInfo ) } - public init(environment: ShareControllerEnvironment, currentContext: ShareControllerAccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [ShareControllerSwitchableAccount] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false) { + public init(environment: ShareControllerEnvironment, currentContext: ShareControllerAccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [ShareControllerSwitchableAccount] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) { self.environment = environment self.currentContext = currentContext self.subject = subject @@ -520,6 +522,7 @@ public final class ShareController: ViewController { self.segmentedValues = segmentedValues self.forceTheme = forceTheme self.shareAsLink = shareAsLink + self.collectibleItemInfo = collectibleItemInfo self.presentationData = updatedPresentationData?.initial ?? environment.presentationData if let forceTheme = self.forceTheme { @@ -717,7 +720,7 @@ public final class ShareController: ViewController { return } strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory) + }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory, collectibleItemInfo: self.collectibleItemInfo) self.controllerNode.completed = self.completed self.controllerNode.enqueued = self.enqueued self.controllerNode.present = { [weak self] c in diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 673cb12c01..b3fd88b49e 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -9,6 +9,11 @@ import TelegramPresentationData import AccountContext import TelegramIntents import ContextUI +import ComponentFlow +import MultilineTextComponent +import TelegramStringFormatting +import BundleIconComponent +import LottieComponent enum ShareState { case preparing(Bool) @@ -21,6 +26,276 @@ enum ShareExternalState { case done } +private final class ShareContentInfoView: UIView { + private struct Params: Equatable { + var environment: ShareControllerEnvironment + var theme: PresentationTheme + var strings: PresentationStrings + var collectibleItemInfo: TelegramCollectibleItemInfo + var availableSize: CGSize + + init(environment: ShareControllerEnvironment, theme: PresentationTheme, strings: PresentationStrings, collectibleItemInfo: TelegramCollectibleItemInfo, availableSize: CGSize) { + self.environment = environment + self.theme = theme + self.strings = strings + self.collectibleItemInfo = collectibleItemInfo + self.availableSize = availableSize + } + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.environment !== rhs.environment { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.collectibleItemInfo != rhs.collectibleItemInfo { + return false + } + if lhs.availableSize != rhs.availableSize { + return false + } + return true + } + } + + private struct Layout { + var params: Params + var size: CGSize + + init(params: Params, size: CGSize) { + self.params = params + self.size = size + } + } + + private let icon = ComponentView() + private let text = ComponentView() + private var currencySymbolIcon: UIImage? + private var arrowIcon: UIImage? + private let backgroundView: BlurredBackgroundView + + private var currentLayout: Layout? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(environment: ShareControllerEnvironment, presentationData: PresentationData, collectibleItemInfo: TelegramCollectibleItemInfo, availableSize: CGSize) -> CGSize { + let params = Params( + environment: environment, + theme: presentationData.theme, + strings: presentationData.strings, + collectibleItemInfo: collectibleItemInfo, + availableSize: availableSize + ) + if let currentLayout = self.currentLayout, currentLayout.params == params { + return currentLayout.size + } + let size = self.updateInternal(params: params) + self.currentLayout = Layout(params: params, size: size) + return size + } + + private func updateInternal(params: Params) -> CGSize { + var username: String = "" + if case let .username(value) = params.collectibleItemInfo.subject { + username = value + } + + let textText = NSMutableAttributedString() + + let dateText = stringForDate(timestamp: params.collectibleItemInfo.purchaseDate, strings: params.strings) + + let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(params.collectibleItemInfo.cryptoCurrencyAmount, currency: params.collectibleItemInfo.cryptoCurrency, customFormat: CurrencyFormatterEntry( + symbol: "~", + thousandsSeparator: ",", + decimalSeparator: ".", + symbolOnLeft: true, + spaceBetweenAmountAndSymbol: false, + decimalDigits: 9 + )) + var cryptoCurrencyText = rawCryptoCurrencyText + while cryptoCurrencyText.hasSuffix("0") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + if cryptoCurrencyText.hasSuffix(".") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + + let (currencyText, currencySign, _) = formatCurrencyAmountCustom(params.collectibleItemInfo.currencyAmount, currency: params.collectibleItemInfo.currency) + + let rawTextString = params.strings.CollectibleItemInfo_UsernameText("@\(username)", params.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(14.0), textColor: .white)) + for range in rawTextString.ranges { + switch range.index { + case 0: + textText.addAttribute(.font, value: Font.semibold(14.0), range: range.range) + case 1: + textText.addAttribute(.font, value: Font.semibold(14.0), range: range.range) + case 3: + textText.addAttribute(.font, value: Font.semibold(14.0), range: range.range) + default: + break + } + } + + let currencySymbolRange = (textText.string as NSString).range(of: "~") + + if self.currencySymbolIcon == nil { + if let templateImage = UIImage(bundleImageName: "Peer Info/CollectibleTonSymbolInline") { + self.currencySymbolIcon = generateImage(CGSize(width: templateImage.size.width, height: templateImage.size.height + 2.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let cgImage = templateImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: templateImage.size.width - 2.0, height: templateImage.size.height - 2.0))) + } + })?.withRenderingMode(.alwaysTemplate) + } + } + + if currencySymbolRange.location != NSNotFound, let currencySymbolIcon = self.currencySymbolIcon { + textText.replaceCharacters(in: currencySymbolRange, with: "$") + textText.addAttribute(.attachment, value: currencySymbolIcon, range: currencySymbolRange) + + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + let font = Font.semibold(14.0) + let runDelegateData = RunDelegateData( + ascent: font.ascender, + descent: font.descender, + width: currencySymbolIcon.size.width + 4.0 + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + textText.addAttribute(NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String), value: runDelegate, range: currencySymbolRange) + } + } + + let accentColor = params.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) + if self.arrowIcon == nil { + if let templateImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") { + let scaleFactor: CGFloat = 0.8 + let imageSize = CGSize(width: floor(templateImage.size.width * scaleFactor), height: floor(templateImage.size.height * scaleFactor)) + self.arrowIcon = generateImage(imageSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let cgImage = templateImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) + } + })?.withRenderingMode(.alwaysTemplate) + } + } + + textText.append(NSAttributedString(string: "\n\(params.strings.CollectibleItemInfo_ShareInlineText_LearnMore)", attributes: [ + .font: Font.medium(14.0), + .foregroundColor: accentColor, + NSAttributedString.Key(rawValue: "URL"): "" + ])) + if let range = textText.string.range(of: ">"), let arrowIcon = self.arrowIcon { + textText.addAttribute(.attachment, value: arrowIcon, range: NSRange(range, in: textText.string)) + } + + let textInsets = UIEdgeInsets(top: 8.0, left: 50.0, bottom: 8.0, right: 10.0) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(textText), + maximumNumberOfLines: 0, + lineSpacing: 0.185, + highlightColor: accentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let params = self.currentLayout?.params else { + return + } + if let environment = params.environment as? ShareControllerAppEnvironment { + environment.sharedContext.applicationBindings.openUrl(params.collectibleItemInfo.url) + } + } + )), + environment: {}, + containerSize: CGSize(width: params.availableSize.width - textInsets.left - textInsets.right, height: 1000.0) + ) + let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.frame = textFrame + } + + let size = CGSize(width: params.availableSize.width, height: textInsets.top + textSize.height + textInsets.bottom) + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "ToastCollectibleUsernameEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((textInsets.left - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.addSubview(iconView) + iconView.playOnce(delay: 0.1) + } + iconView.frame = iconFrame + } + + self.backgroundView.updateColor(color: UIColor(rgb: 0x1C2023), transition: .immediate) + self.backgroundView.update(size: size, cornerRadius: 16.0, transition: .immediate) + self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) + + return size + } +} + final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private weak var controller: ShareController? private let environment: ShareControllerEnvironment @@ -33,6 +308,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let fromForeignApp: Bool private let fromPublicChannel: Bool private let segmentedValues: [ShareControllerSegmentedValue]? + private let collectibleItemInfo: TelegramCollectibleItemInfo? var selectedSegmentedIndex: Int = 0 private let defaultAction: ShareControllerAction? @@ -48,6 +324,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let contentContainerNode: ASDisplayNode private let contentBackgroundNode: ASImageNode + private var contentInfoView: ShareContentInfoView? private var contentNode: (ASDisplayNode & ShareContentContainerNode)? private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)? @@ -90,7 +367,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let showNames = ValuePromise(true) - init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?) { + init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) { self.controller = controller self.environment = environment self.presentationData = presentationData @@ -102,6 +379,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.presentError = presentError self.fromPublicChannel = fromPublicChannel self.segmentedValues = segmentedValues + self.collectibleItemInfo = collectibleItemInfo self.presetText = presetText @@ -156,6 +434,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.contentBackgroundNode.displayWithoutProcessing = true self.contentBackgroundNode.image = roundedBackground + self.contentInfoView = ShareContentInfoView(frame: CGRect()) + self.actionsBackgroundNode = ASImageNode() self.actionsBackgroundNode.isLayerBacked = true self.actionsBackgroundNode.displayWithoutProcessing = true @@ -356,6 +636,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + if let contentInfoView = self.contentInfoView { + self.wrappingScrollNode.view.addSubview(contentInfoView) + } + self.wrappingScrollNode.addSubnode(self.contentContainerNode) self.contentContainerNode.addSubnode(self.actionSeparatorNode) self.contentContainerNode.addSubnode(self.actionsBackgroundNode) @@ -433,12 +717,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } if let searchContentNode = strongSelf.contentNode as? ShareSearchContainerNode { + searchContentNode.setDidBeginDragging(nil) searchContentNode.setContentOffsetUpdated(nil) let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.contentGridNode.scrollView.contentOffset.y if let sourceFrame = searchContentNode.animateOut(peerId: peer.peerId, scrollDelta: scrollDelta) { topicsContentNode.animateIn(sourceFrame: sourceFrame, scrollDelta: scrollDelta) } } else if let peersContentNode = strongSelf.peersContentNode { + peersContentNode.setDidBeginDragging(nil) peersContentNode.setContentOffsetUpdated(nil) let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - peersContentNode.contentGridNode.scrollView.contentOffset.y if let sourceFrame = peersContentNode.animateOut(peerId: peer.peerId, scrollDelta: scrollDelta) { @@ -446,6 +732,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + topicsContentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) topicsContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -459,6 +748,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate guard let topicsContentNode = self.topicsContentNode else { return } + topicsContentNode.setDidBeginDragging(nil) topicsContentNode.setContentOffsetUpdated(nil) if let searchContentNode = self.contentNode as? ShareSearchContainerNode { @@ -472,6 +762,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } if let searchContentNode = self.contentNode as? ShareSearchContainerNode { + searchContentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) searchContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -487,6 +780,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate }) } } else if let peersContentNode = self.peersContentNode { + peersContentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) peersContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -579,6 +875,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate let previous = self.contentNode if let previous = previous { + previous.setDidBeginDragging(nil) previous.setContentOffsetUpdated(nil) if animated { transition = .animated(duration: 0.4, curve: .spring) @@ -597,6 +894,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate previous.removeFromSupernode() self.previousContentNode = nil } + + self.contentNodeDidBeginDragging() } else { transition = .immediate } @@ -607,6 +906,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate contentNode.frame = previous.frame contentNode.updateLayout(size: previous.bounds.size, isLandscape: layout.size.width > layout.size.height, bottomInset: bottomGridInset, transition: .immediate) + contentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -635,6 +937,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } else { if let contentNode = self.contentNode { + contentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -737,6 +1042,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + private func contentNodeDidBeginDragging() { + if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { + Transition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) + Transition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) + } + } + private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { if let (layout, _, _) = self.containerLayout { var insets = layout.insets(options: [.statusBar, .input]) @@ -770,6 +1082,25 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) + if let contentInfoView = self.contentInfoView, let collectibleItemInfo = self.collectibleItemInfo { + let contentInfoSize = contentInfoView.update( + environment: self.environment, + presentationData: self.presentationData, + collectibleItemInfo: collectibleItemInfo, + availableSize: CGSize(width: backgroundFrame.width, height: 1000.0) + ) + let contentInfoFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY - 8.0 - contentInfoSize.height), size: contentInfoSize) + + if contentInfoView.bounds.isEmpty { + if contentInfoFrame.minY < 0.0 { + contentInfoView.alpha = 0.0 + } + } + + transition.updatePosition(layer: contentInfoView.layer, position: contentInfoFrame.center) + transition.updateBounds(layer: contentInfoView.layer, bounds: CGRect(origin: CGPoint(), size: contentInfoFrame.size)) + } + if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset { self.animateContentNodeOffsetFromBackgroundOffset = nil let offset = backgroundFrame.minY - animateContentNodeOffsetFromBackgroundOffset @@ -1019,6 +1350,11 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } self.animatingOut = true + if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { + Transition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) + Transition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) + } + if self.contentNode != nil { var dimCompleted = false var offsetCompleted = false @@ -1223,6 +1559,12 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate return result } if self.bounds.contains(point) { + if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { + if let result = contentInfoView.hitTest(self.view.convert(point, to: contentInfoView), with: event) { + return result + } + } + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { return self.dimNode.view } diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index c643f1ef46..1d6c98dede 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -94,6 +94,9 @@ public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContain public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + public func setDidBeginDragging(_ f: (() -> Void)?) { + } + public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } @@ -308,6 +311,9 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + public func setDidBeginDragging(_ f: (() -> Void)?) { + } + public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 60e45b0d7f..1d4d9439c0 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -126,6 +126,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private let segmentedValues: [ShareControllerSegmentedValue]? + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? var openSearch: (() -> Void)? @@ -297,6 +298,10 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) + + self.contentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) @@ -350,6 +355,10 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.ensurePeerVisibleOnLayout = peerId } + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 3376a93e7d..31c048c033 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -194,6 +194,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { private let searchNode: ShareSearchBarNode private let cancelButtonNode: HighlightableButtonNode + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? var cancel: (() -> Void)? @@ -240,6 +241,14 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.addSubnode(self.cancelButtonNode) self.addSubnode(self.contentSeparatorNode) + self.recentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } + + self.contentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } + self.recentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in if let strongSelf = self, !strongSelf.recentGridNode.isHidden { strongSelf.gridPresentationLayoutUpdated(presentationLayout, transition: transition) @@ -466,6 +475,10 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.ensurePeerVisibleOnLayout = peerId } + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/ShareController/Sources/ShareTopicsContainerNode.swift b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift index 5319df1494..5462d91491 100644 --- a/submodules/ShareController/Sources/ShareTopicsContainerNode.swift +++ b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift @@ -174,6 +174,7 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { private let contentSubtitleNode: ASTextNode private let backNode: CancelButtonNode + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private var validLayout: (CGSize, CGFloat)? @@ -249,6 +250,10 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) + + self.contentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) @@ -286,6 +291,10 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) } } + + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f @@ -458,8 +467,6 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentSubtitleNode.frame = originalSubtitleFrame transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame) - - self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition) } } 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 f09cf1c31d..4cde209664 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -40,6 +40,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/StickerPeekUI:StickerPeekUI", "//submodules/Pasteboard:Pasteboard", + "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index 317605d956..aa2e68337e 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -28,7 +28,7 @@ private struct StickerPackPreviewGridEntry: Comparable, Identifiable { } func item(context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem { - return StickerPackPreviewGridItem(context: context, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isPremium: false, isLocked: false, isEmpty: false) + return StickerPackPreviewGridItem(context: context, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isPremium: false, isLocked: false, isEmpty: false, isEditing: false) } } @@ -144,7 +144,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol super.init() - self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: false, addStickerPack: { _, _ in }, removeStickerPack: { _ in }, emojiSelected: { _, _ in }, emojiLongPressed: { _, _, _, _ in }) + self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: false, addStickerPack: { _, _ in }, removeStickerPack: { _ in }, emojiSelected: { _, _ in }, emojiLongPressed: { _, _, _, _ in }, addPressed: {}) self.backgroundColor = nil self.isOpaque = false diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index 55e78497e7..a50a91f93a 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -16,19 +16,22 @@ import TextFormat final class StickerPackPreviewInteraction { var previewedItem: StickerPreviewPeekItem? + var reorderingFileId: MediaId? var playAnimatedStickers: Bool let addStickerPack: (StickerPackCollectionInfo, [StickerPackItem]) -> Void let removeStickerPack: (StickerPackCollectionInfo) -> Void let emojiSelected: (String, ChatTextInputTextCustomEmojiAttribute) -> Void let emojiLongPressed: (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void + let addPressed: () -> Void - init(playAnimatedStickers: Bool, addStickerPack: @escaping (StickerPackCollectionInfo, [StickerPackItem]) -> Void, removeStickerPack: @escaping (StickerPackCollectionInfo) -> Void, emojiSelected: @escaping (String, ChatTextInputTextCustomEmojiAttribute) -> Void, emojiLongPressed: @escaping (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void) { + init(playAnimatedStickers: Bool, addStickerPack: @escaping (StickerPackCollectionInfo, [StickerPackItem]) -> Void, removeStickerPack: @escaping (StickerPackCollectionInfo) -> Void, emojiSelected: @escaping (String, ChatTextInputTextCustomEmojiAttribute) -> Void, emojiLongPressed: @escaping (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void, addPressed: @escaping () -> Void) { self.playAnimatedStickers = playAnimatedStickers self.addStickerPack = addStickerPack self.removeStickerPack = removeStickerPack self.emojiSelected = emojiSelected self.emojiLongPressed = emojiLongPressed + self.addPressed = addPressed } } @@ -40,10 +43,12 @@ final class StickerPackPreviewGridItem: GridItem { let isPremium: Bool let isLocked: Bool let isEmpty: Bool + let isEditing: Bool + let isAdd: Bool let section: GridSection? = nil - init(context: AccountContext, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isPremium: Bool, isLocked: Bool, isEmpty: Bool) { + init(context: AccountContext, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isPremium: Bool, isLocked: Bool, isEmpty: Bool, isEditing: Bool, isAdd: Bool = false) { self.context = context self.stickerItem = stickerItem self.interaction = interaction @@ -51,11 +56,13 @@ final class StickerPackPreviewGridItem: GridItem { self.isPremium = isPremium self.isLocked = isLocked self.isEmpty = isEmpty + self.isEditing = isEditing + self.isAdd = isAdd } func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = StickerPackPreviewGridItemNode() - node.setup(context: self.context, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isLocked: self.isLocked, isPremium: self.isPremium, isEmpty: self.isEmpty) + node.setup(context: self.context, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isLocked: self.isLocked, isPremium: self.isPremium, isEmpty: self.isEmpty, isEditing: self.isEditing, isAdd: self.isAdd) return node } @@ -64,17 +71,18 @@ final class StickerPackPreviewGridItem: GridItem { assertionFailure() return } - node.setup(context: self.context, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isLocked: self.isLocked, isPremium: self.isPremium, isEmpty: self.isEmpty) + node.setup(context: self.context, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isLocked: self.isLocked, isPremium: self.isPremium, isEmpty: self.isEmpty, isEditing: self.isEditing, isAdd: self.isAdd) } } private let textFont = Font.regular(20.0) final class StickerPackPreviewGridItemNode: GridItemNode { - private var currentState: (AccountContext, StickerPackItem?)? + private var currentState: (AccountContext, StickerPackItem?, Bool, Bool)? private var isLocked: Bool? private var isPremium: Bool? private var isEmpty: Bool? + private let containerNode: ASDisplayNode private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? private var placeholderNode: StickerShimmerEffectNode @@ -85,6 +93,8 @@ final class StickerPackPreviewGridItemNode: GridItemNode { private var theme: PresentationTheme? + private var isEditing = false + override var isVisibleInGrid: Bool { didSet { let visibility = self.isVisibleInGrid && (self.interaction?.playAnimatedStickers ?? true) @@ -103,14 +113,18 @@ final class StickerPackPreviewGridItemNode: GridItemNode { private let effectFetchedDisposable = MetaDisposable() var interaction: StickerPackPreviewInteraction? - - var selected: (() -> Void)? - + var stickerPackItem: StickerPackItem? { return self.currentState?.1 } + var isAdd: Bool { + return self.currentState?.2 == true + } + override init() { + self.containerNode = ASDisplayNode() + self.imageNode = TransformImageNode() self.imageNode.isLayerBacked = !smartInvertColorsEnabled() self.placeholderNode = StickerShimmerEffectNode() @@ -118,8 +132,9 @@ final class StickerPackPreviewGridItemNode: GridItemNode { super.init() - self.addSubnode(self.imageNode) - self.addSubnode(self.placeholderNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.placeholderNode) var firstTime = true self.imageNode.imageUpdated = { [weak self] image in @@ -172,15 +187,62 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) } + @objc private func handleAddTap() { + self.interaction?.addPressed() + } + private var setupTimestamp: Double? - func setup(context: AccountContext, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isLocked: Bool, isPremium: Bool, isEmpty: Bool) { + func setup(context: AccountContext, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isLocked: Bool, isPremium: Bool, isEmpty: Bool, isEditing: Bool, isAdd: Bool) { self.interaction = interaction self.theme = theme + let isFirstTime = self.currentState == nil + if isAdd { + if !isFirstTime { + return + } + + let color = theme.actionSheet.controlAccentColor + self.imageNode.setSignal(.single({ arguments in + let drawingContext = DrawingContext(size: arguments.imageSize, opaque: false) + let size = arguments.imageSize + drawingContext?.withContext({ context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + UIGraphicsPushContext(context) + + context.setFillColor(color.withMultipliedAlpha(0.1).cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0)) + context.setFillColor(color.cgColor) + + let plusSize = CGSize(width: 3.0, height: 21.0) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath) + context.fillPath() + + UIGraphicsPopContext() + }) + return drawingContext + })) + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleAddTap))) + + self.currentState = (context, nil, true, false) + self.setNeedsLayout() + + return + } + + if interaction.reorderingFileId != nil { + self.isHidden = stickerItem?.file.fileId == interaction.reorderingFileId + } else { + self.isHidden = false + } + if self.currentState == nil || self.currentState!.0 !== context || self.currentState!.1 != stickerItem || self.isLocked != isLocked || self.isPremium != isPremium || self.isEmpty != isEmpty { self.isLocked = isLocked - - if isLocked { + + if isLocked || isEditing { let lockBackground: UIVisualEffectView let lockIconNode: ASImageNode if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode { @@ -198,7 +260,24 @@ final class StickerPackPreviewGridItemNode: GridItemNode { lockBackground.isUserInteractionEnabled = false lockIconNode = ASImageNode() lockIconNode.displaysAsynchronously = false - lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) + + if isEditing { + lockIconNode.image = generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + + context.addEllipse(in: CGRect(x: 5.5, y: 11.0, width: 3.0, height: 3.0)) + context.fillPath() + + context.addEllipse(in: CGRect(x: size.width / 2.0 - 1.5, y: 11.0, width: 3.0, height: 3.0)) + context.fillPath() + + context.addEllipse(in: CGRect(x: size.width - 3.0 - 5.5, y: 11.0, width: 3.0, height: 3.0)) + context.fillPath() + }) + } else { + lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) + } let lockTintView = UIView() lockTintView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15) @@ -209,15 +288,21 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.lockIconNode = lockIconNode self.view.addSubview(lockBackground) - self.addSubnode(lockIconNode) + lockBackground.contentView.addSubview(lockIconNode.view) + + if !isFirstTime { + lockBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } } - } else if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { + } else if let lockBackground = self.lockBackground { self.lockBackground = nil self.lockTintView = nil self.lockIconNode = nil - lockBackground.removeFromSuperview() - lockTintView.removeFromSuperview() - lockIconNode.removeFromSupernode() + + lockBackground.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + lockBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in + lockBackground.removeFromSuperview() + }) } if let stickerItem = stickerItem { @@ -237,7 +322,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { if self.animationNode == nil { let animationNode = DefaultAnimatedStickerNodeImpl() self.animationNode = animationNode - self.insertSubnode(animationNode, aboveSubnode: self.imageNode) + self.containerNode.insertSubnode(animationNode, aboveSubnode: self.imageNode) animationNode.started = { [weak self] in guard let strongSelf = self else { return @@ -287,21 +372,85 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.animationNode?.alpha = isLocked ? 0.5 : 1.0 self.imageNode.alpha = isLocked ? 0.5 : 1.0 - self.currentState = (context, stickerItem) + self.currentState = (context, stickerItem, false, isEditing) self.setNeedsLayout() } self.isEmpty = isEmpty + + if self.isEditing != isEditing { + self.isEditing = isEditing + if self.isEditing { + self.startShaking() + } else { + self.containerNode.layer.removeAnimation(forKey: "shaking_position") + self.containerNode.layer.removeAnimation(forKey: "shaking_rotation") + } + } + } + + private func startShaking() { + func degreesToRadians(_ x: CGFloat) -> CGFloat { + return .pi * x / 180.0 + } + + let duration: Double = 0.4 + let displacement: CGFloat = 1.0 + let degreesRotation: CGFloat = 2.0 + + let negativeDisplacement = -1.0 * displacement + let position = CAKeyframeAnimation.init(keyPath: "position") + position.beginTime = 0.8 + position.duration = duration + position.values = [ + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: 0, y: 0)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), + NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) + ] + position.calculationMode = .linear + position.isRemovedOnCompletion = false + position.repeatCount = Float.greatestFiniteMagnitude + position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + position.isAdditive = true + + let transform = CAKeyframeAnimation.init(keyPath: "transform") + transform.beginTime = 2.6 + transform.duration = 0.3 + transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) + transform.values = [ + degreesToRadians(-1.0 * degreesRotation), + degreesToRadians(degreesRotation), + degreesToRadians(-1.0 * degreesRotation) + ] + transform.calculationMode = .linear + transform.isRemovedOnCompletion = false + transform.repeatCount = Float.greatestFiniteMagnitude + transform.isAdditive = true + transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + + self.containerNode.layer.add(position, forKey: "shaking_position") + self.containerNode.layer.add(transform, forKey: "shaking_rotation") } override func layout() { super.layout() let bounds = self.bounds + self.containerNode.frame = bounds + let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0) var boundingSize = CGSize(width: boundsSide, height: boundsSide) - if let (_, item) = self.currentState { - if let item = item, let dimensions = item.file.dimensions?.cgSize { + if let (_, item, isAdd, _) = self.currentState { + if isAdd { + let imageSize = CGSize(width: 512, height: 512).aspectFitted(boundingSize) + let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = imageFrame + + return + } else if let item = item, let dimensions = item.file.dimensions?.cgSize { if item.file.isPremiumSticker { boundingSize = CGSize(width: boundingSize.width * 1.1, height: boundingSize.width * 1.1) } @@ -322,13 +471,20 @@ final class StickerPackPreviewGridItemNode: GridItemNode { let placeholderFrame = imageFrame self.placeholderNode.frame = imageFrame - if let theme = self.theme, let (context, stickerItem) = self.currentState, let item = stickerItem { + if let theme = self.theme, let (context, stickerItem, _, _) = self.currentState, let item = stickerItem { self.placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size, enableEffect: context.sharedContext.energyUsageSettings.fullTranslucency) } if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { - let lockSize = CGSize(width: 16.0, height: 16.0) - let lockBackgroundFrame = CGRect(origin: CGPoint(x: bounds.width - lockSize.width, y: bounds.height - lockSize.height), size: lockSize) + let lockSize: CGSize + let lockBackgroundFrame: CGRect + if let (_, _, _, isEditing) = self.currentState, isEditing { + lockSize = CGSize(width: 24.0, height: 24.0) + lockBackgroundFrame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: lockSize) + } else { + lockSize = CGSize(width: 16.0, height: 16.0) + lockBackgroundFrame = CGRect(origin: CGPoint(x: bounds.width - lockSize.width, y: bounds.height - lockSize.height), size: lockSize) + } lockBackground.frame = lockBackgroundFrame lockBackground.layer.cornerRadius = lockSize.width / 2.0 if #available(iOS 13.0, *) { @@ -337,7 +493,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { lockTintView.frame = CGRect(origin: CGPoint(), size: lockBackgroundFrame.size) if let icon = lockIconNode.image { let iconSize = CGSize(width: icon.size.width - 4.0, height: icon.size.height - 4.0) - lockIconNode.frame = CGRect(origin: CGPoint(x: lockBackgroundFrame.minX + floorToScreenPixels((lockBackgroundFrame.width - iconSize.width) / 2.0), y: lockBackgroundFrame.minY + floorToScreenPixels((lockBackgroundFrame.height - iconSize.height) / 2.0)), size: iconSize) + lockIconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((lockBackgroundFrame.width - iconSize.width) / 2.0), y: floorToScreenPixels((lockBackgroundFrame.height - iconSize.height) / 2.0)), size: iconSize) } } } @@ -355,7 +511,10 @@ final class StickerPackPreviewGridItemNode: GridItemNode { func updatePreviewing(animated: Bool) { var isPreviewing = false - if let (_, maybeItem) = self.currentState, let interaction = self.interaction, let item = maybeItem { + if let (_, maybeItem, isAdd, _) = self.currentState, let interaction = self.interaction, let item = maybeItem { + if isAdd { + return + } isPreviewing = interaction.previewedItem == .pack(item.file) } if self.currentIsPreviewing != isPreviewing { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 53e973f912..a2058cde6a 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -22,26 +22,33 @@ import StickerPeekUI 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) + case sticker(index: Int, stableId: Int, stickerItem: StickerPackItem?, isEmpty: Bool, isPremium: Bool, isLocked: Bool, isEditing: Bool, isAdd: Bool) + case add case emojis(index: Int, stableId: Int, info: StickerPackCollectionInfo, items: [StickerPackItem], title: String?, isInstalled: Bool?) var stableId: Int { switch self { - case let .sticker(_, stableId, _, _, _, _): - return stableId - case let .emojis(_, stableId, _, _, _, _): - return stableId + case let .sticker(_, stableId, _, _, _, _, _, _): + return stableId + case .add: + return -1 + case let .emojis(_, stableId, _, _, _, _): + return stableId } } var index: Int { switch self { - case let .sticker(index, _, _, _, _, _): - return index - case let .emojis(index, _, _, _, _, _): - return index + case let .sticker(index, _, _, _, _, _, _, _): + return index + case .add: + return 100000 + case let .emojis(index, _, _, _, _, _): + return index } } @@ -49,12 +56,14 @@ private enum StickerPackPreviewGridEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) -> GridItem { + func item(context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isEditing: Bool) -> GridItem { switch self { - case let .sticker(_, _, stickerItem, isEmpty, isPremium, isLocked): - return StickerPackPreviewGridItem(context: context, stickerItem: stickerItem, interaction: interaction, theme: theme, isPremium: isPremium, isLocked: isLocked, isEmpty: isEmpty) - case let .emojis(_, _, info, items, title, isInstalled): - return StickerPackEmojisItem(context: context, animationCache: animationCache, animationRenderer: animationRenderer, interaction: interaction, info: info, items: items, theme: theme, strings: strings, title: title, isInstalled: isInstalled, isEmpty: false) + case let .sticker(_, _, stickerItem, isEmpty, isPremium, isLocked, _, isAdd): + return StickerPackPreviewGridItem(context: context, stickerItem: stickerItem, interaction: interaction, theme: theme, isPremium: isPremium, isLocked: isLocked, isEmpty: isEmpty, isEditing: isEditing, isAdd: isAdd) + case .add: + return StickerPackPreviewGridItem(context: context, stickerItem: nil, interaction: interaction, theme: theme, isPremium: false, isLocked: false, isEmpty: false, isEditing: false, isAdd: true) + case let .emojis(_, _, info, items, title, isInstalled): + return StickerPackEmojisItem(context: context, animationCache: animationCache, animationRenderer: animationRenderer, interaction: interaction, info: info, items: items, theme: theme, strings: strings, title: title, isInstalled: isInstalled, isEmpty: false) } } } @@ -65,15 +74,30 @@ private struct StickerPackPreviewGridTransaction { let updates: [GridNodeUpdateItem] let scrollToItem: GridNodeScrollToItem? - init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, scrollToItem: GridNodeScrollToItem?) { + init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, scrollToItem: GridNodeScrollToItem?, isEditing: Bool) { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices - self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer), previousIndex: $0.2) } - self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer)) } + self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer, isEditing: isEditing), previousIndex: $0.2) } + self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer, isEditing: isEditing)) } self.scrollToItem = scrollToItem } + + init(list: [StickerPackPreviewGridEntry], context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, scrollToItem: GridNodeScrollToItem?, isEditing: Bool) { + self.deletions = [] + self.insertions = [] + + var index = 0 + var updates: [GridNodeUpdateItem] = [] + for i in 0 ..< list.count { + updates.append(GridNodeUpdateItem(index: i, previousIndex: i, item: list[i].item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer, isEditing: isEditing))) + index += 1 + } + self.updates = updates + + self.scrollToItem = nil + } } private enum StickerPackAction { @@ -119,6 +143,8 @@ private final class StickerPackContainer: ASDisplayNode { private var currentEntries: [StickerPackPreviewGridEntry] = [] private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = [] + private var updatedTitle: String? + private var itemsDisposable: Disposable? private var currentContents: [LoadedStickerPack]? private(set) var currentStickerPack: (StickerPackCollectionInfo, [StickerPackItem], Bool)? @@ -174,6 +200,7 @@ private final class StickerPackContainer: ASDisplayNode { self.expandProgressUpdated = expandProgressUpdated self.sendSticker = sendSticker self.sendEmoji = sendEmoji + self.isEditing = controller?.initialIsEditing ?? false self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = true @@ -221,6 +248,7 @@ private final class StickerPackContainer: ASDisplayNode { var removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)? var emojiSelectedImpl: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? var emojiLongPressedImpl: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)? + var addPressedImpl: (() -> Void)? self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true, addStickerPack: { info, items in addStickerPackImpl?(info, items) }, removeStickerPack: { info in @@ -229,6 +257,8 @@ private final class StickerPackContainer: ASDisplayNode { emojiSelectedImpl?(text, attribute) }, emojiLongPressed: { text, attribute, node, frame in emojiLongPressedImpl?(text, attribute, node, frame) + }, addPressed: { + addPressedImpl?() }) super.init() @@ -435,18 +465,29 @@ private final class StickerPackContainer: ASDisplayNode { emojiLongPressedImpl = { text, attribute, node, frame in longPressEmoji?(text, attribute, node, frame) } + + addPressedImpl = { [weak self] in + self?.presentAddStickerOptions() + } } deinit { self.itemsDisposable?.dispose() } + private var peekGestureRecognizer: PeekControllerGestureRecognizer? + private var reorderingGestureRecognizer: ReorderingGestureRecognizer? override func didLoad() { super.didLoad() - self.gridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in + let peekGestureRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in if let strongSelf = self { if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { + var canEdit = false + if let (info, _, _) = strongSelf.currentStickerPack, info.flags.contains(.isCreator) { + canEdit = true + } + let accountPeerId = strongSelf.context.account.peerId return combineLatest( strongSelf.context.engine.stickers.isStickerSaved(id: item.file.fileId), @@ -493,6 +534,47 @@ 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 { + self.openEditSticker(item.file) + } + }))) + if !strongSelf.isEditing { + menuItems.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + if let self { + self.updateIsEditing(true) + } + }))) + } + menuItems.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in + if let _ = self { + let contextItems: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: "Back", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c ,f in + c.popItems() + })), + .separator, + .action(ContextMenuActionItem(text: "Delete for Everyone", textColor: .destructive, icon: { _ in return nil }, action: { [weak self] _ ,f in + f(.default) + + if let self, let (info, items, installed) = self.currentStickerPack { + let updatedItems = items.filter { $0.file.fileId != item.file.fileId } + self.currentStickerPack = (info, updatedItems, installed) + self.updateEntries() + + let _ = self.context.engine.stickers.deleteStickerFromStickerSet(sticker: .stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: item.file)).startStandalone() + } + })) + ] + c.pushItems(items: .single(ContextController.Items(content: .list(contextItems)))) + } + }))) + } } return (itemNode.view, itemNode.bounds, StickerPreviewPeekContent(context: strongSelf.context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item.file), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { [weak self] in guard let strongSelf = self else { @@ -535,7 +617,129 @@ private final class StickerPackContainer: ASDisplayNode { } strongSelf.updatePreviewingItem(item: item, animated: true) } - }, activateBySingleTap: true)) + }, activateBySingleTap: true) + peekGestureRecognizer.longPressEnabled = !self.isEditing + self.peekGestureRecognizer = peekGestureRecognizer + self.gridNode.view.addGestureRecognizer(peekGestureRecognizer) + + let reorderingGestureRecognizer = ReorderingGestureRecognizer(animateOnTouch: false, shouldBegin: { [weak self] point in + if let strongSelf = self, !strongSelf.gridNode.scrollView.isDragging && strongSelf.currentEntries.count > 1 { + if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, !itemNode.isAdd { + return (true, true, itemNode) + } + return (false, false, nil) + } + return (false, false, nil) + }, willBegin: { _ in + + }, began: { [weak self] itemNode in + self?.beginReordering(itemNode: itemNode) + }, ended: { [weak self] point in + if let strongSelf = self { + if let point = point { + strongSelf.endReordering(point: point) + } else { + strongSelf.endReordering(point: nil) + } + } + }, moved: { [weak self] point, offset in + self?.updateReordering(point: point, offset: offset) + }) + reorderingGestureRecognizer.isEnabled = self.isEditing + self.reorderingGestureRecognizer = reorderingGestureRecognizer + self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer) + } + + private var reorderFeedback: HapticFeedback? + private var reorderNode: ReorderingItemNode? + + private var isReordering = false + private var reorderPosition: Int? + + private func beginReordering(itemNode: StickerPackPreviewGridItemNode) { + self.isReordering = true + + if let reorderNode = self.reorderNode { + reorderNode.removeFromSupernode() + } + + self.interaction.reorderingFileId = itemNode.stickerPackItem?.file.fileId + + let reorderNode = ReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin) + self.reorderNode = reorderNode + self.gridNode.addSubnode(reorderNode) + + itemNode.isHidden = true + + if self.reorderFeedback == nil { + self.reorderFeedback = HapticFeedback() + } + self.reorderFeedback?.impact() + } + + private func endReordering(point: CGPoint?) { + self.interaction.reorderingFileId = nil + + if let reorderNode = self.reorderNode { + self.reorderNode = nil + + if let itemNode = reorderNode.itemNode, let _ = point { +// var targetNode: StickerPackPreviewGridItemNode? +// if let itemNode = self.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode { +// targetNode = itemNode +// } + +// let _ = itemNode +// let _ = targetNode +// if let targetNode = targetNode, let sourceItem = itemNode.asset as? TGMediaSelectableItem, let targetItem = targetNode.asset as? TGMediaSelectableItem, let targetIndex = self.interaction?.selectionState?.index(of: targetItem) { +// self.interaction?.selectionState?.move(sourceItem, to: targetIndex) +// } + reorderNode.animateCompletion(completion: { [weak reorderNode] in + reorderNode?.removeFromSupernode() + }) + self.reorderFeedback?.tap() + + if let reorderPosition = self.reorderPosition, let file = itemNode.stickerPackItem?.file { + let _ = self.context.engine.stickers.reorderSticker(sticker: .standalone(media: file), position: reorderPosition).startStandalone() + } + } else { + reorderNode.removeFromSupernode() + reorderNode.itemNode?.isHidden = false + } + + self.updateEntries(reload: true) + } + + self.isReordering = false + self.reorderPosition = nil + } + + private func updateReordering(point: CGPoint, offset: CGPoint) { + if let reorderNode = self.reorderNode { + reorderNode.updateOffset(offset: offset) + + var targetNode: StickerPackPreviewGridItemNode? + if let itemNode = self.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode { + targetNode = itemNode + } + + var reorderPosition: Int? + if targetNode !== reorderNode.itemNode { + var index = 0 + for entry in self.currentEntries { + if case let .sticker(_, _, item, _, _, _, _, _) = entry, item?.file.fileId == targetNode?.stickerPackItem?.file.fileId { + reorderPosition = index + break + } + index += 1 + } + } + + if self.reorderPosition != reorderPosition { + self.reorderPosition = reorderPosition + self.updateEntries() + } + } } private func emojiSuggestionPeekContent(itemLayer: CALayer, file: TelegramMediaFile) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError> { @@ -623,7 +827,7 @@ private final class StickerPackContainer: ASDisplayNode { let context = self.context - let _ = context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).start() + let _ = context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).startStandalone() var animateInAsReplacement = false animateInAsReplacement = false @@ -722,7 +926,7 @@ private final class StickerPackContainer: ASDisplayNode { return (strongSelf.view, itemLayer.convert(itemLayer.bounds, to: strongSelf.view.layer), content) } } - + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData @@ -750,8 +954,12 @@ private final class StickerPackContainer: ASDisplayNode { buttonColor = .clear case .none: buttonColor = self.presentationData.theme.list.itemAccentColor - case let .result(_, _, installed): - buttonColor = installed ? self.presentationData.theme.list.itemDestructiveColor : self.presentationData.theme.list.itemCheckColors.foregroundColor + case let .result(info, _, installed): + if info.flags.contains(.isCreator) { + buttonColor = installed ? self.presentationData.theme.list.itemAccentColor : self.presentationData.theme.list.itemCheckColors.foregroundColor + } else { + buttonColor = installed ? self.presentationData.theme.list.itemDestructiveColor : self.presentationData.theme.list.itemCheckColors.foregroundColor + } if installed { buttonFont = Font.regular(17.0) } @@ -761,13 +969,12 @@ private final class StickerPackContainer: ASDisplayNode { self.buttonNode.setTitle(self.buttonNode.attributedTitle(for: .normal)?.string ?? "", with: buttonFont, with: buttonColor, for: .normal) } - if !self.currentEntries.isEmpty, let controller = self.controller { - let transaction = StickerPackPreviewGridTransaction(previousList: self.currentEntries, list: self.currentEntries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: nil) - self.enqueueTransaction(transaction) + if !self.currentEntries.isEmpty { + self.updateEntries() } let titleFont = Font.semibold(17.0) - let title = self.titleNode.attributedText?.string ?? "" + let title = self.updatedTitle ?? (self.titleNode.attributedText?.string ?? "") let entities = generateTextEntities(title, enabledTypes: [.mention]) self.titleNode.attributedText = stringWithAppliedEntities(title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil) @@ -777,6 +984,31 @@ private final class StickerPackContainer: ASDisplayNode { } } + private var isEditing = false + func updateEntries(reload: Bool = false) { + guard let controller = self.controller else { + return + } + let transaction: StickerPackPreviewGridTransaction + if reload { + transaction = StickerPackPreviewGridTransaction(list: self.currentEntries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: nil, isEditing: self.isEditing) + } else { + transaction = StickerPackPreviewGridTransaction(previousList: self.currentEntries, list: self.currentEntries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: nil, isEditing: self.isEditing) + } + self.enqueueTransaction(transaction) + } + + private func updateIsEditing(_ isEditing: Bool) { + self.isEditing = isEditing + self.updateEntries(reload: true) + self.updateButton() + self.peekGestureRecognizer?.longPressEnabled = !isEditing + self.reorderingGestureRecognizer?.isEnabled = isEditing + if let (layout, _, _, _) = self.validLayout { + self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { guard let controller = self.controller else { return @@ -844,10 +1076,288 @@ private final class StickerPackContainer: ASDisplayNode { } }))) + if let (info, _, _) = self.currentStickerPack, info.flags.contains(.isCreator) { + //TODO:localize + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + self?.updateIsEditing(true) + }))) + + items.append(.action(ContextMenuActionItem(text: "Edit Name", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + self?.presentEditPackTitle() + }))) + + items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] c, f in + if let self, let (_, _, isInstalled) = self.currentStickerPack { + if isInstalled { + let contextItems: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: "Delete for Everyone", textColor: .destructive, icon: { _ in return nil }, action: { [weak self] _ ,f in + f(.default) + + self?.presentDeletePack() + })), + .action(ContextMenuActionItem(text: "Remove for Me", icon: { _ in return nil }, action: { [weak self] _ ,f in + f(.default) + + self?.togglePackInstalled() + })) + ] + c.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) + } else { + f(.default) + self.presentDeletePack() + } + } + }))) + + items.append(.separator) + + items.append(.action(ContextMenuActionItem(text: "Check [@stickers]() bot for more options.", textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in + return nil + }, action: { [weak self] _, f in + f(.default) + + guard let self, let controller = self.controller else { + return + } + + controller.controllerNode.openMention("stickers") + }))) + } + let contextController = ContextController(presentationData: self.presentationData, source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.presentInGlobalOverlay(contextController, nil) } + private let stickerPickerInputData = Promise() + private func presentAddStickerOptions() { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: "Create a New Sticker", color: .accent, action: { [weak actionSheet, weak self] in + actionSheet?.dismissAnimated() + + guard let self, let controller = self.controller else { + return + } + self.presentCreateSticker() + controller.controllerNode.dismiss() + })) + items.append(ActionSheetButtonItem(title: "Add an Existing Sticker", color: .accent, action: { [weak actionSheet, weak self] in + actionSheet?.dismissAnimated() + + guard let self, let controller = self.controller else { + return + } + 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 + actionSheet?.dismissAnimated() + }) + ])]) + 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() { + 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: {} + ) + 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() { + guard let (info, _, _) = self.currentStickerPack else { + return + } + let context = self.context + //TODO:localize + var dismissImpl: (() -> Void)? + let controller = stickerPackEditTitleController(context: context, title: "Edit Sticker Set Name", text: "Choose a new name for your set.", placeholder: self.presentationData.strings.ImportStickerPack_NamePlaceholder, actionTitle: presentationData.strings.Common_Done, value: self.updatedTitle ?? info.title, maxLength: 128, apply: { [weak self] title in + guard let self, let title else { + return + } + let _ = (context.engine.stickers.renameStickerSet(packReference: .id(id: info.id.id, accessHash: info.accessHash), title: title) + |> deliverOnMainQueue).startStandalone() + + self.updatedTitle = title + self.updatePresentationData(self.presentationData) + + dismissImpl?() + }, cancel: {}) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + self.controller?.present(controller, in: .window(.root)) + } + + private func presentDeletePack() { + guard let controller = self.controller, let (info, _, _) = self.currentStickerPack else { + return + } + let context = self.context + controller.present(textAlertController(context: context, updatedPresentationData: controller.updatedPresentationData, title: "Delete Sticker Set", text: "This will delete the sticker set for all users.", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: "Delete", action: { [weak self] in + let _ = (context.engine.stickers.deleteStickerSet(packReference: .id(id: info.id.id, accessHash: info.accessHash)) + |> deliverOnMainQueue).startStandalone() + + self?.controller?.controllerNode.dismiss() + })]), in: .window(.root)) + } + @objc func cancelPressed() { self.requestDismiss() } @@ -896,7 +1406,19 @@ private final class StickerPackContainer: ASDisplayNode { self.controller?.actionPerformed?(installedPacks) } self.requestDismiss() - } else if let (info, items, installed) = self.currentStickerPack { + } else if let (info, _, installed) = self.currentStickerPack { + if installed, info.flags.contains(.isCreator) { + self.updateIsEditing(!self.isEditing) + return + } + self.togglePackInstalled() + } else { + self.requestDismiss() + } + } + + private func togglePackInstalled() { + if let (info, items, installed) = self.currentStickerPack { var dismissed = false switch self.decideNextAction(self, installed ? .remove : .add) { case .dismiss: @@ -923,8 +1445,6 @@ private final class StickerPackContainer: ASDisplayNode { actionPerformed?([(info, items, .add)]) } } - } else { - self.requestDismiss() } } @@ -958,6 +1478,74 @@ private final class StickerPackContainer: ASDisplayNode { transition.updateAlpha(node: self.actionAreaSeparatorNode, alpha: backgroundAlpha, delay: delay) } + private func updateButton(count: Int32 = 0) { + if let currentContents = self.currentContents, currentContents.count == 1, let content = currentContents.first, case let .result(info, _, installed) = content { + if installed { + let text: String + if info.flags.contains(.isCreator) { + if self.isEditing { + var updated = false + if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != self.presentationData.strings.Common_Done { + updated = true + } + + if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.buttonNode.view.frame + self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + self.buttonNode.setTitle(self.presentationData.strings.Common_Done, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) + self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) + } else { + var updated = false + if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != "Edit Stickers" { + updated = true + } + + if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.buttonNode.view.frame + self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + //TODO:localize + text = "Edit Stickers" + self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) + self.buttonNode.setBackgroundImage(nil, for: []) + } + } else { + if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + text = self.presentationData.strings.StickerPack_RemoveStickerCount(count) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + text = self.presentationData.strings.StickerPack_RemoveEmojiCount(count) + } else { + text = self.presentationData.strings.StickerPack_RemoveMaskCount(count) + } + self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal) + self.buttonNode.setBackgroundImage(nil, for: []) + } + } else { + let text: String + if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + text = self.presentationData.strings.StickerPack_AddStickerCount(count) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + text = self.presentationData.strings.StickerPack_AddEmojiCount(count) + } else { + text = self.presentationData.strings.StickerPack_AddMaskCount(count) + } + self.buttonNode.setTitle(text, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) + self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) + } + } + } + private func updateStickerPackContents(_ contents: [LoadedStickerPack], hasPremium: Bool) { self.currentContents = contents self.didReceiveStickerPackResult = true @@ -1045,7 +1633,7 @@ private final class StickerPackContainer: ASDisplayNode { for _ in 0 ..< 16 { var stableId: Int? inner: for entry in self.currentEntries { - if case let .sticker(index, currentStableId, stickerItem, _, _, _) = entry, stickerItem == nil, index == entries.count { + if case let .sticker(index, currentStableId, stickerItem, _, _, _, _, _) = entry, stickerItem == nil, index == entries.count { stableId = currentStableId break inner } @@ -1060,7 +1648,7 @@ private final class StickerPackContainer: ASDisplayNode { } self.nextStableId += 1 - entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: false, isPremium: false, isLocked: false)) + entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: false, isPremium: false, isLocked: false, isEditing: false, isAdd: false)) } if self.titlePlaceholderNode == nil { let titlePlaceholderNode = ShimmerEffectNode() @@ -1112,7 +1700,7 @@ private final class StickerPackContainer: ASDisplayNode { let addItem: (StickerPackItem, Bool, Bool) -> Void = { item, isPremium, isLocked in var stableId: Int? inner: for entry in self.currentEntries { - if case let .sticker(_, currentStableId, stickerItem, _, _, _) = entry, let stickerItem = stickerItem, stickerItem.file.fileId == item.file.fileId { + if case let .sticker(_, currentStableId, stickerItem, _, _, _, _, _) = entry, let stickerItem = stickerItem, stickerItem.file.fileId == item.file.fileId { stableId = currentStableId break inner } @@ -1124,7 +1712,7 @@ private final class StickerPackContainer: ASDisplayNode { resolvedStableId = self.nextStableId self.nextStableId += 1 } - entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false, isPremium: isPremium, isLocked: isLocked)) + entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false, isPremium: isPremium, isLocked: isLocked, isEditing: false, isAdd: false)) } for item in generalItems { @@ -1142,41 +1730,21 @@ private final class StickerPackContainer: ASDisplayNode { if let mainActionTitle = self.controller?.mainActionTitle { self.buttonNode.setTitle(mainActionTitle, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) - let roundedAccentBackground = generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) - })?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 11) - self.buttonNode.setBackgroundImage(roundedAccentBackground, for: []) + self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) } else { - if installed { - let text: String - if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { - text = self.presentationData.strings.StickerPack_RemoveStickerCount(Int32(entries.count)) - } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { - text = self.presentationData.strings.StickerPack_RemoveEmojiCount(Int32(items.count)) - } else { - text = self.presentationData.strings.StickerPack_RemoveMaskCount(Int32(entries.count)) - } - self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal) - self.buttonNode.setBackgroundImage(nil, for: []) + let count: Int32 + if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + count = Int32(entries.count) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + count = Int32(items.count) } else { - let text: String - if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { - text = self.presentationData.strings.StickerPack_AddStickerCount(Int32(entries.count)) - } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { - text = self.presentationData.strings.StickerPack_AddEmojiCount(Int32(items.count)) - } else { - text = self.presentationData.strings.StickerPack_AddMaskCount(Int32(entries.count)) - } - self.buttonNode.setTitle(text, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) - let roundedAccentBackground = generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(self.presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) - })?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 11) - self.buttonNode.setBackgroundImage(roundedAccentBackground, for: []) + count = Int32(entries.count) } + self.updateButton(count: count) + } + + if info.flags.contains(.isCreator) { + entries.append(.add) } } } @@ -1197,7 +1765,86 @@ private final class StickerPackContainer: ASDisplayNode { } if let controller = self.controller { - let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: scrollToItem) + let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: scrollToItem, isEditing: self.isEditing) + self.enqueueTransaction(transaction) + } + } + + func updateEntries() { + guard let (_, items, _) = self.currentStickerPack else { + return + } + let hasPremium = self.context.isPremium + let previousEntries = self.currentEntries + var entries: [StickerPackPreviewGridEntry] = [] + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + + var generalItems: [StickerPackItem] = [] + var premiumItems: [StickerPackItem] = [] + + for item in items { + if item.file.isPremiumSticker { + premiumItems.append(item) + } else { + generalItems.append(item) + } + } + + let addItem: (StickerPackItem, Bool, Bool) -> Void = { item, isPremium, isLocked in + var stableId: Int? + inner: for entry in self.currentEntries { + if case let .sticker(_, currentStableId, stickerItem, _, _, _, _, _) = entry, let stickerItem = stickerItem, stickerItem.file.fileId == item.file.fileId { + stableId = currentStableId + break inner + } + } + let resolvedStableId: Int + if let stableId = stableId { + resolvedStableId = stableId + } else { + resolvedStableId = self.nextStableId + self.nextStableId += 1 + } + + entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false, isPremium: isPremium, isLocked: isLocked, isEditing: false, isAdd: false)) + } + + var currentIndex: Int = 0 + for item in generalItems { + if self.isReordering, let reorderNode = self.reorderNode, let reorderItem = reorderNode.itemNode?.stickerPackItem, let reorderPosition { + if currentIndex == reorderPosition { + addItem(reorderItem, false, false) + currentIndex += 1 + } + + if item.file.fileId == reorderItem.file.fileId { + + } else { + addItem(item, false, false) + currentIndex += 1 + } + } else { + addItem(item, false, false) + currentIndex += 1 + } + } + + if !premiumConfiguration.isPremiumDisabled { + if !premiumItems.isEmpty { + for item in premiumItems { + addItem(item, true, !hasPremium) + currentIndex += 1 + } + } + } + + entries.append(.add) + + self.currentEntries = entries + + if let controller = self.controller { + let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, context: self.context, interaction: self.interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, animationCache: controller.animationCache, animationRenderer: controller.animationRenderer, scrollToItem: nil, isEditing: self.isEditing) self.enqueueTransaction(transaction) } } @@ -1257,7 +1904,7 @@ private final class StickerPackContainer: ASDisplayNode { actionAreaBottomInset = 2.0 } } - if let (_, _, isInstalled) = self.currentStickerPack, isInstalled { + if let (info, _, isInstalled) = self.currentStickerPack, isInstalled, !info.flags.contains(.isCreator) { buttonHeight = 42.0 actionAreaTopInset = 1.0 actionAreaBottomInset = 2.0 @@ -1341,6 +1988,10 @@ private final class StickerPackContainer: ASDisplayNode { self.moreButtonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 46.0, y: 5.0), size: CGSize(width: 44.0, height: 44.0)) + + transition.updateAlpha(node: self.cancelButtonNode, alpha: self.isEditing ? 0.0 : 1.0) + transition.updateAlpha(node: self.moreButtonNode, alpha: self.isEditing ? 0.0 : 1.0) + if firstTime { while !self.enqueuedTransactions.isEmpty { self.dequeueTransaction() @@ -1467,7 +2118,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? private let longPressEmoji: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)? - private let openMention: (String) -> Void + fileprivate let openMention: (String) -> Void private let dimNode: ASDisplayNode private let shadowNode: ASImageNode @@ -1894,10 +2545,10 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { } } -public final class StickerPackScreenImpl: ViewController { +public final class StickerPackScreenImpl: ViewController, StickerPackScreen { private let context: AccountContext fileprivate var presentationData: PresentationData - private let updatedPresentationData: (initial: PresentationData, signal: Signal)? + fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? private var presentationDataDisposable: Disposable? private let stickerPacks: [StickerPackReference] @@ -1908,7 +2559,7 @@ public final class StickerPackScreenImpl: ViewController { private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? - private var controllerNode: StickerPackScreenNode { + fileprivate var controllerNode: StickerPackScreenNode { return self.displayNode as! StickerPackScreenNode } @@ -1926,6 +2577,7 @@ public final class StickerPackScreenImpl: ViewController { private var alreadyDidAppear: Bool = false private var animatedIn: Bool = false + fileprivate var initialIsEditing: Bool = false let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer @@ -1941,6 +2593,7 @@ public final class StickerPackScreenImpl: ViewController { selectedStickerPackIndex: Int = 0, mainActionTitle: String? = nil, actionTitle: String? = nil, + isEditing: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, @@ -1954,6 +2607,7 @@ public final class StickerPackScreenImpl: ViewController { self.initialSelectedStickerPackIndex = selectedStickerPackIndex self.mainActionTitle = mainActionTitle self.actionTitle = actionTitle + self.initialIsEditing = isEditing self.parentNavigationController = parentNavigationController self.sendSticker = sendSticker self.sendEmoji = sendEmoji @@ -2071,8 +2725,8 @@ public final class StickerPackScreenImpl: ViewController { } if let peer { if let parentNavigationController = strongSelf.parentNavigationController { - strongSelf.dismiss() - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: parentNavigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), animated: true)) + strongSelf.controllerNode.dismiss() + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: parentNavigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, animated: true)) } } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) @@ -2186,6 +2840,7 @@ public func StickerPackScreen( loadedStickerPacks: [LoadedStickerPack] = [], mainActionTitle: String? = nil, actionTitle: String? = nil, + isEditing: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? = nil, @@ -2201,6 +2856,7 @@ public func StickerPackScreen( selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, mainActionTitle: mainActionTitle, actionTitle: actionTitle, + isEditing: isEditing, parentNavigationController: parentNavigationController, sendSticker: sendSticker, sendEmoji: sendEmoji, @@ -2256,3 +2912,276 @@ private func generateArrowImage(color: UIColor) -> UIImage? { try? drawSvgPath(context, path: "M183.219,208.89 H206.781 C205.648,208.89 204.567,209.371 203.808,210.214 L197.23,217.523 C196.038,218.848 193.962,218.848 192.77,217.523 L186.192,210.214 C185.433,209.371 184.352,208.89 183.219,208.89 Z ") }) } + + +private class ReorderingGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: StickerPackPreviewGridItemNode?) + private let willBegin: (CGPoint) -> Void + private let began: (StickerPackPreviewGridItemNode) -> Void + private let ended: (CGPoint?) -> Void + private let moved: (CGPoint, CGPoint) -> Void + + private var initialLocation: CGPoint? + private var longPressTimer: SwiftSignalKit.Timer? + + var animateOnTouch = true + + private var itemNode: StickerPackPreviewGridItemNode? + + public init(animateOnTouch: Bool, shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: StickerPackPreviewGridItemNode?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (StickerPackPreviewGridItemNode) -> Void, ended: @escaping (CGPoint?) -> Void, moved: @escaping (CGPoint, CGPoint) -> Void) { + self.animateOnTouch = animateOnTouch + self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began + self.ended = ended + self.moved = moved + + super.init(target: nil, action: nil) + } + + deinit { + self.longPressTimer?.invalidate() + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemNode = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { + super.reset() + + self.itemNode = nil + self.stopLongPressTimer() + self.initialLocation = nil + } + + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + if let itemNode = self.itemNode { + self.began(itemNode) + } + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.state = .failed + self.ended(nil) + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location) + if allowed { + if let itemNode = itemNode, self.animateOnTouch { + itemNode.layer.animateScale(from: 1.0, to: 0.98, duration: 0.2, delay: 0.1) + } + self.itemNode = itemNode + self.initialLocation = location + if requiresLongPress { + self.startLongPressTimer() + } else { + self.state = .began + if let itemNode = self.itemNode { + self.began(itemNode) + } + } + } else { + self.state = .failed + } + } else { + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + if let location = touches.first?.location(in: self.view) { + self.ended(location) + } else { + self.ended(nil) + } + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.ended(nil) + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + self.moved(location, CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.itemNode?.layer.removeAllAnimations() + + self.stopLongPressTimer() + self.initialLocation = nil + self.state = .failed + } + } + } +} + +private func generateShadowImage(corners: CACornerMask, radius: CGFloat) -> UIImage? { + return generateImage(CGSize(width: 120.0, height: 120), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + +// context.saveGState() + context.setShadow(offset: CGSize(), blur: 28.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) + + var rectCorners: UIRectCorner = [] + if corners.contains(.layerMinXMinYCorner) { + rectCorners.insert(.topLeft) + } + if corners.contains(.layerMaxXMinYCorner) { + rectCorners.insert(.topRight) + } + if corners.contains(.layerMinXMaxYCorner) { + rectCorners.insert(.bottomLeft) + } + if corners.contains(.layerMaxXMaxYCorner) { + rectCorners.insert(.bottomRight) + } + + let path = UIBezierPath(roundedRect: CGRect(x: 30.0, y: 30.0, width: 60.0, height: 60.0), byRoundingCorners: rectCorners, cornerRadii: CGSize(width: radius, height: radius)).cgPath + context.addPath(path) + context.fillPath() +// context.restoreGState() + +// context.setBlendMode(.clear) +// context.addPath(path) +// context.fillPath() + })?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60) +} + +private final class CopyView: UIView { + let shadow: UIImageView + var snapshotView: UIView? + + init(frame: CGRect, corners: CACornerMask, radius: CGFloat) { + self.shadow = UIImageView() + self.shadow.contentMode = .scaleToFill + + super.init(frame: frame) + + self.addSubview(self.shadow) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class ReorderingItemNode: ASDisplayNode { + weak var itemNode: StickerPackPreviewGridItemNode? + + var currentState: (Int, Int)? + + private let copyView: CopyView + private let initialLocation: CGPoint + + init(itemNode: StickerPackPreviewGridItemNode, initialLocation: CGPoint) { + self.itemNode = itemNode + self.copyView = CopyView(frame: CGRect(), corners: [], radius: 0.0) + let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) + self.initialLocation = initialLocation + + super.init() + + if let snapshotView = snapshotView { + snapshotView.frame = CGRect(origin: CGPoint(), size: itemNode.bounds.size) + snapshotView.bounds.origin = itemNode.bounds.origin + snapshotView.layer.shadowRadius = 10.0 + snapshotView.layer.shadowColor = UIColor.black.cgColor + self.copyView.addSubview(snapshotView) + self.copyView.snapshotView = snapshotView + } + self.view.addSubview(self.copyView) + self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size) + self.copyView.shadow.frame = CGRect(origin: CGPoint(x: -30.0, y: -30.0), size: CGSize(width: itemNode.bounds.size.width + 60.0, height: itemNode.bounds.size.height + 60.0)) + self.copyView.shadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + self.copyView.snapshotView?.layer.animateScale(from: 1.0, to: 1.1, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.copyView.shadow.layer.animateScale(from: 1.0, to: 1.1, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + + func updateOffset(offset: CGPoint) { + self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x + offset.x, y: initialLocation.y + offset.y), size: copyView.bounds.size) + } + + func currentOffset() -> CGFloat? { + return self.copyView.center.y + } + + func animateCompletion(completion: @escaping () -> Void) { + if let itemNode = self.itemNode { + itemNode.view.superview?.bringSubviewToFront(itemNode.view) + + itemNode.layer.animateScale(from: 1.1, to: 1.0, duration: 0.25, removeOnCompletion: false) + +// let sourceFrame = self.view.convert(self.copyView.frame, to: itemNode.supernode?.view) +// let targetFrame = itemNode.frame +// itemNode.updateLayout(size: sourceFrame.size, transition: .immediate) +// itemNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in +// completion() +// }) +// itemNode.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.3, curve: .spring)) + + itemNode.isHidden = false + self.copyView.isHidden = true + + completion() + } else { + completion() + } + } +} diff --git a/submodules/StickerPeekUI/BUILD b/submodules/StickerPeekUI/BUILD index ec67754833..33f9618a5b 100644 --- a/submodules/StickerPeekUI/BUILD +++ b/submodules/StickerPeekUI/BUILD @@ -28,6 +28,8 @@ swift_library( "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ContextUI:ContextUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", + "//submodules/ReactionSelectionNode", + "//submodules/TelegramUI/Components/EntityKeyboard", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift index 47ebea3c4a..02b443717c 100644 --- a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift @@ -13,17 +13,22 @@ import SolidRoundedButtonNode import TelegramPresentationData import AccountContext import AppBundle +import ReactionSelectionNode +import EntityKeyboard public enum StickerPreviewPeekItem: Equatable { case pack(TelegramMediaFile) case found(FoundStickerItem) + case image(UIImage) - public var file: TelegramMediaFile { + public var file: TelegramMediaFile? { switch self { case let .pack(file): return file case let .found(item): return item.file + case .image: + return nil } } } @@ -34,20 +39,24 @@ public final class StickerPreviewPeekContent: PeekControllerContent { let strings: PresentationStrings public let item: StickerPreviewPeekItem let isLocked: Bool + let isCreating: Bool + let reactionItems: [ReactionItem] let menu: [ContextMenuItem] let openPremiumIntro: () -> Void - public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, item: StickerPreviewPeekItem, isLocked: Bool = false, menu: [ContextMenuItem], openPremiumIntro: @escaping () -> Void) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, item: StickerPreviewPeekItem, isLocked: Bool = false, isCreating: Bool = false, menu: [ContextMenuItem], reactionItems: [ReactionItem] = [], openPremiumIntro: @escaping () -> Void) { self.context = context self.theme = theme self.strings = strings self.item = item self.isLocked = isLocked + self.isCreating = isCreating if isLocked { self.menu = [] } else { self.menu = menu } + self.reactionItems = reactionItems self.openPremiumIntro = openPremiumIntro } @@ -72,10 +81,11 @@ public final class StickerPreviewPeekContent: PeekControllerContent { } public func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? { + if self.isCreating { + return EmojiStickerAccessoryNode(context: self.context, theme: self.theme, reactionItems: self.reactionItems) + } if self.isLocked { - let isEmoji = self.item.file.isCustomEmoji - - return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings, isEmoji: isEmoji, proceed: self.openPremiumIntro) + return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings, isEmoji: self.item.file?.isCustomEmoji ?? false, proceed: self.openPremiumIntro) } else { return nil } @@ -112,51 +122,57 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC self.textNode = ASTextNode() self.imageNode = TransformImageNode() - for case let .Sticker(text, _, _) in item.file.attributes { - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) - break - } - - let isPremiumSticker = item.file.isPremiumSticker - - if item.file.isAnimatedSticker || item.file.isVideoSticker { - let animationNode = DefaultAnimatedStickerNodeImpl() - animationNode.overrideVisibility = true - self.animationNode = animationNode - - let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fitSize: CGSize - if item.file.isCustomEmoji { - fitSize = CGSize(width: 200.0, height: 200.0) - } else { - fitSize = CGSize(width: 400.0, height: 400.0) - } - let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize) - - if item.file.isCustomTemplateEmoji { - animationNode.dynamicColor = theme.list.itemPrimaryTextColor + var isPremiumSticker = false + if let file = item.file { + for case let .Sticker(text, _, _) in file.attributes { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) + break } - animationNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: item.file.resource, isVideo: item.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: isPremiumSticker ? .once : .loop, mode: .direct(cachePathPrefix: nil)) - animationNode.visibility = true - animationNode.addSubnode(self.textNode) + isPremiumSticker = file.isPremiumSticker - if isPremiumSticker, let effect = item.file.videoThumbnails.first { - self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: item.file), resource: effect.resource).start()) + if file.isAnimatedSticker || file.isVideoSticker { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.overrideVisibility = true + self.animationNode = animationNode - let source = AnimatedStickerResourceSource(account: context.account, resource: effect.resource, fitzModifier: nil) - let additionalAnimationNode = DefaultAnimatedStickerNodeImpl() - additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 2.0), height: Int(fittedDimensions.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) - additionalAnimationNode.visibility = true - self.additionalAnimationNode = additionalAnimationNode + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fitSize: CGSize + if file.isCustomEmoji { + fitSize = CGSize(width: 200.0, height: 200.0) + } else { + fitSize = CGSize(width: 400.0, height: 400.0) + } + let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize) + + if file.isCustomTemplateEmoji { + animationNode.dynamicColor = theme.list.itemPrimaryTextColor + } + + animationNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: isPremiumSticker ? .once : .loop, mode: .direct(cachePathPrefix: nil)) + animationNode.visibility = true + animationNode.addSubnode(self.textNode) + + if isPremiumSticker, let effect = file.videoThumbnails.first { + self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start()) + + let source = AnimatedStickerResourceSource(account: context.account, resource: effect.resource, fitzModifier: nil) + let additionalAnimationNode = DefaultAnimatedStickerNodeImpl() + additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 2.0), height: Int(fittedDimensions.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + additionalAnimationNode.visibility = true + self.additionalAnimationNode = additionalAnimationNode + } + } else { + self.imageNode.addSubnode(self.textNode) + self.animationNode = nil } - } else { - self.imageNode.addSubnode(self.textNode) - self.animationNode = nil + + self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: file, small: false, fetched: true)) + } else if case let .image(image) = item { + self.imageNode.contents = image.cgImage + self._ready.set(.single(true)) } - self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: item.file, small: false, fetched: true)) - super.init() self.isUserInteractionEnabled = false @@ -209,57 +225,60 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let boundingSize: CGSize - if self.item.file.isCustomEmoji { + if self.item.file?.isCustomEmoji == true { boundingSize = CGSize(width: 120.0, height: 120.0) } else if let _ = self.additionalAnimationNode { boundingSize = CGSize(width: 240.0, height: 240.0).fitted(size) } else { boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size) } - - if let dimensitons = self.item.file.dimensions { - var topOffset: CGFloat = 0.0 - var textSpacing: CGFloat = 50.0 - - if size.width == 292.0 { - topOffset = 60.0 - textSpacing -= 10.0 - } else if size.width == 347.0 && size.height == 577.0 { - topOffset = 60.0 - textSpacing -= 10.0 - } - - let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) - - let imageSize = dimensitons.cgSize.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - var imageFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0), y: textSize.height + textSpacing - topOffset), size: imageSize) - var centerOffset: CGFloat = 0.0 - if self.item.file.isPremiumSticker { - let originalImageFrame = imageFrame - imageFrame.origin.x = min(imageFrame.minX + imageFrame.width * 0.1, size.width - imageFrame.width - 18.0) - centerOffset = imageFrame.minX - originalImageFrame.minX - } - self.imageNode.frame = imageFrame - if let animationNode = self.animationNode { - animationNode.frame = imageFrame - animationNode.updateLayout(size: imageSize) - - if let additionalAnimationNode = self.additionalAnimationNode { - additionalAnimationNode.frame = imageFrame.offsetBy(dx: -imageFrame.width * 0.245 + 21.0, dy: -1.0).insetBy(dx: -imageFrame.width * 0.245, dy: -imageFrame.height * 0.245) - additionalAnimationNode.updateLayout(size: additionalAnimationNode.frame.size) - } - } - - self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0) - centerOffset, y: -textSize.height - textSpacing), size: textSize) - - if self.item.file.isCustomEmoji { - return CGSize(width: boundingSize.width, height: imageFrame.height) - } else { - return CGSize(width: boundingSize.width, height: imageFrame.height + textSize.height + textSpacing) - } + + let dimensions: PixelDimensions + if let dimensionsValue = self.item.file?.dimensions { + dimensions = dimensionsValue } else { - return CGSize(width: size.width, height: 10.0) + dimensions = PixelDimensions(width: 512, height: 512) + } + + var topOffset: CGFloat = 0.0 + var textSpacing: CGFloat = 50.0 + + if size.width == 292.0 { + topOffset = 60.0 + textSpacing -= 10.0 + } else if size.width == 347.0 && size.height == 577.0 { + topOffset = 60.0 + textSpacing -= 10.0 + } + + let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) + + let imageSize = dimensions.cgSize.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + var imageFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0), y: textSize.height + textSpacing - topOffset), size: imageSize) + var centerOffset: CGFloat = 0.0 + if self.item.file?.isPremiumSticker == true { + let originalImageFrame = imageFrame + imageFrame.origin.x = min(imageFrame.minX + imageFrame.width * 0.1, size.width - imageFrame.width - 18.0) + centerOffset = imageFrame.minX - originalImageFrame.minX + } + self.imageNode.frame = imageFrame + if let animationNode = self.animationNode { + animationNode.frame = imageFrame + animationNode.updateLayout(size: imageSize) + + if let additionalAnimationNode = self.additionalAnimationNode { + additionalAnimationNode.frame = imageFrame.offsetBy(dx: -imageFrame.width * 0.245 + 21.0, dy: -1.0).insetBy(dx: -imageFrame.width * 0.245, dy: -imageFrame.height * 0.245) + additionalAnimationNode.updateLayout(size: additionalAnimationNode.frame.size) + } + } + + self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0) - centerOffset, y: -textSize.height - textSpacing), size: textSize) + + if self.item.file?.isCustomEmoji == true { + return CGSize(width: boundingSize.width, height: imageFrame.height) + } else { + return CGSize(width: boundingSize.width, height: imageFrame.height + textSize.height + textSpacing) } } } @@ -337,3 +356,146 @@ final class PremiumStickerPackAccessoryNode: SparseNode, PeekControllerAccessory self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: size.height - cancelSize.height - 48.0 - buttonHeight - 20.0 - textSize.height - 31.0 + bottomOffset), size: textSize) } } + +final class EmojiStickerAccessoryNode: SparseNode, PeekControllerAccessoryNode { + let context: AccountContext + + let reactionContextNode: ReactionContextNode + + var dismiss: () -> Void = {} + + init(context: AccountContext, theme: PresentationTheme, reactionItems: [ReactionItem]) { + self.context = context + + var layoutImpl: ((ContainedViewLayoutTransition) -> Void)? + + var items: [ReactionItem] = [] + if let reaction = reactionItems.first(where: { $0.reaction.rawValue == .builtin("👍") }) { + items.append(reaction) + } + if let reaction = reactionItems.first(where: { $0.reaction.rawValue == .builtin("👎") }) { + items.append(reaction) + } + if let reaction = reactionItems.first(where: { $0.reaction.rawValue == .builtin("❤") }) { + items.append(reaction) + } + if let reaction = reactionItems.first(where: { $0.reaction.rawValue == .builtin("🔥") }) { + items.append(reaction) + } + if let reaction = reactionItems.first(where: { $0.reaction.rawValue == .builtin("🥰") }) { + items.append(reaction) + } + if let reaction = reactionItems.first(where: { $0.reaction.rawValue == .builtin("👏") }) { + items.append(reaction) + } + if let reaction = reactionItems.first(where: { $0.reaction.rawValue == .builtin("😁") }) { + items.append(reaction) + } + + let selectedItems = ValuePromise>() + //TODO:localize + let reactionContextNode = ReactionContextNode( + context: self.context, + animationCache: self.context.animationCache, + presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), + items: items.map { .reaction($0) }, + selectedItems: Set(), + title: "Set emoji that corresponds to your sticker", + reactionsLocked: false, + alwaysAllowPremiumReactions: true, + allPresetReactionsAreAvailable: true, + getEmojiContent: { animationCache, animationRenderer in + let mappedReactionItems: [EmojiComponentReactionItem] = items.map { reaction -> EmojiComponentReactionItem in + return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) + } + + return selectedItems.get() + |> mapToSignal { selectedItems in + let selected = Set() + + return EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + isStandalone: false, + subject: .stickerAlt, + hasTrending: false, + topReactionItems: mappedReactionItems, + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: false, + chatPeerId: context.account.peerId, + selectedItems: selected, + hasRecent: false, + premiumIfSavedMessages: false + ) + } + }, + isExpandedUpdated: { transition in + layoutImpl?(transition) + }, + requestLayout: { transition in + layoutImpl?(transition) + }, + requestUpdateOverlayWantsToBeBelowKeyboard: { transition in + layoutImpl?(transition) + } + ) + reactionContextNode.displayTail = true + reactionContextNode.forceTailToRight = true + reactionContextNode.forceDark = true + self.reactionContextNode = reactionContextNode + + super.init() + + layoutImpl = { [weak self] transition in + self?.requestLayout(transition: transition) + } + + reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in + guard let self else { + return + } + self.reactionContextNode.collapse() + + let _ = (selectedItems.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { items in + var items = items + items.insert(updateReaction.reaction) + selectedItems.set(items) + }) + } + + self.addSubnode(reactionContextNode) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if let result, result.isDescendant(of: self.reactionContextNode.view) { + return result + } + return nil + } + + func requestLayout(transition: ContainedViewLayoutTransition) { + guard let size = self.currentLayout else { + return + } + self.updateLayout(size: size, transition: transition) + } + + private var currentLayout: CGSize? + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let isFirstTime = self.currentLayout == nil + self.currentLayout = size + + let anchorRect = CGRect(x: size.width / 2.0, y: size.height / 3.0 - 50.0, width: 0.0, height: 0.0) + + transition.updateFrame(view: self.reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: size)) + self.reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition) + + if isFirstTime { + self.reactionContextNode.animateIn(from: anchorRect) + } + } +} diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 1a147326a5..787db8d696 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -6,6 +6,7 @@ public enum Api { public enum channels {} public enum chatlists {} public enum contacts {} + public enum fragment {} public enum help {} public enum messages {} public enum payments {} @@ -28,6 +29,7 @@ public enum Api { public enum chatlists {} public enum contacts {} public enum folders {} + public enum fragment {} public enum help {} public enum langpack {} public enum messages {} @@ -321,6 +323,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1736378792] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordEmpty($0) } dict[-763367294] = { return Api.InputCheckPasswordSRP.parse_inputCheckPasswordSRP($0) } dict[1968737087] = { return Api.InputClientProxy.parse_inputClientProxy($0) } + dict[-1562241884] = { return Api.InputCollectible.parse_inputCollectiblePhone($0) } + dict[-476815191] = { return Api.InputCollectible.parse_inputCollectibleUsername($0) } dict[-208488460] = { return Api.InputContact.parse_inputPhoneContact($0) } dict[-55902537] = { return Api.InputDialogPeer.parse_inputDialogPeer($0) } dict[1684014375] = { return Api.InputDialogPeer.parse_inputDialogPeerFolder($0) } @@ -491,7 +495,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } dict[64088654] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } - dict[-1502839044] = { return Api.Message.parse_message($0) } + dict[592953125] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[721967202] = { return Api.Message.parse_messageService($0) } dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) } @@ -667,7 +671,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-901375139] = { return Api.PeerLocated.parse_peerLocated($0) } dict[-118740917] = { return Api.PeerLocated.parse_peerSelfLocated($0) } dict[-1721619444] = { return Api.PeerNotifySettings.parse_peerNotifySettings($0) } - dict[-1525149427] = { return Api.PeerSettings.parse_peerSettings($0) } + dict[-1395233698] = { return Api.PeerSettings.parse_peerSettings($0) } dict[-1707742823] = { return Api.PeerStories.parse_peerStories($0) } dict[-1770029977] = { return Api.PhoneCall.parse_phoneCall($0) } dict[912311057] = { return Api.PhoneCall.parse_phoneCallAccepted($0) } @@ -884,7 +888,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1873947492] = { return Api.Update.parse_updateBotChatBoost($0) } dict[299870598] = { return Api.Update.parse_updateBotChatInviteRequester($0) } dict[1299263278] = { return Api.Update.parse_updateBotCommands($0) } - dict[-1590796039] = { return Api.Update.parse_updateBotDeleteBusinessMessage($0) } + dict[-1607821266] = { return Api.Update.parse_updateBotDeleteBusinessMessage($0) } dict[1420915171] = { return Api.Update.parse_updateBotEditBusinessMessage($0) } dict[1232025500] = { return Api.Update.parse_updateBotInlineQuery($0) } dict[317794823] = { return Api.Update.parse_updateBotInlineSend($0) } @@ -1125,6 +1129,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1891070632] = { return Api.contacts.TopPeers.parse_topPeers($0) } dict[-1255369827] = { return Api.contacts.TopPeers.parse_topPeersDisabled($0) } dict[-567906571] = { return Api.contacts.TopPeers.parse_topPeersNotModified($0) } + dict[1857945489] = { return Api.fragment.CollectibleInfo.parse_collectibleInfo($0) } dict[-585598930] = { return Api.help.AppConfig.parse_appConfig($0) } dict[2094949405] = { return Api.help.AppConfig.parse_appConfigNotModified($0) } dict[-860107216] = { return Api.help.AppUpdate.parse_appUpdate($0) } @@ -1203,6 +1208,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1938715001] = { return Api.messages.Messages.parse_messages($0) } dict[1951620897] = { return Api.messages.Messages.parse_messagesNotModified($0) } dict[978610270] = { return Api.messages.Messages.parse_messagesSlice($0) } + dict[-83926371] = { return Api.messages.MyStickers.parse_myStickers($0) } dict[863093588] = { return Api.messages.PeerDialogs.parse_peerDialogs($0) } dict[1753266509] = { return Api.messages.PeerSettings.parse_peerSettings($0) } dict[-963811691] = { return Api.messages.QuickReplies.parse_quickReplies($0) } @@ -1315,7 +1321,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") return nil } } @@ -1587,6 +1593,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InputClientProxy: _1.serialize(buffer, boxed) + case let _1 as Api.InputCollectible: + _1.serialize(buffer, boxed) case let _1 as Api.InputContact: _1.serialize(buffer, boxed) case let _1 as Api.InputDialogPeer: @@ -2043,6 +2051,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.contacts.TopPeers: _1.serialize(buffer, boxed) + case let _1 as Api.fragment.CollectibleInfo: + _1.serialize(buffer, boxed) case let _1 as Api.help.AppConfig: _1.serialize(buffer, boxed) case let _1 as Api.help.AppUpdate: @@ -2149,6 +2159,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.messages.Messages: _1.serialize(buffer, boxed) + case let _1 as Api.messages.MyStickers: + _1.serialize(buffer, boxed) case let _1 as Api.messages.PeerDialogs: _1.serialize(buffer, boxed) case let _1 as Api.messages.PeerSettings: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 4fe4c26f44..2148237347 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1,3 +1,183 @@ +public extension Api { + enum InputPrivacyRule: TypeConstructorDescription { + case inputPrivacyValueAllowAll + case inputPrivacyValueAllowChatParticipants(chats: [Int64]) + case inputPrivacyValueAllowCloseFriends + case inputPrivacyValueAllowContacts + case inputPrivacyValueAllowUsers(users: [Api.InputUser]) + case inputPrivacyValueDisallowAll + case inputPrivacyValueDisallowChatParticipants(chats: [Int64]) + case inputPrivacyValueDisallowContacts + case inputPrivacyValueDisallowUsers(users: [Api.InputUser]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputPrivacyValueAllowAll: + if boxed { + buffer.appendInt32(407582158) + } + + break + case .inputPrivacyValueAllowChatParticipants(let chats): + if boxed { + buffer.appendInt32(-2079962673) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .inputPrivacyValueAllowCloseFriends: + if boxed { + buffer.appendInt32(793067081) + } + + break + case .inputPrivacyValueAllowContacts: + if boxed { + buffer.appendInt32(218751099) + } + + break + case .inputPrivacyValueAllowUsers(let users): + if boxed { + buffer.appendInt32(320652927) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .inputPrivacyValueDisallowAll: + if boxed { + buffer.appendInt32(-697604407) + } + + break + case .inputPrivacyValueDisallowChatParticipants(let chats): + if boxed { + buffer.appendInt32(-380694650) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .inputPrivacyValueDisallowContacts: + if boxed { + buffer.appendInt32(195371015) + } + + break + case .inputPrivacyValueDisallowUsers(let users): + if boxed { + buffer.appendInt32(-1877932953) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputPrivacyValueAllowAll: + return ("inputPrivacyValueAllowAll", []) + case .inputPrivacyValueAllowChatParticipants(let chats): + return ("inputPrivacyValueAllowChatParticipants", [("chats", chats as Any)]) + case .inputPrivacyValueAllowCloseFriends: + return ("inputPrivacyValueAllowCloseFriends", []) + case .inputPrivacyValueAllowContacts: + return ("inputPrivacyValueAllowContacts", []) + case .inputPrivacyValueAllowUsers(let users): + return ("inputPrivacyValueAllowUsers", [("users", users as Any)]) + case .inputPrivacyValueDisallowAll: + return ("inputPrivacyValueDisallowAll", []) + case .inputPrivacyValueDisallowChatParticipants(let chats): + return ("inputPrivacyValueDisallowChatParticipants", [("chats", chats as Any)]) + case .inputPrivacyValueDisallowContacts: + return ("inputPrivacyValueDisallowContacts", []) + case .inputPrivacyValueDisallowUsers(let users): + return ("inputPrivacyValueDisallowUsers", [("users", users as Any)]) + } + } + + public static func parse_inputPrivacyValueAllowAll(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowAll + } + public static func parse_inputPrivacyValueAllowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(chats: _1!) + } + else { + return nil + } + } + public static func parse_inputPrivacyValueAllowCloseFriends(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowCloseFriends + } + public static func parse_inputPrivacyValueAllowContacts(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueAllowContacts + } + public static func parse_inputPrivacyValueAllowUsers(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Api.InputUser]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueAllowUsers(users: _1!) + } + else { + return nil + } + } + public static func parse_inputPrivacyValueDisallowAll(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueDisallowAll + } + public static func parse_inputPrivacyValueDisallowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(chats: _1!) + } + else { + return nil + } + } + public static func parse_inputPrivacyValueDisallowContacts(_ reader: BufferReader) -> InputPrivacyRule? { + return Api.InputPrivacyRule.inputPrivacyValueDisallowContacts + } + public static func parse_inputPrivacyValueDisallowUsers(_ reader: BufferReader) -> InputPrivacyRule? { + var _1: [Api.InputUser]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPrivacyRule.inputPrivacyValueDisallowUsers(users: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputQuickReplyShortcut: TypeConstructorDescription { case inputQuickReplyShortcut(shortcut: String) @@ -844,229 +1024,3 @@ public extension Api { } } -public extension Api { - enum InputTheme: TypeConstructorDescription { - case inputTheme(id: Int64, accessHash: Int64) - case inputThemeSlug(slug: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputTheme(let id, let accessHash): - if boxed { - buffer.appendInt32(1012306921) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputThemeSlug(let slug): - if boxed { - buffer.appendInt32(-175567375) - } - serializeString(slug, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputTheme(let id, let accessHash): - return ("inputTheme", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputThemeSlug(let slug): - return ("inputThemeSlug", [("slug", slug as Any)]) - } - } - - public static func parse_inputTheme(_ reader: BufferReader) -> InputTheme? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputTheme.inputTheme(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputThemeSlug(_ reader: BufferReader) -> InputTheme? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputTheme.inputThemeSlug(slug: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputThemeSettings: TypeConstructorDescription { - case inputThemeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, outboxAccentColor: Int32?, messageColors: [Int32]?, wallpaper: Api.InputWallPaper?, wallpaperSettings: Api.WallPaperSettings?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputThemeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper, let wallpaperSettings): - if boxed { - buffer.appendInt32(-1881255857) - } - serializeInt32(flags, buffer: buffer, boxed: false) - baseTheme.serialize(buffer, true) - serializeInt32(accentColor, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 3) != 0 {serializeInt32(outboxAccentColor!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messageColors!.count)) - for item in messageColors! { - serializeInt32(item, buffer: buffer, boxed: false) - }} - if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {wallpaperSettings!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputThemeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper, let wallpaperSettings): - return ("inputThemeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any), ("wallpaperSettings", wallpaperSettings as Any)]) - } - } - - public static func parse_inputThemeSettings(_ reader: BufferReader) -> InputThemeSettings? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.BaseTheme? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } - var _5: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } } - var _6: Api.InputWallPaper? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.InputWallPaper - } } - var _7: Api.WallPaperSettings? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.WallPaperSettings - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.InputThemeSettings.inputThemeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6, wallpaperSettings: _7) - } - else { - return nil - } - } - - } -} -public extension Api { - indirect enum InputUser: TypeConstructorDescription { - case inputUser(userId: Int64, accessHash: Int64) - case inputUserEmpty - case inputUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) - case inputUserSelf - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputUser(let userId, let accessHash): - if boxed { - buffer.appendInt32(-233744186) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputUserEmpty: - if boxed { - buffer.appendInt32(-1182234929) - } - - break - case .inputUserFromMessage(let peer, let msgId, let userId): - if boxed { - buffer.appendInt32(497305826) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) - break - case .inputUserSelf: - if boxed { - buffer.appendInt32(-138301121) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputUser(let userId, let accessHash): - return ("inputUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) - case .inputUserEmpty: - return ("inputUserEmpty", []) - case .inputUserFromMessage(let peer, let msgId, let userId): - return ("inputUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) - case .inputUserSelf: - return ("inputUserSelf", []) - } - } - - public static func parse_inputUser(_ reader: BufferReader) -> InputUser? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputUser.inputUser(userId: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputUserEmpty(_ reader: BufferReader) -> InputUser? { - return Api.InputUser.inputUserEmpty - } - public static func parse_inputUserFromMessage(_ reader: BufferReader) -> InputUser? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputUser.inputUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) - } - else { - return nil - } - } - public static func parse_inputUserSelf(_ reader: BufferReader) -> InputUser? { - return Api.InputUser.inputUserSelf - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 89037ef849..024db526a7 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1,3 +1,229 @@ +public extension Api { + enum InputTheme: TypeConstructorDescription { + case inputTheme(id: Int64, accessHash: Int64) + case inputThemeSlug(slug: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputTheme(let id, let accessHash): + if boxed { + buffer.appendInt32(1012306921) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputThemeSlug(let slug): + if boxed { + buffer.appendInt32(-175567375) + } + serializeString(slug, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputTheme(let id, let accessHash): + return ("inputTheme", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputThemeSlug(let slug): + return ("inputThemeSlug", [("slug", slug as Any)]) + } + } + + public static func parse_inputTheme(_ reader: BufferReader) -> InputTheme? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputTheme.inputTheme(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputThemeSlug(_ reader: BufferReader) -> InputTheme? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputTheme.inputThemeSlug(slug: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputThemeSettings: TypeConstructorDescription { + case inputThemeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, outboxAccentColor: Int32?, messageColors: [Int32]?, wallpaper: Api.InputWallPaper?, wallpaperSettings: Api.WallPaperSettings?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputThemeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper, let wallpaperSettings): + if boxed { + buffer.appendInt32(-1881255857) + } + serializeInt32(flags, buffer: buffer, boxed: false) + baseTheme.serialize(buffer, true) + serializeInt32(accentColor, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(outboxAccentColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messageColors!.count)) + for item in messageColors! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {wallpaperSettings!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputThemeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper, let wallpaperSettings): + return ("inputThemeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any), ("wallpaperSettings", wallpaperSettings as Any)]) + } + } + + public static func parse_inputThemeSettings(_ reader: BufferReader) -> InputThemeSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.BaseTheme? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } + var _5: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } + var _6: Api.InputWallPaper? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.InputWallPaper + } } + var _7: Api.WallPaperSettings? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.WallPaperSettings + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.InputThemeSettings.inputThemeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6, wallpaperSettings: _7) + } + else { + return nil + } + } + + } +} +public extension Api { + indirect enum InputUser: TypeConstructorDescription { + case inputUser(userId: Int64, accessHash: Int64) + case inputUserEmpty + case inputUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) + case inputUserSelf + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputUser(let userId, let accessHash): + if boxed { + buffer.appendInt32(-233744186) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputUserEmpty: + if boxed { + buffer.appendInt32(-1182234929) + } + + break + case .inputUserFromMessage(let peer, let msgId, let userId): + if boxed { + buffer.appendInt32(497305826) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + break + case .inputUserSelf: + if boxed { + buffer.appendInt32(-138301121) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputUser(let userId, let accessHash): + return ("inputUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) + case .inputUserEmpty: + return ("inputUserEmpty", []) + case .inputUserFromMessage(let peer, let msgId, let userId): + return ("inputUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) + case .inputUserSelf: + return ("inputUserSelf", []) + } + } + + public static func parse_inputUser(_ reader: BufferReader) -> InputUser? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputUser.inputUser(userId: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputUserEmpty(_ reader: BufferReader) -> InputUser? { + return Api.InputUser.inputUserEmpty + } + public static func parse_inputUserFromMessage(_ reader: BufferReader) -> InputUser? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputUser.inputUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) + } + else { + return nil + } + } + public static func parse_inputUserSelf(_ reader: BufferReader) -> InputUser? { + return Api.InputUser.inputUserSelf + } + + } +} public extension Api { enum InputWallPaper: TypeConstructorDescription { case inputWallPaper(id: Int64, accessHash: Int64) @@ -942,45 +1168,3 @@ public extension Api { } } -public extension Api { - enum KeyboardButtonRow: TypeConstructorDescription { - case keyboardButtonRow(buttons: [Api.KeyboardButton]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .keyboardButtonRow(let buttons): - if boxed { - buffer.appendInt32(2002815875) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(buttons.count)) - for item in buttons { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .keyboardButtonRow(let buttons): - return ("keyboardButtonRow", [("buttons", buttons as Any)]) - } - } - - public static func parse_keyboardButtonRow(_ reader: BufferReader) -> KeyboardButtonRow? { - var _1: [Api.KeyboardButton]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButton.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.KeyboardButtonRow.keyboardButtonRow(buttons: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 1edeff5668..6c71c62e49 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,3 +1,45 @@ +public extension Api { + enum KeyboardButtonRow: TypeConstructorDescription { + case keyboardButtonRow(buttons: [Api.KeyboardButton]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .keyboardButtonRow(let buttons): + if boxed { + buffer.appendInt32(2002815875) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(buttons.count)) + for item in buttons { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .keyboardButtonRow(let buttons): + return ("keyboardButtonRow", [("buttons", buttons as Any)]) + } + } + + public static func parse_keyboardButtonRow(_ reader: BufferReader) -> KeyboardButtonRow? { + var _1: [Api.KeyboardButton]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButton.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.KeyboardButtonRow.keyboardButtonRow(buttons: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum LabeledPrice: TypeConstructorDescription { case labeledPrice(label: String, amount: Int64) @@ -586,17 +628,18 @@ public extension Api { } public extension Api { indirect enum Message: TypeConstructorDescription { - case message(flags: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?) + case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?) case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?) case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, ttlPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .message(let flags, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId): + case .message(let flags, let flags2, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let viaBusinessBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId): if boxed { - buffer.appendInt32(-1502839044) + buffer.appendInt32(592953125) } serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(flags2, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 8) != 0 {fromId!.serialize(buffer, true)} if Int(flags) & Int(1 << 29) != 0 {serializeInt32(fromBoostsApplied!, buffer: buffer, boxed: false)} @@ -604,6 +647,7 @@ public extension Api { if Int(flags) & Int(1 << 28) != 0 {savedPeerId!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {fwdFrom!.serialize(buffer, true)} if Int(flags) & Int(1 << 11) != 0 {serializeInt64(viaBotId!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 0) != 0 {serializeInt64(viaBusinessBotId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)} serializeInt32(date, buffer: buffer, boxed: false) serializeString(message, buffer: buffer, boxed: false) @@ -655,8 +699,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .message(let flags, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId): - return ("message", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any)]) + case .message(let flags, let flags2, let id, let fromId, let fromBoostsApplied, let peerId, let savedPeerId, let fwdFrom, let viaBotId, let viaBusinessBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod, let quickReplyShortcutId): + return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any)]) case .messageEmpty(let flags, let id, let peerId): return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod): @@ -669,98 +713,104 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() - var _3: Api.Peer? + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.Peer? if Int(_1!) & Int(1 << 8) != 0 {if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.Peer + _4 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _4: Int32? - if Int(_1!) & Int(1 << 29) != 0 {_4 = reader.readInt32() } - var _5: Api.Peer? - if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.Peer - } + var _5: Int32? + if Int(_1!) & Int(1 << 29) != 0 {_5 = reader.readInt32() } var _6: Api.Peer? - if Int(_1!) & Int(1 << 28) != 0 {if let signature = reader.readInt32() { + if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _7: Api.Peer? + if Int(_1!) & Int(1 << 28) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _7: Api.MessageFwdHeader? + var _8: Api.MessageFwdHeader? if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader + _8 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader } } - var _8: Int64? - if Int(_1!) & Int(1 << 11) != 0 {_8 = reader.readInt64() } - var _9: Api.MessageReplyHeader? + var _9: Int64? + if Int(_1!) & Int(1 << 11) != 0 {_9 = reader.readInt64() } + var _10: Int64? + if Int(_2!) & Int(1 << 0) != 0 {_10 = reader.readInt64() } + var _11: Api.MessageReplyHeader? if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader + _11 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader } } - var _10: Int32? - _10 = reader.readInt32() - var _11: String? - _11 = parseString(reader) - var _12: Api.MessageMedia? + var _12: Int32? + _12 = reader.readInt32() + var _13: String? + _13 = parseString(reader) + var _14: Api.MessageMedia? if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { - _12 = Api.parse(reader, signature: signature) as? Api.MessageMedia + _14 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } - var _13: Api.ReplyMarkup? + var _15: Api.ReplyMarkup? if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { - _13 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + _15 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } - var _14: [Api.MessageEntity]? + var _16: [Api.MessageEntity]? if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() { - _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - var _15: Int32? - if Int(_1!) & Int(1 << 10) != 0 {_15 = reader.readInt32() } - var _16: Int32? - if Int(_1!) & Int(1 << 10) != 0 {_16 = reader.readInt32() } - var _17: Api.MessageReplies? - if Int(_1!) & Int(1 << 23) != 0 {if let signature = reader.readInt32() { - _17 = Api.parse(reader, signature: signature) as? Api.MessageReplies + _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } + var _17: Int32? + if Int(_1!) & Int(1 << 10) != 0 {_17 = reader.readInt32() } var _18: Int32? - if Int(_1!) & Int(1 << 15) != 0 {_18 = reader.readInt32() } - var _19: String? - if Int(_1!) & Int(1 << 16) != 0 {_19 = parseString(reader) } - var _20: Int64? - if Int(_1!) & Int(1 << 17) != 0 {_20 = reader.readInt64() } - var _21: Api.MessageReactions? + if Int(_1!) & Int(1 << 10) != 0 {_18 = reader.readInt32() } + var _19: Api.MessageReplies? + if Int(_1!) & Int(1 << 23) != 0 {if let signature = reader.readInt32() { + _19 = Api.parse(reader, signature: signature) as? Api.MessageReplies + } } + var _20: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_20 = reader.readInt32() } + var _21: String? + if Int(_1!) & Int(1 << 16) != 0 {_21 = parseString(reader) } + var _22: Int64? + if Int(_1!) & Int(1 << 17) != 0 {_22 = reader.readInt64() } + var _23: Api.MessageReactions? if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() { - _21 = Api.parse(reader, signature: signature) as? Api.MessageReactions + _23 = Api.parse(reader, signature: signature) as? Api.MessageReactions } } - var _22: [Api.RestrictionReason]? + var _24: [Api.RestrictionReason]? if Int(_1!) & Int(1 << 22) != 0 {if let _ = reader.readInt32() { - _22 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) + _24 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) } } - var _23: Int32? - if Int(_1!) & Int(1 << 25) != 0 {_23 = reader.readInt32() } - var _24: Int32? - if Int(_1!) & Int(1 << 30) != 0 {_24 = reader.readInt32() } + var _25: Int32? + if Int(_1!) & Int(1 << 25) != 0 {_25 = reader.readInt32() } + var _26: Int32? + if Int(_1!) & Int(1 << 30) != 0 {_26 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 29) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 28) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 11) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil - let _c10 = _10 != nil - let _c11 = _11 != nil - let _c12 = (Int(_1!) & Int(1 << 9) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 6) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 7) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 10) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 10) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 23) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 15) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 16) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 17) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 20) == 0) || _21 != nil - let _c22 = (Int(_1!) & Int(1 << 22) == 0) || _22 != nil - let _c23 = (Int(_1!) & Int(1 << 25) == 0) || _23 != nil - let _c24 = (Int(_1!) & Int(1 << 30) == 0) || _24 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 { - return Api.Message.message(flags: _1!, id: _2!, fromId: _3, fromBoostsApplied: _4, peerId: _5!, savedPeerId: _6, fwdFrom: _7, viaBotId: _8, replyTo: _9, date: _10!, message: _11!, media: _12, replyMarkup: _13, entities: _14, views: _15, forwards: _16, replies: _17, editDate: _18, postAuthor: _19, groupedId: _20, reactions: _21, restrictionReason: _22, ttlPeriod: _23, quickReplyShortcutId: _24) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 29) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 28) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 11) == 0) || _9 != nil + let _c10 = (Int(_2!) & Int(1 << 0) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil + let _c12 = _12 != nil + let _c13 = _13 != nil + let _c14 = (Int(_1!) & Int(1 << 9) == 0) || _14 != nil + let _c15 = (Int(_1!) & Int(1 << 6) == 0) || _15 != nil + let _c16 = (Int(_1!) & Int(1 << 7) == 0) || _16 != nil + let _c17 = (Int(_1!) & Int(1 << 10) == 0) || _17 != nil + let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil + let _c19 = (Int(_1!) & Int(1 << 23) == 0) || _19 != nil + let _c20 = (Int(_1!) & Int(1 << 15) == 0) || _20 != nil + let _c21 = (Int(_1!) & Int(1 << 16) == 0) || _21 != nil + let _c22 = (Int(_1!) & Int(1 << 17) == 0) || _22 != nil + let _c23 = (Int(_1!) & Int(1 << 20) == 0) || _23 != nil + let _c24 = (Int(_1!) & Int(1 << 22) == 0) || _24 != nil + let _c25 = (Int(_1!) & Int(1 << 25) == 0) || _25 != nil + let _c26 = (Int(_1!) & Int(1 << 30) == 0) || _26 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 { + return Api.Message.message(flags: _1!, flags2: _2!, id: _3!, fromId: _4, fromBoostsApplied: _5, peerId: _6!, savedPeerId: _7, fwdFrom: _8, viaBotId: _9, viaBusinessBotId: _10, replyTo: _11, date: _12!, message: _13!, media: _14, replyMarkup: _15, entities: _16, views: _17, forwards: _18, replies: _19, editDate: _20, postAuthor: _21, groupedId: _22, reactions: _23, restrictionReason: _24, ttlPeriod: _25, quickReplyShortcutId: _26) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 258955a62f..88dfb1d60c 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -898,26 +898,28 @@ public extension Api { } public extension Api { enum PeerSettings: TypeConstructorDescription { - case peerSettings(flags: Int32, geoDistance: Int32?, requestChatTitle: String?, requestChatDate: Int32?) + case peerSettings(flags: Int32, geoDistance: Int32?, requestChatTitle: String?, requestChatDate: Int32?, businessBotId: Int64?, businessBotManageUrl: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate): + case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl): if boxed { - buffer.appendInt32(-1525149427) + buffer.appendInt32(-1395233698) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 6) != 0 {serializeInt32(geoDistance!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 9) != 0 {serializeString(requestChatTitle!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 9) != 0 {serializeInt32(requestChatDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 13) != 0 {serializeInt64(businessBotId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 13) != 0 {serializeString(businessBotManageUrl!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate): - return ("peerSettings", [("flags", flags as Any), ("geoDistance", geoDistance as Any), ("requestChatTitle", requestChatTitle as Any), ("requestChatDate", requestChatDate as Any)]) + case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl): + return ("peerSettings", [("flags", flags as Any), ("geoDistance", geoDistance as Any), ("requestChatTitle", requestChatTitle as Any), ("requestChatDate", requestChatDate as Any), ("businessBotId", businessBotId as Any), ("businessBotManageUrl", businessBotManageUrl as Any)]) } } @@ -930,12 +932,18 @@ public extension Api { if Int(_1!) & Int(1 << 9) != 0 {_3 = parseString(reader) } var _4: Int32? if Int(_1!) & Int(1 << 9) != 0 {_4 = reader.readInt32() } + var _5: Int64? + if Int(_1!) & Int(1 << 13) != 0 {_5 = reader.readInt64() } + var _6: String? + if Int(_1!) & Int(1 << 13) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 6) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 9) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 9) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.PeerSettings.peerSettings(flags: _1!, geoDistance: _2, requestChatTitle: _3, requestChatDate: _4) + let _c5 = (Int(_1!) & Int(1 << 13) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 13) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.PeerSettings.peerSettings(flags: _1!, geoDistance: _2, requestChatTitle: _3, requestChatDate: _4, businessBotId: _5, businessBotManageUrl: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index e7175c51f5..7400b61fc3 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -513,7 +513,7 @@ public extension Api { case updateBotChatBoost(peer: Api.Peer, boost: Api.Boost, qts: Int32) case updateBotChatInviteRequester(peer: Api.Peer, date: Int32, userId: Int64, about: String, invite: Api.ExportedChatInvite, qts: Int32) case updateBotCommands(peer: Api.Peer, botId: Int64, commands: [Api.BotCommand]) - case updateBotDeleteBusinessMessage(connectionId: String, messages: [Int32], qts: Int32) + case updateBotDeleteBusinessMessage(connectionId: String, peer: Api.Peer, messages: [Int32], qts: Int32) case updateBotEditBusinessMessage(connectionId: String, message: Api.Message, qts: Int32) case updateBotInlineQuery(flags: Int32, queryId: Int64, userId: Int64, query: String, geo: Api.GeoPoint?, peerType: Api.InlineQueryPeerType?, offset: String) case updateBotInlineSend(flags: Int32, userId: Int64, query: String, geo: Api.GeoPoint?, id: String, msgId: Api.InputBotInlineMessageID?) @@ -707,11 +707,12 @@ public extension Api { item.serialize(buffer, true) } break - case .updateBotDeleteBusinessMessage(let connectionId, let messages, let qts): + case .updateBotDeleteBusinessMessage(let connectionId, let peer, let messages, let qts): if boxed { - buffer.appendInt32(-1590796039) + buffer.appendInt32(-1607821266) } serializeString(connectionId, buffer: buffer, boxed: false) + peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { @@ -1827,8 +1828,8 @@ public extension Api { return ("updateBotChatInviteRequester", [("peer", peer as Any), ("date", date as Any), ("userId", userId as Any), ("about", about as Any), ("invite", invite as Any), ("qts", qts as Any)]) case .updateBotCommands(let peer, let botId, let commands): return ("updateBotCommands", [("peer", peer as Any), ("botId", botId as Any), ("commands", commands as Any)]) - case .updateBotDeleteBusinessMessage(let connectionId, let messages, let qts): - return ("updateBotDeleteBusinessMessage", [("connectionId", connectionId as Any), ("messages", messages as Any), ("qts", qts as Any)]) + case .updateBotDeleteBusinessMessage(let connectionId, let peer, let messages, let qts): + return ("updateBotDeleteBusinessMessage", [("connectionId", connectionId as Any), ("peer", peer as Any), ("messages", messages as Any), ("qts", qts as Any)]) case .updateBotEditBusinessMessage(let connectionId, let message, let qts): return ("updateBotEditBusinessMessage", [("connectionId", connectionId as Any), ("message", message as Any), ("qts", qts as Any)]) case .updateBotInlineQuery(let flags, let queryId, let userId, let query, let geo, let peerType, let offset): @@ -2217,17 +2218,22 @@ public extension Api { public static func parse_updateBotDeleteBusinessMessage(_ reader: BufferReader) -> Update? { var _1: String? _1 = parseString(reader) - var _2: [Int32]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer } - var _3: Int32? - _3 = reader.readInt32() + var _3: [Int32]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _4: Int32? + _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.Update.updateBotDeleteBusinessMessage(connectionId: _1!, messages: _2!, qts: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.Update.updateBotDeleteBusinessMessage(connectionId: _1!, peer: _2!, messages: _3!, qts: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index e688ea037b..6967f4eb80 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -634,6 +634,62 @@ public extension Api.contacts { } } +public extension Api.fragment { + enum CollectibleInfo: TypeConstructorDescription { + case collectibleInfo(purchaseDate: Int32, currency: String, amount: Int64, cryptoCurrency: String, cryptoAmount: Int64, url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .collectibleInfo(let purchaseDate, let currency, let amount, let cryptoCurrency, let cryptoAmount, let url): + if boxed { + buffer.appendInt32(1857945489) + } + serializeInt32(purchaseDate, buffer: buffer, boxed: false) + serializeString(currency, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + serializeString(cryptoCurrency, buffer: buffer, boxed: false) + serializeInt64(cryptoAmount, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .collectibleInfo(let purchaseDate, let currency, let amount, let cryptoCurrency, let cryptoAmount, let url): + return ("collectibleInfo", [("purchaseDate", purchaseDate as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("url", url as Any)]) + } + } + + public static func parse_collectibleInfo(_ reader: BufferReader) -> CollectibleInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + var _6: String? + _6 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.fragment.CollectibleInfo.collectibleInfo(purchaseDate: _1!, currency: _2!, amount: _3!, cryptoCurrency: _4!, cryptoAmount: _5!, url: _6!) + } + else { + return nil + } + } + + } +} public extension Api.help { enum AppConfig: TypeConstructorDescription { case appConfig(hash: Int32, config: Api.JSONValue) @@ -1308,89 +1364,3 @@ public extension Api.help { } } -public extension Api.help { - enum PremiumPromo: TypeConstructorDescription { - case premiumPromo(statusText: String, statusEntities: [Api.MessageEntity], videoSections: [String], videos: [Api.Document], periodOptions: [Api.PremiumSubscriptionOption], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): - if boxed { - buffer.appendInt32(1395946908) - } - serializeString(statusText, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(statusEntities.count)) - for item in statusEntities { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(videoSections.count)) - for item in videoSections { - serializeString(item, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(videos.count)) - for item in videos { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(periodOptions.count)) - for item in periodOptions { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): - return ("premiumPromo", [("statusText", statusText as Any), ("statusEntities", statusEntities as Any), ("videoSections", videoSections as Any), ("videos", videos as Any), ("periodOptions", periodOptions as Any), ("users", users as Any)]) - } - } - - public static func parse_premiumPromo(_ reader: BufferReader) -> PremiumPromo? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.MessageEntity]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } - var _3: [String]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) - } - var _4: [Api.Document]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - var _5: [Api.PremiumSubscriptionOption]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumSubscriptionOption.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.help.PremiumPromo.premiumPromo(statusText: _1!, statusEntities: _2!, videoSections: _3!, videos: _4!, periodOptions: _5!, users: _6!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 5069e6504e..91159e86a9 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,89 @@ +public extension Api.help { + enum PremiumPromo: TypeConstructorDescription { + case premiumPromo(statusText: String, statusEntities: [Api.MessageEntity], videoSections: [String], videos: [Api.Document], periodOptions: [Api.PremiumSubscriptionOption], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): + if boxed { + buffer.appendInt32(1395946908) + } + serializeString(statusText, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(statusEntities.count)) + for item in statusEntities { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(videoSections.count)) + for item in videoSections { + serializeString(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(videos.count)) + for item in videos { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(periodOptions.count)) + for item in periodOptions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): + return ("premiumPromo", [("statusText", statusText as Any), ("statusEntities", statusEntities as Any), ("videoSections", videoSections as Any), ("videos", videos as Any), ("periodOptions", periodOptions as Any), ("users", users as Any)]) + } + } + + public static func parse_premiumPromo(_ reader: BufferReader) -> PremiumPromo? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageEntity]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + var _3: [String]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } + var _4: [Api.Document]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + var _5: [Api.PremiumSubscriptionOption]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumSubscriptionOption.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.help.PremiumPromo.premiumPromo(statusText: _1!, statusEntities: _2!, videoSections: _3!, videos: _4!, periodOptions: _5!, users: _6!) + } + else { + return nil + } + } + + } +} public extension Api.help { enum PromoData: TypeConstructorDescription { case promoData(flags: Int32, expires: Int32, peer: Api.Peer, chats: [Api.Chat], users: [Api.User], psaType: String?, psaMessage: String?) @@ -1290,49 +1376,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum DialogFilters: TypeConstructorDescription { - case dialogFilters(flags: Int32, filters: [Api.DialogFilter]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .dialogFilters(let flags, let filters): - if boxed { - buffer.appendInt32(718878489) - } - serializeInt32(flags, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(filters.count)) - for item in filters { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .dialogFilters(let flags, let filters): - return ("dialogFilters", [("flags", flags as Any), ("filters", filters as Any)]) - } - } - - public static func parse_dialogFilters(_ reader: BufferReader) -> DialogFilters? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.DialogFilter]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogFilter.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.DialogFilters.dialogFilters(flags: _1!, filters: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index cb218918b1..df72f59ee8 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,49 @@ +public extension Api.messages { + enum DialogFilters: TypeConstructorDescription { + case dialogFilters(flags: Int32, filters: [Api.DialogFilter]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .dialogFilters(let flags, let filters): + if boxed { + buffer.appendInt32(718878489) + } + serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(filters.count)) + for item in filters { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .dialogFilters(let flags, let filters): + return ("dialogFilters", [("flags", flags as Any), ("filters", filters as Any)]) + } + } + + public static func parse_dialogFilters(_ reader: BufferReader) -> DialogFilters? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.DialogFilter]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogFilter.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.DialogFilters.dialogFilters(flags: _1!, filters: _2!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum Dialogs: TypeConstructorDescription { case dialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) @@ -1304,6 +1350,52 @@ public extension Api.messages { } } +public extension Api.messages { + enum MyStickers: TypeConstructorDescription { + case myStickers(count: Int32, sets: [Api.StickerSetCovered]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .myStickers(let count, let sets): + if boxed { + buffer.appendInt32(-83926371) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .myStickers(let count, let sets): + return ("myStickers", [("count", count as Any), ("sets", sets as Any)]) + } + } + + public static func parse_myStickers(_ reader: BufferReader) -> MyStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.MyStickers.myStickers(count: _1!, sets: _2!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum PeerDialogs: TypeConstructorDescription { case peerDialogs(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) @@ -1524,61 +1616,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum Reactions: TypeConstructorDescription { - case reactions(hash: Int64, reactions: [Api.Reaction]) - case reactionsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .reactions(let hash, let reactions): - if boxed { - buffer.appendInt32(-352454890) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions.count)) - for item in reactions { - item.serialize(buffer, true) - } - break - case .reactionsNotModified: - if boxed { - buffer.appendInt32(-1334846497) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .reactions(let hash, let reactions): - return ("reactions", [("hash", hash as Any), ("reactions", reactions as Any)]) - case .reactionsNotModified: - return ("reactionsNotModified", []) - } - } - - public static func parse_reactions(_ reader: BufferReader) -> Reactions? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Reaction]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.Reactions.reactions(hash: _1!, reactions: _2!) - } - else { - return nil - } - } - public static func parse_reactionsNotModified(_ reader: BufferReader) -> Reactions? { - return Api.messages.Reactions.reactionsNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index fa6a398716..6e364e808a 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,61 @@ +public extension Api.messages { + enum Reactions: TypeConstructorDescription { + case reactions(hash: Int64, reactions: [Api.Reaction]) + case reactionsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .reactions(let hash, let reactions): + if boxed { + buffer.appendInt32(-352454890) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + break + case .reactionsNotModified: + if boxed { + buffer.appendInt32(-1334846497) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .reactions(let hash, let reactions): + return ("reactions", [("hash", hash as Any), ("reactions", reactions as Any)]) + case .reactionsNotModified: + return ("reactionsNotModified", []) + } + } + + public static func parse_reactions(_ reader: BufferReader) -> Reactions? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Reaction]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.Reactions.reactions(hash: _1!, reactions: _2!) + } + else { + return nil + } + } + public static func parse_reactionsNotModified(_ reader: BufferReader) -> Reactions? { + return Api.messages.Reactions.reactionsNotModified + } + + } +} public extension Api.messages { enum RecentStickers: TypeConstructorDescription { case recentStickers(hash: Int64, packs: [Api.StickerPack], stickers: [Api.Document], dates: [Int32]) @@ -1346,121 +1404,3 @@ public extension Api.payments { } } -public extension Api.payments { - enum PaymentForm: TypeConstructorDescription { - case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, additionalMethods: [Api.PaymentFormMethod]?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: [Api.PaymentSavedCredentials]?, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): - if boxed { - buffer.appendInt32(-1610250415) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(formId, buffer: buffer, boxed: false) - serializeInt64(botId, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)} - invoice.serialize(buffer, true) - serializeInt64(providerId, buffer: buffer, boxed: false) - serializeString(url, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 4) != 0 {serializeString(nativeProvider!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {nativeParams!.serialize(buffer, true)} - if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(additionalMethods!.count)) - for item in additionalMethods! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 0) != 0 {savedInfo!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(savedCredentials!.count)) - for item in savedCredentials! { - item.serialize(buffer, true) - }} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): - return ("paymentForm", [("flags", flags as Any), ("formId", formId as Any), ("botId", botId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("providerId", providerId as Any), ("url", url as Any), ("nativeProvider", nativeProvider as Any), ("nativeParams", nativeParams as Any), ("additionalMethods", additionalMethods as Any), ("savedInfo", savedInfo as Any), ("savedCredentials", savedCredentials as Any), ("users", users as Any)]) - } - } - - public static func parse_paymentForm(_ reader: BufferReader) -> PaymentForm? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int64? - _3 = reader.readInt64() - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: Api.WebDocument? - if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.WebDocument - } } - var _7: Api.Invoice? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.Invoice - } - var _8: Int64? - _8 = reader.readInt64() - var _9: String? - _9 = parseString(reader) - var _10: String? - if Int(_1!) & Int(1 << 4) != 0 {_10 = parseString(reader) } - var _11: Api.DataJSON? - if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { - _11 = Api.parse(reader, signature: signature) as? Api.DataJSON - } } - var _12: [Api.PaymentFormMethod]? - if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() { - _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentFormMethod.self) - } } - var _13: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _13 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo - } } - var _14: [Api.PaymentSavedCredentials]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentSavedCredentials.self) - } } - var _15: [Api.User]? - if let _ = reader.readInt32() { - _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 0) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 1) == 0) || _14 != nil - let _c15 = _15 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { - return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, additionalMethods: _12, savedInfo: _13, savedCredentials: _14, users: _15!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 3e5ca3e260..54afb14e4b 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,121 @@ +public extension Api.payments { + enum PaymentForm: TypeConstructorDescription { + case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, additionalMethods: [Api.PaymentFormMethod]?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: [Api.PaymentSavedCredentials]?, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): + if boxed { + buffer.appendInt32(-1610250415) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(formId, buffer: buffer, boxed: false) + serializeInt64(botId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)} + invoice.serialize(buffer, true) + serializeInt64(providerId, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 4) != 0 {serializeString(nativeProvider!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {nativeParams!.serialize(buffer, true)} + if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(additionalMethods!.count)) + for item in additionalMethods! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 0) != 0 {savedInfo!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(savedCredentials!.count)) + for item in savedCredentials! { + item.serialize(buffer, true) + }} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): + return ("paymentForm", [("flags", flags as Any), ("formId", formId as Any), ("botId", botId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("providerId", providerId as Any), ("url", url as Any), ("nativeProvider", nativeProvider as Any), ("nativeParams", nativeParams as Any), ("additionalMethods", additionalMethods as Any), ("savedInfo", savedInfo as Any), ("savedCredentials", savedCredentials as Any), ("users", users as Any)]) + } + } + + public static func parse_paymentForm(_ reader: BufferReader) -> PaymentForm? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Api.WebDocument? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _7: Api.Invoice? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Invoice + } + var _8: Int64? + _8 = reader.readInt64() + var _9: String? + _9 = parseString(reader) + var _10: String? + if Int(_1!) & Int(1 << 4) != 0 {_10 = parseString(reader) } + var _11: Api.DataJSON? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _11 = Api.parse(reader, signature: signature) as? Api.DataJSON + } } + var _12: [Api.PaymentFormMethod]? + if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() { + _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentFormMethod.self) + } } + var _13: Api.PaymentRequestedInfo? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _13 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + } } + var _14: [Api.PaymentSavedCredentials]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentSavedCredentials.self) + } } + var _15: [Api.User]? + if let _ = reader.readInt32() { + _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 0) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 1) == 0) || _14 != nil + let _c15 = _15 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { + return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, additionalMethods: _12, savedInfo: _13, savedCredentials: _14, users: _15!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum PaymentReceipt: TypeConstructorDescription { case paymentReceipt(flags: Int32, date: Int32, botId: Int64, providerId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, info: Api.PaymentRequestedInfo?, shipping: Api.ShippingOption?, tipAmount: Int64?, currency: String, totalAmount: Int64, credentialsTitle: String, users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index 5b224f22fc..9338176e30 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -221,6 +221,21 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func disablePeerConnectedBot(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1581481689) + peer.serialize(buffer, true) + return (FunctionDescription(name: "account.disablePeerConnectedBot", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func finishTakeoutSession(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -1254,6 +1269,22 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func toggleConnectedBotPaused(peer: Api.InputPeer, paused: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1684934807) + peer.serialize(buffer, true) + paused.serialize(buffer, true) + return (FunctionDescription(name: "account.toggleConnectedBotPaused", parameters: [("peer", String(describing: peer)), ("paused", String(describing: paused))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func toggleUsername(username: String, active: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3934,6 +3965,21 @@ public extension Api.functions.folders { }) } } +public extension Api.functions.fragment { + static func getCollectibleInfo(collectible: Api.InputCollectible) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1105295942) + collectible.serialize(buffer, true) + return (FunctionDescription(name: "fragment.getCollectibleInfo", parameters: [("collectible", String(describing: collectible))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.fragment.CollectibleInfo? in + let reader = BufferReader(buffer) + var result: Api.fragment.CollectibleInfo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.fragment.CollectibleInfo + } + return result + }) + } +} public extension Api.functions.help { static func acceptTermsOfService(id: Api.DataJSON) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5851,6 +5897,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getMyStickers(offsetId: Int64, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-793386500) + serializeInt64(offsetId, buffer: buffer, boxed: false) + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getMyStickers", parameters: [("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.MyStickers? in + let reader = BufferReader(buffer) + var result: Api.messages.MyStickers? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.MyStickers + } + return result + }) + } +} public extension Api.functions.messages { static func getOldFeaturedStickers(offset: Int32, limit: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7385,12 +7447,22 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendQuickReplyMessages(peer: Api.InputPeer, shortcutId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendQuickReplyMessages(peer: Api.InputPeer, shortcutId: Int32, id: [Int32], randomId: [Int64]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(857029332) + buffer.appendInt32(1819610593) peer.serialize(buffer, true) serializeInt32(shortcutId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "messages.sendQuickReplyMessages", parameters: [("peer", String(describing: peer)), ("shortcutId", String(describing: shortcutId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(id.count)) + for item in id { + serializeInt32(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomId.count)) + for item in randomId { + serializeInt64(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "messages.sendQuickReplyMessages", parameters: [("peer", String(describing: peer)), ("shortcutId", String(describing: shortcutId)), ("id", String(describing: id)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -8095,12 +8167,14 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func uploadMedia(peer: Api.InputPeer, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func uploadMedia(flags: Int32, businessConnectionId: String?, peer: Api.InputPeer, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1369162417) + buffer.appendInt32(345405816) + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(businessConnectionId!, buffer: buffer, boxed: false)} peer.serialize(buffer, true) media.serialize(buffer, true) - return (FunctionDescription(name: "messages.uploadMedia", parameters: [("peer", String(describing: peer)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in + return (FunctionDescription(name: "messages.uploadMedia", parameters: [("flags", String(describing: flags)), ("businessConnectionId", String(describing: businessConnectionId)), ("peer", String(describing: peer)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in let reader = BufferReader(buffer) var result: Api.MessageMedia? if let signature = reader.readInt32() { @@ -9444,6 +9518,22 @@ public extension Api.functions.stickers { }) } } +public extension Api.functions.stickers { + static func replaceSticker(sticker: Api.InputDocument, newSticker: Api.InputStickerSetItem) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1184253338) + sticker.serialize(buffer, true) + newSticker.serialize(buffer, true) + return (FunctionDescription(name: "stickers.replaceSticker", parameters: [("sticker", String(describing: sticker)), ("newSticker", String(describing: newSticker))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.StickerSet? in + let reader = BufferReader(buffer) + var result: Api.messages.StickerSet? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.StickerSet + } + return result + }) + } +} public extension Api.functions.stickers { static func setStickerSetThumb(flags: Int32, stickerset: Api.InputStickerSet, thumb: Api.InputDocument?, thumbDocumentId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index cbdf0ebf38..7ad68b056b 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -712,6 +712,62 @@ public extension Api { } } +public extension Api { + enum InputCollectible: TypeConstructorDescription { + case inputCollectiblePhone(phone: String) + case inputCollectibleUsername(username: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputCollectiblePhone(let phone): + if boxed { + buffer.appendInt32(-1562241884) + } + serializeString(phone, buffer: buffer, boxed: false) + break + case .inputCollectibleUsername(let username): + if boxed { + buffer.appendInt32(-476815191) + } + serializeString(username, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputCollectiblePhone(let phone): + return ("inputCollectiblePhone", [("phone", phone as Any)]) + case .inputCollectibleUsername(let username): + return ("inputCollectibleUsername", [("username", username as Any)]) + } + } + + public static func parse_inputCollectiblePhone(_ reader: BufferReader) -> InputCollectible? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputCollectible.inputCollectiblePhone(phone: _1!) + } + else { + return nil + } + } + public static func parse_inputCollectibleUsername(_ reader: BufferReader) -> InputCollectible? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputCollectible.inputCollectibleUsername(username: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputContact: TypeConstructorDescription { case inputPhoneContact(clientId: Int64, phone: String, firstName: String, lastName: String) @@ -1102,313 +1158,3 @@ public extension Api { } } -public extension Api { - indirect enum InputFileLocation: TypeConstructorDescription { - case inputDocumentFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, thumbSize: String) - case inputEncryptedFileLocation(id: Int64, accessHash: Int64) - case inputFileLocation(volumeId: Int64, localId: Int32, secret: Int64, fileReference: Buffer) - case inputGroupCallStream(flags: Int32, call: Api.InputGroupCall, timeMs: Int64, scale: Int32, videoChannel: Int32?, videoQuality: Int32?) - case inputPeerPhotoFileLocation(flags: Int32, peer: Api.InputPeer, photoId: Int64) - case inputPhotoFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, thumbSize: String) - case inputPhotoLegacyFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, volumeId: Int64, localId: Int32, secret: Int64) - case inputSecureFileLocation(id: Int64, accessHash: Int64) - case inputStickerSetThumb(stickerset: Api.InputStickerSet, thumbVersion: Int32) - case inputTakeoutFileLocation - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputDocumentFileLocation(let id, let accessHash, let fileReference, let thumbSize): - if boxed { - buffer.appendInt32(-1160743548) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeBytes(fileReference, buffer: buffer, boxed: false) - serializeString(thumbSize, buffer: buffer, boxed: false) - break - case .inputEncryptedFileLocation(let id, let accessHash): - if boxed { - buffer.appendInt32(-182231723) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputFileLocation(let volumeId, let localId, let secret, let fileReference): - if boxed { - buffer.appendInt32(-539317279) - } - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - serializeBytes(fileReference, buffer: buffer, boxed: false) - break - case .inputGroupCallStream(let flags, let call, let timeMs, let scale, let videoChannel, let videoQuality): - if boxed { - buffer.appendInt32(93890858) - } - serializeInt32(flags, buffer: buffer, boxed: false) - call.serialize(buffer, true) - serializeInt64(timeMs, buffer: buffer, boxed: false) - serializeInt32(scale, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(videoChannel!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(videoQuality!, buffer: buffer, boxed: false)} - break - case .inputPeerPhotoFileLocation(let flags, let peer, let photoId): - if boxed { - buffer.appendInt32(925204121) - } - serializeInt32(flags, buffer: buffer, boxed: false) - peer.serialize(buffer, true) - serializeInt64(photoId, buffer: buffer, boxed: false) - break - case .inputPhotoFileLocation(let id, let accessHash, let fileReference, let thumbSize): - if boxed { - buffer.appendInt32(1075322878) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeBytes(fileReference, buffer: buffer, boxed: false) - serializeString(thumbSize, buffer: buffer, boxed: false) - break - case .inputPhotoLegacyFileLocation(let id, let accessHash, let fileReference, let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(-667654413) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeBytes(fileReference, buffer: buffer, boxed: false) - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - case .inputSecureFileLocation(let id, let accessHash): - if boxed { - buffer.appendInt32(-876089816) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputStickerSetThumb(let stickerset, let thumbVersion): - if boxed { - buffer.appendInt32(-1652231205) - } - stickerset.serialize(buffer, true) - serializeInt32(thumbVersion, buffer: buffer, boxed: false) - break - case .inputTakeoutFileLocation: - if boxed { - buffer.appendInt32(700340377) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputDocumentFileLocation(let id, let accessHash, let fileReference, let thumbSize): - return ("inputDocumentFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("thumbSize", thumbSize as Any)]) - case .inputEncryptedFileLocation(let id, let accessHash): - return ("inputEncryptedFileLocation", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputFileLocation(let volumeId, let localId, let secret, let fileReference): - return ("inputFileLocation", [("volumeId", volumeId as Any), ("localId", localId as Any), ("secret", secret as Any), ("fileReference", fileReference as Any)]) - case .inputGroupCallStream(let flags, let call, let timeMs, let scale, let videoChannel, let videoQuality): - return ("inputGroupCallStream", [("flags", flags as Any), ("call", call as Any), ("timeMs", timeMs as Any), ("scale", scale as Any), ("videoChannel", videoChannel as Any), ("videoQuality", videoQuality as Any)]) - case .inputPeerPhotoFileLocation(let flags, let peer, let photoId): - return ("inputPeerPhotoFileLocation", [("flags", flags as Any), ("peer", peer as Any), ("photoId", photoId as Any)]) - case .inputPhotoFileLocation(let id, let accessHash, let fileReference, let thumbSize): - return ("inputPhotoFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("thumbSize", thumbSize as Any)]) - case .inputPhotoLegacyFileLocation(let id, let accessHash, let fileReference, let volumeId, let localId, let secret): - return ("inputPhotoLegacyFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("volumeId", volumeId as Any), ("localId", localId as Any), ("secret", secret as Any)]) - case .inputSecureFileLocation(let id, let accessHash): - return ("inputSecureFileLocation", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputStickerSetThumb(let stickerset, let thumbVersion): - return ("inputStickerSetThumb", [("stickerset", stickerset as Any), ("thumbVersion", thumbVersion as Any)]) - case .inputTakeoutFileLocation: - return ("inputTakeoutFileLocation", []) - } - } - - public static func parse_inputDocumentFileLocation(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - var _3: Buffer? - _3 = parseBytes(reader) - var _4: String? - _4 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputFileLocation.inputDocumentFileLocation(id: _1!, accessHash: _2!, fileReference: _3!, thumbSize: _4!) - } - else { - return nil - } - } - public static func parse_inputEncryptedFileLocation(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputFileLocation.inputEncryptedFileLocation(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputFileLocation(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - var _4: Buffer? - _4 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputFileLocation.inputFileLocation(volumeId: _1!, localId: _2!, secret: _3!, fileReference: _4!) - } - else { - return nil - } - } - public static func parse_inputGroupCallStream(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.InputGroupCall? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.InputGroupCall - } - var _3: Int64? - _3 = reader.readInt64() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } - var _6: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.InputFileLocation.inputGroupCallStream(flags: _1!, call: _2!, timeMs: _3!, scale: _4!, videoChannel: _5, videoQuality: _6) - } - else { - return nil - } - } - public static func parse_inputPeerPhotoFileLocation(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.InputPeer? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputFileLocation.inputPeerPhotoFileLocation(flags: _1!, peer: _2!, photoId: _3!) - } - else { - return nil - } - } - public static func parse_inputPhotoFileLocation(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - var _3: Buffer? - _3 = parseBytes(reader) - var _4: String? - _4 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputFileLocation.inputPhotoFileLocation(id: _1!, accessHash: _2!, fileReference: _3!, thumbSize: _4!) - } - else { - return nil - } - } - public static func parse_inputPhotoLegacyFileLocation(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - var _3: Buffer? - _3 = parseBytes(reader) - var _4: Int64? - _4 = reader.readInt64() - var _5: Int32? - _5 = reader.readInt32() - var _6: Int64? - _6 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.InputFileLocation.inputPhotoLegacyFileLocation(id: _1!, accessHash: _2!, fileReference: _3!, volumeId: _4!, localId: _5!, secret: _6!) - } - else { - return nil - } - } - public static func parse_inputSecureFileLocation(_ reader: BufferReader) -> InputFileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputFileLocation.inputSecureFileLocation(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputStickerSetThumb(_ reader: BufferReader) -> InputFileLocation? { - var _1: Api.InputStickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputStickerSet - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputFileLocation.inputStickerSetThumb(stickerset: _1!, thumbVersion: _2!) - } - else { - return nil - } - } - public static func parse_inputTakeoutFileLocation(_ reader: BufferReader) -> InputFileLocation? { - return Api.InputFileLocation.inputTakeoutFileLocation - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index 8048250b31..3276fce952 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -1,3 +1,313 @@ +public extension Api { + indirect enum InputFileLocation: TypeConstructorDescription { + case inputDocumentFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, thumbSize: String) + case inputEncryptedFileLocation(id: Int64, accessHash: Int64) + case inputFileLocation(volumeId: Int64, localId: Int32, secret: Int64, fileReference: Buffer) + case inputGroupCallStream(flags: Int32, call: Api.InputGroupCall, timeMs: Int64, scale: Int32, videoChannel: Int32?, videoQuality: Int32?) + case inputPeerPhotoFileLocation(flags: Int32, peer: Api.InputPeer, photoId: Int64) + case inputPhotoFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, thumbSize: String) + case inputPhotoLegacyFileLocation(id: Int64, accessHash: Int64, fileReference: Buffer, volumeId: Int64, localId: Int32, secret: Int64) + case inputSecureFileLocation(id: Int64, accessHash: Int64) + case inputStickerSetThumb(stickerset: Api.InputStickerSet, thumbVersion: Int32) + case inputTakeoutFileLocation + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputDocumentFileLocation(let id, let accessHash, let fileReference, let thumbSize): + if boxed { + buffer.appendInt32(-1160743548) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeBytes(fileReference, buffer: buffer, boxed: false) + serializeString(thumbSize, buffer: buffer, boxed: false) + break + case .inputEncryptedFileLocation(let id, let accessHash): + if boxed { + buffer.appendInt32(-182231723) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputFileLocation(let volumeId, let localId, let secret, let fileReference): + if boxed { + buffer.appendInt32(-539317279) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + serializeBytes(fileReference, buffer: buffer, boxed: false) + break + case .inputGroupCallStream(let flags, let call, let timeMs, let scale, let videoChannel, let videoQuality): + if boxed { + buffer.appendInt32(93890858) + } + serializeInt32(flags, buffer: buffer, boxed: false) + call.serialize(buffer, true) + serializeInt64(timeMs, buffer: buffer, boxed: false) + serializeInt32(scale, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(videoChannel!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(videoQuality!, buffer: buffer, boxed: false)} + break + case .inputPeerPhotoFileLocation(let flags, let peer, let photoId): + if boxed { + buffer.appendInt32(925204121) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt64(photoId, buffer: buffer, boxed: false) + break + case .inputPhotoFileLocation(let id, let accessHash, let fileReference, let thumbSize): + if boxed { + buffer.appendInt32(1075322878) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeBytes(fileReference, buffer: buffer, boxed: false) + serializeString(thumbSize, buffer: buffer, boxed: false) + break + case .inputPhotoLegacyFileLocation(let id, let accessHash, let fileReference, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(-667654413) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeBytes(fileReference, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .inputSecureFileLocation(let id, let accessHash): + if boxed { + buffer.appendInt32(-876089816) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputStickerSetThumb(let stickerset, let thumbVersion): + if boxed { + buffer.appendInt32(-1652231205) + } + stickerset.serialize(buffer, true) + serializeInt32(thumbVersion, buffer: buffer, boxed: false) + break + case .inputTakeoutFileLocation: + if boxed { + buffer.appendInt32(700340377) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputDocumentFileLocation(let id, let accessHash, let fileReference, let thumbSize): + return ("inputDocumentFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("thumbSize", thumbSize as Any)]) + case .inputEncryptedFileLocation(let id, let accessHash): + return ("inputEncryptedFileLocation", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputFileLocation(let volumeId, let localId, let secret, let fileReference): + return ("inputFileLocation", [("volumeId", volumeId as Any), ("localId", localId as Any), ("secret", secret as Any), ("fileReference", fileReference as Any)]) + case .inputGroupCallStream(let flags, let call, let timeMs, let scale, let videoChannel, let videoQuality): + return ("inputGroupCallStream", [("flags", flags as Any), ("call", call as Any), ("timeMs", timeMs as Any), ("scale", scale as Any), ("videoChannel", videoChannel as Any), ("videoQuality", videoQuality as Any)]) + case .inputPeerPhotoFileLocation(let flags, let peer, let photoId): + return ("inputPeerPhotoFileLocation", [("flags", flags as Any), ("peer", peer as Any), ("photoId", photoId as Any)]) + case .inputPhotoFileLocation(let id, let accessHash, let fileReference, let thumbSize): + return ("inputPhotoFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("thumbSize", thumbSize as Any)]) + case .inputPhotoLegacyFileLocation(let id, let accessHash, let fileReference, let volumeId, let localId, let secret): + return ("inputPhotoLegacyFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("volumeId", volumeId as Any), ("localId", localId as Any), ("secret", secret as Any)]) + case .inputSecureFileLocation(let id, let accessHash): + return ("inputSecureFileLocation", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputStickerSetThumb(let stickerset, let thumbVersion): + return ("inputStickerSetThumb", [("stickerset", stickerset as Any), ("thumbVersion", thumbVersion as Any)]) + case .inputTakeoutFileLocation: + return ("inputTakeoutFileLocation", []) + } + } + + public static func parse_inputDocumentFileLocation(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Buffer? + _3 = parseBytes(reader) + var _4: String? + _4 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputFileLocation.inputDocumentFileLocation(id: _1!, accessHash: _2!, fileReference: _3!, thumbSize: _4!) + } + else { + return nil + } + } + public static func parse_inputEncryptedFileLocation(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputFileLocation.inputEncryptedFileLocation(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputFileLocation(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + var _4: Buffer? + _4 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputFileLocation.inputFileLocation(volumeId: _1!, localId: _2!, secret: _3!, fileReference: _4!) + } + else { + return nil + } + } + public static func parse_inputGroupCallStream(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputGroupCall? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputGroupCall + } + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() } + var _6: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputFileLocation.inputGroupCallStream(flags: _1!, call: _2!, timeMs: _3!, scale: _4!, videoChannel: _5, videoQuality: _6) + } + else { + return nil + } + } + public static func parse_inputPeerPhotoFileLocation(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputPeer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputFileLocation.inputPeerPhotoFileLocation(flags: _1!, peer: _2!, photoId: _3!) + } + else { + return nil + } + } + public static func parse_inputPhotoFileLocation(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Buffer? + _3 = parseBytes(reader) + var _4: String? + _4 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputFileLocation.inputPhotoFileLocation(id: _1!, accessHash: _2!, fileReference: _3!, thumbSize: _4!) + } + else { + return nil + } + } + public static func parse_inputPhotoLegacyFileLocation(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Buffer? + _3 = parseBytes(reader) + var _4: Int64? + _4 = reader.readInt64() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int64? + _6 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputFileLocation.inputPhotoLegacyFileLocation(id: _1!, accessHash: _2!, fileReference: _3!, volumeId: _4!, localId: _5!, secret: _6!) + } + else { + return nil + } + } + public static func parse_inputSecureFileLocation(_ reader: BufferReader) -> InputFileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputFileLocation.inputSecureFileLocation(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputStickerSetThumb(_ reader: BufferReader) -> InputFileLocation? { + var _1: Api.InputStickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputStickerSet + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputFileLocation.inputStickerSetThumb(stickerset: _1!, thumbVersion: _2!) + } + else { + return nil + } + } + public static func parse_inputTakeoutFileLocation(_ reader: BufferReader) -> InputFileLocation? { + return Api.InputFileLocation.inputTakeoutFileLocation + } + + } +} public extension Api { indirect enum InputFolderPeer: TypeConstructorDescription { case inputFolderPeer(peer: Api.InputPeer, folderId: Int32) @@ -884,195 +1194,3 @@ public extension Api { } } -public extension Api { - enum InputMessage: TypeConstructorDescription { - case inputMessageCallbackQuery(id: Int32, queryId: Int64) - case inputMessageID(id: Int32) - case inputMessagePinned - case inputMessageReplyTo(id: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputMessageCallbackQuery(let id, let queryId): - if boxed { - buffer.appendInt32(-1392895362) - } - serializeInt32(id, buffer: buffer, boxed: false) - serializeInt64(queryId, buffer: buffer, boxed: false) - break - case .inputMessageID(let id): - if boxed { - buffer.appendInt32(-1502174430) - } - serializeInt32(id, buffer: buffer, boxed: false) - break - case .inputMessagePinned: - if boxed { - buffer.appendInt32(-2037963464) - } - - break - case .inputMessageReplyTo(let id): - if boxed { - buffer.appendInt32(-1160215659) - } - serializeInt32(id, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputMessageCallbackQuery(let id, let queryId): - return ("inputMessageCallbackQuery", [("id", id as Any), ("queryId", queryId as Any)]) - case .inputMessageID(let id): - return ("inputMessageID", [("id", id as Any)]) - case .inputMessagePinned: - return ("inputMessagePinned", []) - case .inputMessageReplyTo(let id): - return ("inputMessageReplyTo", [("id", id as Any)]) - } - } - - public static func parse_inputMessageCallbackQuery(_ reader: BufferReader) -> InputMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputMessage.inputMessageCallbackQuery(id: _1!, queryId: _2!) - } - else { - return nil - } - } - public static func parse_inputMessageID(_ reader: BufferReader) -> InputMessage? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.InputMessage.inputMessageID(id: _1!) - } - else { - return nil - } - } - public static func parse_inputMessagePinned(_ reader: BufferReader) -> InputMessage? { - return Api.InputMessage.inputMessagePinned - } - public static func parse_inputMessageReplyTo(_ reader: BufferReader) -> InputMessage? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.InputMessage.inputMessageReplyTo(id: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - indirect enum InputNotifyPeer: TypeConstructorDescription { - case inputNotifyBroadcasts - case inputNotifyChats - case inputNotifyForumTopic(peer: Api.InputPeer, topMsgId: Int32) - case inputNotifyPeer(peer: Api.InputPeer) - case inputNotifyUsers - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputNotifyBroadcasts: - if boxed { - buffer.appendInt32(-1311015810) - } - - break - case .inputNotifyChats: - if boxed { - buffer.appendInt32(1251338318) - } - - break - case .inputNotifyForumTopic(let peer, let topMsgId): - if boxed { - buffer.appendInt32(1548122514) - } - peer.serialize(buffer, true) - serializeInt32(topMsgId, buffer: buffer, boxed: false) - break - case .inputNotifyPeer(let peer): - if boxed { - buffer.appendInt32(-1195615476) - } - peer.serialize(buffer, true) - break - case .inputNotifyUsers: - if boxed { - buffer.appendInt32(423314455) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputNotifyBroadcasts: - return ("inputNotifyBroadcasts", []) - case .inputNotifyChats: - return ("inputNotifyChats", []) - case .inputNotifyForumTopic(let peer, let topMsgId): - return ("inputNotifyForumTopic", [("peer", peer as Any), ("topMsgId", topMsgId as Any)]) - case .inputNotifyPeer(let peer): - return ("inputNotifyPeer", [("peer", peer as Any)]) - case .inputNotifyUsers: - return ("inputNotifyUsers", []) - } - } - - public static func parse_inputNotifyBroadcasts(_ reader: BufferReader) -> InputNotifyPeer? { - return Api.InputNotifyPeer.inputNotifyBroadcasts - } - public static func parse_inputNotifyChats(_ reader: BufferReader) -> InputNotifyPeer? { - return Api.InputNotifyPeer.inputNotifyChats - } - public static func parse_inputNotifyForumTopic(_ reader: BufferReader) -> InputNotifyPeer? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputNotifyPeer.inputNotifyForumTopic(peer: _1!, topMsgId: _2!) - } - else { - return nil - } - } - public static func parse_inputNotifyPeer(_ reader: BufferReader) -> InputNotifyPeer? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - let _c1 = _1 != nil - if _c1 { - return Api.InputNotifyPeer.inputNotifyPeer(peer: _1!) - } - else { - return nil - } - } - public static func parse_inputNotifyUsers(_ reader: BufferReader) -> InputNotifyPeer? { - return Api.InputNotifyPeer.inputNotifyUsers - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 8605e3757f..b838506bde 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -1,3 +1,195 @@ +public extension Api { + enum InputMessage: TypeConstructorDescription { + case inputMessageCallbackQuery(id: Int32, queryId: Int64) + case inputMessageID(id: Int32) + case inputMessagePinned + case inputMessageReplyTo(id: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputMessageCallbackQuery(let id, let queryId): + if boxed { + buffer.appendInt32(-1392895362) + } + serializeInt32(id, buffer: buffer, boxed: false) + serializeInt64(queryId, buffer: buffer, boxed: false) + break + case .inputMessageID(let id): + if boxed { + buffer.appendInt32(-1502174430) + } + serializeInt32(id, buffer: buffer, boxed: false) + break + case .inputMessagePinned: + if boxed { + buffer.appendInt32(-2037963464) + } + + break + case .inputMessageReplyTo(let id): + if boxed { + buffer.appendInt32(-1160215659) + } + serializeInt32(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputMessageCallbackQuery(let id, let queryId): + return ("inputMessageCallbackQuery", [("id", id as Any), ("queryId", queryId as Any)]) + case .inputMessageID(let id): + return ("inputMessageID", [("id", id as Any)]) + case .inputMessagePinned: + return ("inputMessagePinned", []) + case .inputMessageReplyTo(let id): + return ("inputMessageReplyTo", [("id", id as Any)]) + } + } + + public static func parse_inputMessageCallbackQuery(_ reader: BufferReader) -> InputMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputMessage.inputMessageCallbackQuery(id: _1!, queryId: _2!) + } + else { + return nil + } + } + public static func parse_inputMessageID(_ reader: BufferReader) -> InputMessage? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputMessage.inputMessageID(id: _1!) + } + else { + return nil + } + } + public static func parse_inputMessagePinned(_ reader: BufferReader) -> InputMessage? { + return Api.InputMessage.inputMessagePinned + } + public static func parse_inputMessageReplyTo(_ reader: BufferReader) -> InputMessage? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputMessage.inputMessageReplyTo(id: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + indirect enum InputNotifyPeer: TypeConstructorDescription { + case inputNotifyBroadcasts + case inputNotifyChats + case inputNotifyForumTopic(peer: Api.InputPeer, topMsgId: Int32) + case inputNotifyPeer(peer: Api.InputPeer) + case inputNotifyUsers + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputNotifyBroadcasts: + if boxed { + buffer.appendInt32(-1311015810) + } + + break + case .inputNotifyChats: + if boxed { + buffer.appendInt32(1251338318) + } + + break + case .inputNotifyForumTopic(let peer, let topMsgId): + if boxed { + buffer.appendInt32(1548122514) + } + peer.serialize(buffer, true) + serializeInt32(topMsgId, buffer: buffer, boxed: false) + break + case .inputNotifyPeer(let peer): + if boxed { + buffer.appendInt32(-1195615476) + } + peer.serialize(buffer, true) + break + case .inputNotifyUsers: + if boxed { + buffer.appendInt32(423314455) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputNotifyBroadcasts: + return ("inputNotifyBroadcasts", []) + case .inputNotifyChats: + return ("inputNotifyChats", []) + case .inputNotifyForumTopic(let peer, let topMsgId): + return ("inputNotifyForumTopic", [("peer", peer as Any), ("topMsgId", topMsgId as Any)]) + case .inputNotifyPeer(let peer): + return ("inputNotifyPeer", [("peer", peer as Any)]) + case .inputNotifyUsers: + return ("inputNotifyUsers", []) + } + } + + public static func parse_inputNotifyBroadcasts(_ reader: BufferReader) -> InputNotifyPeer? { + return Api.InputNotifyPeer.inputNotifyBroadcasts + } + public static func parse_inputNotifyChats(_ reader: BufferReader) -> InputNotifyPeer? { + return Api.InputNotifyPeer.inputNotifyChats + } + public static func parse_inputNotifyForumTopic(_ reader: BufferReader) -> InputNotifyPeer? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputNotifyPeer.inputNotifyForumTopic(peer: _1!, topMsgId: _2!) + } + else { + return nil + } + } + public static func parse_inputNotifyPeer(_ reader: BufferReader) -> InputNotifyPeer? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + let _c1 = _1 != nil + if _c1 { + return Api.InputNotifyPeer.inputNotifyPeer(peer: _1!) + } + else { + return nil + } + } + public static func parse_inputNotifyUsers(_ reader: BufferReader) -> InputNotifyPeer? { + return Api.InputNotifyPeer.inputNotifyUsers + } + + } +} public extension Api { enum InputPaymentCredentials: TypeConstructorDescription { case inputPaymentCredentials(flags: Int32, data: Api.DataJSON) @@ -584,183 +776,3 @@ public extension Api { } } -public extension Api { - enum InputPrivacyRule: TypeConstructorDescription { - case inputPrivacyValueAllowAll - case inputPrivacyValueAllowChatParticipants(chats: [Int64]) - case inputPrivacyValueAllowCloseFriends - case inputPrivacyValueAllowContacts - case inputPrivacyValueAllowUsers(users: [Api.InputUser]) - case inputPrivacyValueDisallowAll - case inputPrivacyValueDisallowChatParticipants(chats: [Int64]) - case inputPrivacyValueDisallowContacts - case inputPrivacyValueDisallowUsers(users: [Api.InputUser]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPrivacyValueAllowAll: - if boxed { - buffer.appendInt32(407582158) - } - - break - case .inputPrivacyValueAllowChatParticipants(let chats): - if boxed { - buffer.appendInt32(-2079962673) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .inputPrivacyValueAllowCloseFriends: - if boxed { - buffer.appendInt32(793067081) - } - - break - case .inputPrivacyValueAllowContacts: - if boxed { - buffer.appendInt32(218751099) - } - - break - case .inputPrivacyValueAllowUsers(let users): - if boxed { - buffer.appendInt32(320652927) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .inputPrivacyValueDisallowAll: - if boxed { - buffer.appendInt32(-697604407) - } - - break - case .inputPrivacyValueDisallowChatParticipants(let chats): - if boxed { - buffer.appendInt32(-380694650) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .inputPrivacyValueDisallowContacts: - if boxed { - buffer.appendInt32(195371015) - } - - break - case .inputPrivacyValueDisallowUsers(let users): - if boxed { - buffer.appendInt32(-1877932953) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputPrivacyValueAllowAll: - return ("inputPrivacyValueAllowAll", []) - case .inputPrivacyValueAllowChatParticipants(let chats): - return ("inputPrivacyValueAllowChatParticipants", [("chats", chats as Any)]) - case .inputPrivacyValueAllowCloseFriends: - return ("inputPrivacyValueAllowCloseFriends", []) - case .inputPrivacyValueAllowContacts: - return ("inputPrivacyValueAllowContacts", []) - case .inputPrivacyValueAllowUsers(let users): - return ("inputPrivacyValueAllowUsers", [("users", users as Any)]) - case .inputPrivacyValueDisallowAll: - return ("inputPrivacyValueDisallowAll", []) - case .inputPrivacyValueDisallowChatParticipants(let chats): - return ("inputPrivacyValueDisallowChatParticipants", [("chats", chats as Any)]) - case .inputPrivacyValueDisallowContacts: - return ("inputPrivacyValueDisallowContacts", []) - case .inputPrivacyValueDisallowUsers(let users): - return ("inputPrivacyValueDisallowUsers", [("users", users as Any)]) - } - } - - public static func parse_inputPrivacyValueAllowAll(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowAll - } - public static func parse_inputPrivacyValueAllowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(chats: _1!) - } - else { - return nil - } - } - public static func parse_inputPrivacyValueAllowCloseFriends(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowCloseFriends - } - public static func parse_inputPrivacyValueAllowContacts(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueAllowContacts - } - public static func parse_inputPrivacyValueAllowUsers(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Api.InputUser]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueAllowUsers(users: _1!) - } - else { - return nil - } - } - public static func parse_inputPrivacyValueDisallowAll(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueDisallowAll - } - public static func parse_inputPrivacyValueDisallowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(chats: _1!) - } - else { - return nil - } - } - public static func parse_inputPrivacyValueDisallowContacts(_ reader: BufferReader) -> InputPrivacyRule? { - return Api.InputPrivacyRule.inputPrivacyValueDisallowContacts - } - public static func parse_inputPrivacyValueDisallowUsers(_ reader: BufferReader) -> InputPrivacyRule? { - var _1: [Api.InputUser]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPrivacyRule.inputPrivacyValueDisallowUsers(users: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index 55646590ff..186d35e138 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -686,6 +686,9 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) { } + func setDidBeginDragging(_ f: (() -> Void)?) { + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 6ed005ceef..1114aaa8bc 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -144,13 +144,13 @@ public class UnauthorizedAccount { return accountManager.transaction { transaction -> (LocalizationSettings?, ProxySettings?) in return (transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self), transaction.getSharedData(SharedDataKeys.proxySettings)?.get(ProxySettings.self)) } - |> mapToSignal { localizationSettings, proxySettings -> Signal<(LocalizationSettings?, ProxySettings?, NetworkSettings?), NoError> in - return self.postbox.transaction { transaction -> (LocalizationSettings?, ProxySettings?, NetworkSettings?) in - return (localizationSettings, proxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings)?.get(NetworkSettings.self)) + |> mapToSignal { localizationSettings, proxySettings -> Signal<(LocalizationSettings?, ProxySettings?, NetworkSettings?, AppConfiguration), NoError> in + return self.postbox.transaction { transaction -> (LocalizationSettings?, ProxySettings?, NetworkSettings?, AppConfiguration) in + return (localizationSettings, proxySettings, transaction.getPreferencesEntry(key: PreferencesKeys.networkSettings)?.get(NetworkSettings.self), transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue) } } - |> mapToSignal { (localizationSettings, proxySettings, networkSettings) -> Signal in - return initializedNetwork(accountId: self.id, arguments: self.networkArguments, supplementary: false, datacenterId: Int(masterDatacenterId), keychain: keychain, basePath: self.basePath, testingEnvironment: self.testingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: false) + |> mapToSignal { localizationSettings, proxySettings, networkSettings, appConfiguration -> Signal in + return initializedNetwork(accountId: self.id, arguments: self.networkArguments, supplementary: false, datacenterId: Int(masterDatacenterId), keychain: keychain, basePath: self.basePath, testingEnvironment: self.testingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: nil, useRequestTimeoutTimers: false, appConfiguration: appConfiguration) |> map { network in let updated = UnauthorizedAccount(networkArguments: self.networkArguments, id: self.id, rootPath: self.rootPath, basePath: self.basePath, testingEnvironment: self.testingEnvironment, postbox: self.postbox, network: network) updated.shouldBeServiceTaskMaster.set(self.shouldBeServiceTaskMaster.get()) @@ -248,7 +248,7 @@ public func accountWithId(accountManager: AccountManager map { network -> AccountResult in return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: unauthorizedState.isTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } @@ -257,7 +257,7 @@ public func accountWithId(accountManager: AccountManager mapToSignal { phoneNumber in - return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: Int(authorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: phoneNumber, useRequestTimeoutTimers: useRequestTimeoutTimers) + return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: Int(authorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: phoneNumber, useRequestTimeoutTimers: useRequestTimeoutTimers, appConfiguration: appConfig) |> map { network -> AccountResult in return .authorized(Account(accountManager: accountManager, id: id, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, postbox: postbox, network: network, networkArguments: networkArguments, peerId: authorizedState.peerId, auxiliaryMethods: auxiliaryMethods, supplementary: supplementary)) } @@ -267,7 +267,7 @@ public func accountWithId(accountManager: AccountManager map { network -> AccountResult in return .unauthorized(UnauthorizedAccount(networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } @@ -889,6 +889,11 @@ public func accountBackupData(postbox: Postbox) -> Signal map { network -> AccountStateManager? in Logger.shared.log("StandaloneStateManager", "received network") diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index c70bf2b60f..e80ff2715c 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -90,6 +90,7 @@ private var declaredEncodables: Void = { declareEncodable(ChannelState.self, f: { ChannelState(decoder: $0) }) declareEncodable(RegularChatState.self, f: { RegularChatState(decoder: $0) }) declareEncodable(InlineBotMessageAttribute.self, f: { InlineBotMessageAttribute(decoder: $0) }) + declareEncodable(InlineBusinessBotMessageAttribute.self, f: { InlineBusinessBotMessageAttribute(decoder: $0) }) declareEncodable(TextEntitiesMessageAttribute.self, f: { TextEntitiesMessageAttribute(decoder: $0) }) declareEncodable(ReplyMessageAttribute.self, f: { ReplyMessageAttribute(decoder: $0) }) declareEncodable(QuotedReplyMessageAttribute.self, f: { QuotedReplyMessageAttribute(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 5ecd19cfc1..e0a1e24c10 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -126,7 +126,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { switch messsage { - case let .message(_, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let chatPeerId = messagePeerId return chatPeerId.peerId case let .messageEmpty(_, _, peerId): @@ -142,7 +142,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { switch message { - case let .message(_, _, fromId, _, chatPeerId, savedPeerId, fwdHeader, viaBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, fromId, _, chatPeerId, savedPeerId, fwdHeader, viaBotId, viaBusinessBotId, replyTo, _, _, media, _, entities, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId var result = [peerId] @@ -171,6 +171,9 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { if let viaBotId = viaBotId { result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBotId))) } + if let viaBusinessBotId { + result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBusinessBotId))) + } if let savedPeerId = savedPeerId { result.append(savedPeerId.peerId) @@ -263,7 +266,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: ReferencedReplyMessageIds, generalIds: [MessageId])? { switch message { - case let .message(_, id, _, _, chatPeerId, _, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, id, _, _, chatPeerId, _, _, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): if let replyTo = replyTo { let peerId: PeerId = chatPeerId.peerId @@ -597,7 +600,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { - case let .message(flags, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId): + case let .message(flags, _, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId): let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId var namespace = namespace @@ -799,6 +802,10 @@ extension StoreMessage { if let viaBotId = viaBotId { attributes.append(InlineBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBotId)), title: nil)) } + + if let viaBusinessBotId { + attributes.append(InlineBusinessBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBusinessBotId)), title: nil)) + } if namespace != Namespaces.Message.ScheduledCloud && namespace != Namespaces.Message.QuickReplyCloud { if let views = views { diff --git a/submodules/TelegramCore/Sources/Network/Download.swift b/submodules/TelegramCore/Sources/Network/Download.swift index 2ebe6cff22..7966a99445 100644 --- a/submodules/TelegramCore/Sources/Network/Download.swift +++ b/submodules/TelegramCore/Sources/Network/Download.swift @@ -103,7 +103,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { self.context.authTokenForDatacenter(withIdRequired: self.datacenterId, authToken:self.mtProto.requiredAuthToken, masterDatacenterId: self.mtProto.authTokenMasterDatacenterId) } - static func uploadPart(multiplexedManager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64, tag: MediaResourceFetchTag?, fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false) -> Signal { + static func uploadPart(multiplexedManager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64, tag: MediaResourceFetchTag?, fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false, onFloodWaitError: ((String) -> Void)? = nil) -> Signal { let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse) if asBigPart { let totalParts: Int32 @@ -117,7 +117,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data)) } - return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true, expectedResponseSize: nil) + return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true, onFloodWaitError: onFloodWaitError, expectedResponseSize: nil) |> mapError { error -> UploadPartError in if error.errorCode == 400 { return .invalidMedia @@ -130,7 +130,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { } } - func uploadPart(fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false) -> Signal { + func uploadPart(fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false, onFloodWaitError: ((String) -> Void)? = nil) -> Signal { return Signal { subscriber in let request = MTRequest() @@ -159,6 +159,13 @@ class Download: NSObject, MTRequestMessageServiceDelegate { request.dependsOnPasswordEntry = false request.shouldContinueExecutionWithErrorContext = { errorContext in + guard let errorContext = errorContext else { + return true + } + if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText { + onFloodWaitError(errorText) + } + return true } @@ -295,7 +302,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { |> retryRequest } - func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), expectedResponseSize: Int32? = nil, automaticFloodWait: Bool = true) -> Signal { + func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), expectedResponseSize: Int32? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal { return Signal { subscriber in let request = MTRequest() request.expectedResponseSize = expectedResponseSize ?? 0 @@ -314,6 +321,9 @@ class Download: NSObject, MTRequestMessageServiceDelegate { guard let errorContext = errorContext else { return true } + if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText { + onFloodWaitError(errorText) + } if errorContext.floodWaitSeconds > 0 && !automaticFloodWait { return false } @@ -344,7 +354,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { } } - func requestWithAdditionalData(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false, expectedResponseSize: Int32? = nil) -> Signal<(T, Double), (MTRpcError, Double)> { + func requestWithAdditionalData(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, failOnServerErrors: Bool = false, expectedResponseSize: Int32? = nil) -> Signal<(T, Double), (MTRpcError, Double)> { return Signal { subscriber in let request = MTRequest() request.expectedResponseSize = expectedResponseSize ?? 0 @@ -363,6 +373,9 @@ class Download: NSObject, MTRequestMessageServiceDelegate { guard let errorContext = errorContext else { return true } + if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText { + onFloodWaitError(errorText) + } if errorContext.floodWaitSeconds > 0 && !automaticFloodWait { return false } @@ -396,7 +409,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate { } } - func rawRequest(_ data: (FunctionDescription, Buffer, (Buffer) -> Any?), automaticFloodWait: Bool = true, failOnServerErrors: Bool = false, logPrefix: String = "", expectedResponseSize: Int32? = nil) -> Signal<(Any, NetworkResponseInfo), (MTRpcError, Double)> { + func rawRequest(_ data: (FunctionDescription, Buffer, (Buffer) -> Any?), automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, failOnServerErrors: Bool = false, logPrefix: String = "", expectedResponseSize: Int32? = nil) -> Signal<(Any, NetworkResponseInfo), (MTRpcError, Double)> { let requestService = self.requestService return Signal { subscriber in let request = MTRequest() @@ -416,6 +429,9 @@ class Download: NSObject, MTRequestMessageServiceDelegate { guard let errorContext = errorContext else { return true } + if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText { + onFloodWaitError(errorText) + } if errorContext.floodWaitSeconds > 0 && !automaticFloodWait { return false } diff --git a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift index 5c9a473a3a..83d0c4b40c 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift @@ -104,14 +104,14 @@ private struct DownloadWrapper { self.useMainConnection = useMainConnection } - func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), MTRpcError> { + func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, expectedResponseSize: Int32?, onFloodWaitError: @escaping (String) -> Void) -> Signal<(T, NetworkResponseInfo), MTRpcError> { let target: MultiplexedRequestTarget if self.isCdn { target = .cdn(Int(self.datacenterId)) } else { target = .main(Int(self.datacenterId)) } - return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, resourceId: self.resourceId, data: data, tag: tag, continueInBackground: continueInBackground, expectedResponseSize: expectedResponseSize) + return network.multiplexedRequestManager.requestWithAdditionalInfo(to: target, consumerId: self.consumerId, resourceId: self.resourceId, data: data, tag: tag, continueInBackground: continueInBackground, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize) |> mapError { error, _ -> MTRpcError in return error } @@ -192,7 +192,7 @@ private final class MultipartCdnHashSource { clusterContext = ClusterContext(disposable: disposable) self.clusterContexts[offset] = clusterContext - disposable.set((self.masterDownload.request(Api.functions.upload.getCdnFileHashes(fileToken: Buffer(data: self.fileToken), offset: offset), tag: nil, continueInBackground: self.continueInBackground, expectedResponseSize: nil) + disposable.set((self.masterDownload.request(Api.functions.upload.getCdnFileHashes(fileToken: Buffer(data: self.fileToken), offset: offset), tag: nil, continueInBackground: self.continueInBackground, expectedResponseSize: nil, onFloodWaitError: { _ in }) |> map { partHashes, _ -> [Int64: Data] in var parsedPartHashes: [Int64: Data] = [:] for part in partHashes { @@ -322,7 +322,7 @@ private enum MultipartFetchSource { } } - func request(offset: Int64, limit: Int64, tag: MediaResourceFetchTag?, resource: TelegramMediaResource, resourceReference: FetchResourceReference, fileReference: Data?, continueInBackground: Bool) -> Signal<(Data, NetworkResponseInfo), MultipartFetchDownloadError> { + func request(offset: Int64, limit: Int64, tag: MediaResourceFetchTag?, resource: TelegramMediaResource, resourceReference: FetchResourceReference, fileReference: Data?, continueInBackground: Bool, onFloodWaitError: @escaping (String) -> Void) -> Signal<(Data, NetworkResponseInfo), MultipartFetchDownloadError> { var resourceReferenceValue: MediaResourceReference? switch resourceReference { case .forceRevalidate: @@ -348,7 +348,9 @@ private enum MultipartFetchSource { case .revalidate: return .fail(.revalidateMediaReference) case let .location(parsedLocation): - return download.request(Api.functions.upload.getFile(flags: 0, location: parsedLocation, offset: offset, limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit)) + return download.request(Api.functions.upload.getFile(flags: 0, location: parsedLocation, offset: offset, limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit), onFloodWaitError: { error in + onFloodWaitError(error) + }) |> mapError { error -> MultipartFetchDownloadError in if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") { return .revalidateMediaReference @@ -380,7 +382,9 @@ private enum MultipartFetchSource { } } case let .web(_, location): - return download.request(Api.functions.upload.getWebFile(location: location, offset: Int32(offset), limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit)) + return download.request(Api.functions.upload.getWebFile(location: location, offset: Int32(offset), limit: Int32(limit)), tag: tag, continueInBackground: continueInBackground, expectedResponseSize: Int32(limit), onFloodWaitError: { error in + onFloodWaitError(error) + }) |> mapError { error -> MultipartFetchDownloadError in if error.errorDescription == "WEBFILE_NOT_AVAILABLE" { return .webfileNotAvailable @@ -404,7 +408,9 @@ private enum MultipartFetchSource { updatedLength += 1 } - let part = download.request(Api.functions.upload.getCdnFile(fileToken: Buffer(data: fileToken), offset: offset, limit: Int32(updatedLength)), tag: nil, continueInBackground: continueInBackground, expectedResponseSize: Int32(updatedLength)) + let part = download.request(Api.functions.upload.getCdnFile(fileToken: Buffer(data: fileToken), offset: offset, limit: Int32(updatedLength)), tag: nil, continueInBackground: continueInBackground, expectedResponseSize: Int32(updatedLength), onFloodWaitError: { error in + onFloodWaitError(error) + }) |> mapError { _ -> MultipartFetchDownloadError in return .generic } @@ -723,6 +729,13 @@ private final class MultipartFetchManager { } } + + private func processFloodWaitError(error: String) { + if error.hasPrefix("FLOOD_PREMIUM_WAIT") { + self.network.addNetworkSpeedLimitedEvent(event: .download) + } + } + func checkState() { guard let currentIntervals = self.currentIntervals else { return @@ -836,7 +849,15 @@ private final class MultipartFetchManager { } let partSize: Int32 = Int32(downloadRange.upperBound - downloadRange.lowerBound) - let part = self.source.request(offset: downloadRange.lowerBound, limit: downloadRange.upperBound - downloadRange.lowerBound, tag: self.parameters?.tag, resource: self.resource, resourceReference: self.resourceReference, fileReference: self.fileReference, continueInBackground: self.continueInBackground) + let queue = self.queue + let part = self.source.request(offset: downloadRange.lowerBound, limit: downloadRange.upperBound - downloadRange.lowerBound, tag: self.parameters?.tag, resource: self.resource, resourceReference: self.resourceReference, fileReference: self.fileReference, continueInBackground: self.continueInBackground, onFloodWaitError: { [weak self] error in + queue.async { + guard let self else { + return + } + self.processFloodWaitError(error: error) + } + }) |> deliverOn(self.queue) let partDisposable = MetaDisposable() self.fetchingParts[downloadRange.lowerBound] = FetchingPart(size: Int64(downloadRange.count), disposable: partDisposable) @@ -919,7 +940,7 @@ private final class MultipartFetchManager { case let .cdn(_, _, fileToken, _, _, _, masterDownload, _): if !strongSelf.reuploadingToCdn { strongSelf.reuploadingToCdn = true - let reupload: Signal<[Api.FileHash], NoError> = masterDownload.request(Api.functions.upload.reuploadCdnFile(fileToken: Buffer(data: fileToken), requestToken: Buffer(data: token)), tag: nil, continueInBackground: strongSelf.continueInBackground, expectedResponseSize: nil) + let reupload: Signal<[Api.FileHash], NoError> = masterDownload.request(Api.functions.upload.reuploadCdnFile(fileToken: Buffer(data: fileToken), requestToken: Buffer(data: token)), tag: nil, continueInBackground: strongSelf.continueInBackground, expectedResponseSize: nil, onFloodWaitError: { _ in }) |> map { result, _ -> [Api.FileHash] in return result } diff --git a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift index 95331fcf9f..3f07e3bb5e 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift @@ -470,12 +470,21 @@ func multipartUpload(network: Network, postbox: Postbox, source: MultipartUpload fetchedResource = .complete() } + let onFloodWaitError: (String) -> Void = { [weak network] error in + guard let network else { + return + } + if error.hasPrefix("FLOOD_PREMIUM_WAIT") { + network.addNetworkSpeedLimitedEvent(event: .upload) + } + } + let manager = MultipartUploadManager(headerSize: headerSize, data: dataSignal, encryptionKey: encryptionKey, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts, useLargerParts: useLargerParts, increaseParallelParts: increaseParallelParts, uploadPart: { part in switch uploadInterface { case let .download(download): - return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression) + return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression, onFloodWaitError: onFloodWaitError) case let .multiplexed(multiplexed, datacenterId, consumerId): - return Download.uploadPart(multiplexedManager: multiplexed, datacenterId: datacenterId, consumerId: consumerId, tag: nil, fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression) + return Download.uploadPart(multiplexedManager: multiplexed, datacenterId: datacenterId, consumerId: consumerId, tag: nil, fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression, onFloodWaitError: onFloodWaitError) } }, progress: { progress in subscriber.putNext(.progress(progress)) diff --git a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift index 7e1a846480..1172b3c739 100644 --- a/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift +++ b/submodules/TelegramCore/Sources/Network/MultiplexedRequestManager.swift @@ -33,12 +33,13 @@ private final class RequestData { let tag: MediaResourceFetchTag? let continueInBackground: Bool let automaticFloodWait: Bool + let onFloodWaitError: ((String) -> Void)? let expectedResponseSize: Int32? let deserializeResponse: (Buffer) -> Any? let completed: (Any, NetworkResponseInfo) -> Void let error: (MTRpcError, Double) -> Void - init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, expectedResponseSize: Int32?, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) { + init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, onFloodWaitError: ((String) -> Void)?, expectedResponseSize: Int32?, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) { self.id = id self.consumerId = consumerId self.resourceId = resourceId @@ -47,6 +48,7 @@ private final class RequestData { self.tag = tag self.continueInBackground = continueInBackground self.automaticFloodWait = automaticFloodWait + self.onFloodWaitError = onFloodWaitError self.expectedResponseSize = expectedResponseSize self.payload = payload self.deserializeResponse = deserializeResponse @@ -155,12 +157,12 @@ private final class MultiplexedRequestManagerContext { } } - func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, expectedResponseSize: Int32?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable { + func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable { let targetKey = MultiplexedRequestTargetKey(target: target, continueInBackground: continueInBackground) let requestId = self.nextId self.nextId += 1 - self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, deserializeResponse: { buffer in + self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, deserializeResponse: { buffer in return data.2(buffer) }, completed: { result, info in completed(result, info) @@ -254,7 +256,7 @@ private final class MultiplexedRequestManagerContext { let requestId = request.id selectedContext.requests.append(ExecutingRequestData(requestId: requestId, disposable: disposable)) let queue = self.queue - disposable.set(selectedContext.worker.rawRequest((request.functionDescription, request.payload, request.deserializeResponse), automaticFloodWait: request.automaticFloodWait, expectedResponseSize: request.expectedResponseSize).start(next: { [weak self, weak selectedContext] result, info in + disposable.set(selectedContext.worker.rawRequest((request.functionDescription, request.payload, request.deserializeResponse), automaticFloodWait: request.automaticFloodWait, onFloodWaitError: request.onFloodWaitError, expectedResponseSize: request.expectedResponseSize).start(next: { [weak self, weak selectedContext] result, info in queue.async { guard let strongSelf = self else { return @@ -354,13 +356,13 @@ final class MultiplexedRequestManager { return disposable } - func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, expectedResponseSize: Int32?) -> Signal { + func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.context.with { context in disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in return data.2.parse(buffer) - }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, completed: { result, _ in + }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, completed: { result, _ in if let result = result as? T { subscriber.putNext(result) subscriber.putCompletion() @@ -375,13 +377,13 @@ final class MultiplexedRequestManager { } } - func requestWithAdditionalInfo(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> { + func requestWithAdditionalInfo(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> { return Signal { subscriber in let disposable = MetaDisposable() self.context.with { context in disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in return data.2.parse(buffer) - }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, expectedResponseSize: expectedResponseSize, completed: { result, info in + }), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, completed: { result, info in if let result = result as? T { subscriber.putNext((result, info)) subscriber.putCompletion() diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index 2914559d88..4d82d66fba 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -459,7 +459,7 @@ public struct NetworkInitializationArguments { private let cloudDataContext = Atomic(value: nil) #endif -func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializationArguments, supplementary: Bool, datacenterId: Int, keychain: Keychain, basePath: String, testingEnvironment: Bool, languageCode: String?, proxySettings: ProxySettings?, networkSettings: NetworkSettings?, phoneNumber: String?, useRequestTimeoutTimers: Bool) -> Signal { +func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializationArguments, supplementary: Bool, datacenterId: Int, keychain: Keychain, basePath: String, testingEnvironment: Bool, languageCode: String?, proxySettings: ProxySettings?, networkSettings: NetworkSettings?, phoneNumber: String?, useRequestTimeoutTimers: Bool, appConfiguration: AppConfiguration) -> Signal { return Signal { subscriber in let queue = Queue() queue.async { @@ -612,6 +612,11 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa let useExperimentalFeatures = networkSettings?.useExperimentalDownload ?? false let network = Network(queue: queue, datacenterId: datacenterId, context: context, mtProto: mtProto, requestService: requestService, connectionStatusDelegate: connectionStatusDelegate, _connectionStatus: connectionStatus, basePath: basePath, appDataDisposable: appDataDisposable, encryptionProvider: arguments.encryptionProvider, useRequestTimeoutTimers: useRequestTimeoutTimers, useBetaFeatures: arguments.useBetaFeatures, useExperimentalFeatures: useExperimentalFeatures) + + if let data = appConfiguration.data, let notifyInterval = data["upload_premium_speedup_notify_period"] as? Double { + network.updateNetworkSpeedLimitedEventNotifyInterval(value: notifyInterval) + } + appDataUpdatedImpl = { [weak network] data in guard let data = data else { return @@ -734,6 +739,22 @@ public enum NetworkRequestResult { case progress(Float, Int32) } +private final class NetworkSpeedLimitedEventState { + var notifyInterval: Double = 60.0 * 60.0 + var lastNotifyTimestamp: Double = 0.0 + + func add(event: NetworkSpeedLimitedEvent) -> Bool { + let timestamp = CFAbsoluteTimeGetCurrent() + + if self.lastNotifyTimestamp + self.notifyInterval < timestamp { + self.lastNotifyTimestamp = timestamp + return true + } else { + return false + } + } +} + public final class Network: NSObject, MTRequestMessageServiceDelegate { public let encryptionProvider: EncryptionProvider @@ -766,6 +787,12 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { return self._connectionStatus.get() |> distinctUntilChanged } + public var networkSpeedLimitedEvents: Signal { + return self.networkSpeedLimitedEventPipe.signal() + } + private let networkSpeedLimitedEventPipe = ValuePipe() + private let networkSpeedLimitedEventState = Atomic(value: NetworkSpeedLimitedEventState()) + public func dropConnectionStatus() { _connectionStatus.set(.single(.waitingForNetwork)) } @@ -826,18 +853,18 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { let array = NSMutableArray() if let result = result { switch result { - case let .cdnConfig(publicKeys): - for key in publicKeys { - switch key { - case let .cdnPublicKey(dcId, publicKey): - if id == Int(dcId) { - let dict = NSMutableDictionary() - dict["key"] = publicKey - dict["fingerprint"] = MTRsaFingerprint(encryptionProvider, publicKey) - array.add(dict) - } + case let .cdnConfig(publicKeys): + for key in publicKeys { + switch key { + case let .cdnPublicKey(dcId, publicKey): + if id == Int(dcId) { + let dict = NSMutableDictionary() + dict["key"] = publicKey + dict["fingerprint"] = MTRsaFingerprint(encryptionProvider, publicKey) + array.add(dict) } } + } } } return array @@ -867,12 +894,12 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { let isCdn: Bool let isMedia: Bool = true switch target { - case let .main(id): - datacenterId = id - isCdn = false - case let .cdn(id): - datacenterId = id - isCdn = true + case let .main(id): + datacenterId = id + isCdn = false + case let .cdn(id): + datacenterId = id + isCdn = true } return strongSelf.makeWorker(datacenterId: datacenterId, isCdn: isCdn, isMedia: isMedia, tag: tag, continueInBackground: continueInBackground) } @@ -880,7 +907,7 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { }) let shouldKeepConnectionSignal = self.shouldKeepConnection.get() - |> distinctUntilChanged |> deliverOn(queue) + |> distinctUntilChanged |> deliverOn(queue) self.shouldKeepConnectionDisposable.set(shouldKeepConnectionSignal.start(next: { [weak self] value in if let strongSelf = self { if value { @@ -967,11 +994,11 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { self.context.addAddressForDatacenter(withId: Int(datacenterId), address: address) /*let currentScheme = self.context.transportSchemeForDatacenter(withId: Int(datacenterId), media: false, isProxy: false) - if let currentScheme = currentScheme, currentScheme.address.isEqual(to: address) { - } else { - let scheme = MTTransportScheme(transport: MTTcpTransport.self, address: address, media: false) - self.context.updateTransportSchemeForDatacenter(withId: Int(datacenterId), transportScheme: scheme, media: false, isProxy: false) - }*/ + if let currentScheme = currentScheme, currentScheme.address.isEqual(to: address) { + } else { + let scheme = MTTransportScheme(transport: MTTcpTransport.self, address: address, media: false) + self.context.updateTransportSchemeForDatacenter(withId: Int(datacenterId), transportScheme: scheme, media: false, isProxy: false) + }*/ let currentSchemes = self.context.transportSchemesForDatacenter(withId: Int(datacenterId), media: false, enforceMedia: false, isProxy: false) var found = false @@ -988,7 +1015,7 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { } } - public func requestWithAdditionalInfo(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true) -> Signal, MTRpcError> { + public func requestWithAdditionalInfo(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal, MTRpcError> { let requestService = self.requestService return Signal { subscriber in let request = MTRequest() @@ -1006,6 +1033,9 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { guard let errorContext = errorContext else { return true } + if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText { + onFloodWaitError(errorText) + } if errorContext.floodWaitSeconds > 0 && !automaticFloodWait { return false } @@ -1056,8 +1086,8 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { } } } - - public func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true) -> Signal { + + public func request(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal { let requestService = self.requestService return Signal { subscriber in let request = MTRequest() @@ -1075,6 +1105,9 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { guard let errorContext = errorContext else { return true } + if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText { + onFloodWaitError(errorText) + } if errorContext.floodWaitSeconds > 0 && !automaticFloodWait { return false } @@ -1113,6 +1146,21 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { } } } + + func updateNetworkSpeedLimitedEventNotifyInterval(value: Double) { + let _ = self.networkSpeedLimitedEventState.with { state in + state.notifyInterval = value + } + } + + func addNetworkSpeedLimitedEvent(event: NetworkSpeedLimitedEvent) { + let notify = self.networkSpeedLimitedEventState.with { state in + return state.add(event: event) + } + if notify { + self.networkSpeedLimitedEventPipe.putNext(event) + } + } } public func retryRequest(signal: 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/Settings/PeerContactSettings.swift b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift index c421160341..2333ad61f0 100644 --- a/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/PeerContactSettings.swift @@ -6,33 +6,44 @@ import SwiftSignalKit extension PeerStatusSettings { init(apiSettings: Api.PeerSettings) { switch apiSettings { - case let .peerSettings(flags, geoDistance, requestChatTitle, requestChatDate): - var result = PeerStatusSettings.Flags() - if (flags & (1 << 1)) != 0 { - result.insert(.canAddContact) - } - if (flags & (1 << 0)) != 0 { - result.insert(.canReport) - } - if (flags & (1 << 2)) != 0 { - result.insert(.canBlock) - } - if (flags & (1 << 3)) != 0 { - result.insert(.canShareContact) - } - if (flags & (1 << 4)) != 0 { - result.insert(.addExceptionWhenAddingContact) - } - if (flags & (1 << 5)) != 0 { - result.insert(.canReportIrrelevantGeoLocation) - } - if (flags & (1 << 7)) != 0 { - result.insert(.autoArchived) - } - if (flags & (1 << 8)) != 0 { - result.insert(.suggestAddMembers) - } - self = PeerStatusSettings(flags: result, geoDistance: geoDistance, requestChatTitle: requestChatTitle, requestChatDate: requestChatDate, requestChatIsChannel: (flags & (1 << 10)) != 0) + case let .peerSettings(flags, geoDistance, requestChatTitle, requestChatDate, businessBotId, businessBotManageUrl): + var result = PeerStatusSettings.Flags() + if (flags & (1 << 1)) != 0 { + result.insert(.canAddContact) + } + if (flags & (1 << 0)) != 0 { + result.insert(.canReport) + } + if (flags & (1 << 2)) != 0 { + result.insert(.canBlock) + } + if (flags & (1 << 3)) != 0 { + result.insert(.canShareContact) + } + if (flags & (1 << 4)) != 0 { + result.insert(.addExceptionWhenAddingContact) + } + if (flags & (1 << 5)) != 0 { + result.insert(.canReportIrrelevantGeoLocation) + } + if (flags & (1 << 7)) != 0 { + result.insert(.autoArchived) + } + if (flags & (1 << 8)) != 0 { + result.insert(.suggestAddMembers) + } + + var managingBot: ManagingBot? + if let businessBotId { + managingBot = ManagingBot( + id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(businessBotId)), + manageUrl: businessBotManageUrl, + isPaused: (flags & (1 << 11)) != 0, + canReply: (flags & (1 << 12)) != 0 + ) + } + + self = PeerStatusSettings(flags: result, geoDistance: geoDistance, requestChatTitle: requestChatTitle, requestChatDate: requestChatDate, requestChatIsChannel: (flags & (1 << 10)) != 0, managingBot: managingBot) } } } diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 0a2a0b1fb4..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/ChatHistoryPreloadManager.swift b/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift index acd550c523..37e1406e1f 100644 --- a/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift +++ b/submodules/TelegramCore/Sources/State/ChatHistoryPreloadManager.swift @@ -360,11 +360,11 @@ final class ChatHistoryPreloadManager { guard let strongSelf = self else { return } - #if DEBUG + /*#if DEBUG if "".isEmpty { return } - #endif + #endif*/ var indices: [(ChatHistoryPreloadIndex, Bool, Bool)] = [] for item in loadItems { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 6e1311188e..03166bd199 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 176 + return 177 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift index b6788804a4..fc2c881026 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift @@ -40,56 +40,57 @@ public func getIsStickerSaved(transaction: Transaction, fileId: MediaId) -> Bool public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMediaFile, limit: Int = 5) -> Signal { return postbox.transaction { transaction -> Signal in for attribute in file.attributes { - if case let .Sticker(_, maybePackReference, _) = attribute, let packReference = maybePackReference { - var fetchReference: StickerPackReference? - switch packReference { - case .name: - fetchReference = packReference - case let .id(id, _): - let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id)) - var found = false - inner: for item in items { - if let stickerItem = item as? StickerPackItem { - if stickerItem.file.fileId == file.fileId { - let stringRepresentations = stickerItem.getStringRepresentationsOfIndexKeys() - found = true - addSavedSticker(transaction: transaction, file: stickerItem.file, stringRepresentations: stringRepresentations) - break inner + if case let .Sticker(_, maybePackReference, _) = attribute { + if let packReference = maybePackReference { + var fetchReference: StickerPackReference? + switch packReference { + case .name: + fetchReference = packReference + case let .id(id, _): + let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id)) + var found = false + inner: for item in items { + if let stickerItem = item as? StickerPackItem { + if stickerItem.file.fileId == file.fileId { + let stringRepresentations = stickerItem.getStringRepresentationsOfIndexKeys() + found = true + addSavedSticker(transaction: transaction, file: stickerItem.file, stringRepresentations: stringRepresentations) + break inner + } } } - } - if !found { - fetchReference = packReference + if !found { + fetchReference = packReference + } + case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts, .emojiGenericAnimations, .iconStatusEmoji, .iconChannelStatusEmoji, .iconTopicEmoji: + break } - case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts, .emojiGenericAnimations, .iconStatusEmoji, .iconChannelStatusEmoji, .iconTopicEmoji: - break - } - if let fetchReference = fetchReference { - return network.request(Api.functions.messages.getStickerSet(stickerset: fetchReference.apiInputStickerSet, hash: 0)) + if let fetchReference = fetchReference { + return network.request(Api.functions.messages.getStickerSet(stickerset: fetchReference.apiInputStickerSet, hash: 0)) |> mapError { _ -> AddSavedStickerError in return .generic } |> mapToSignal { result -> Signal in var stickerStringRepresentations: [String]? switch result { - case .stickerSetNotModified: - break - case let .stickerSet(_, packs, _, _): - var stringRepresentationsByFile: [MediaId: [String]] = [:] - for pack in packs { - switch pack { - case let .stickerPack(text, fileIds): - for fileId in fileIds { - let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) - if stringRepresentationsByFile[mediaId] == nil { - stringRepresentationsByFile[mediaId] = [text] - } else { - stringRepresentationsByFile[mediaId]!.append(text) - } - } + case .stickerSetNotModified: + break + case let .stickerSet(_, packs, _, _): + var stringRepresentationsByFile: [MediaId: [String]] = [:] + for pack in packs { + switch pack { + case let .stickerPack(text, fileIds): + for fileId in fileIds { + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if stringRepresentationsByFile[mediaId] == nil { + stringRepresentationsByFile[mediaId] = [text] + } else { + stringRepresentationsByFile[mediaId]!.append(text) + } } } - stickerStringRepresentations = stringRepresentationsByFile[file.fileId] + } + stickerStringRepresentations = stringRepresentationsByFile[file.fileId] } if let stickerStringRepresentations = stickerStringRepresentations { return postbox.transaction { transaction -> Void in @@ -99,8 +100,13 @@ public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMe return .fail(.notFound) } } + } + return .complete() + } else { + return postbox.transaction { transaction -> Void in + addSavedSticker(transaction: transaction, file: file, stringRepresentations: []) + } |> mapError { _ -> AddSavedStickerError in } } - return .complete() } } return .complete() diff --git a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift index 279064d68a..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, 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, 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 a7fcc88093..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/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index cfef517df3..7c8fac5dd5 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -551,7 +551,7 @@ public final class CachedChannelData: CachedPeerData { var peerIds = Set() if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift index 5959310a0b..eb8391bec0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift @@ -203,7 +203,7 @@ public final class CachedGroupData: CachedPeerData { self.exportedInvitation = decoder.decode(ExportedInvitation.self, forKey: "i") self.botInfos = decoder.decodeObjectArrayWithDecoderForKey("b") as [CachedPeerBotInfo] if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 5e9a6f657b..e6cb200972 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -558,7 +558,7 @@ public final class CachedUserData: CachedPeerData { self.botInfo = decoder.decodeObjectForKey("bi") as? BotInfo self.editableBotInfo = decoder.decodeObjectForKey("ebi") as? EditableBotInfo if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InlineBotMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InlineBotMessageAttribute.swift index 51b93722a9..58f4122c15 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InlineBotMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InlineBotMessageAttribute.swift @@ -40,3 +40,43 @@ public class InlineBotMessageAttribute: MessageAttribute { } } } + +public class InlineBusinessBotMessageAttribute: MessageAttribute { + public let peerId: PeerId? + public let title: String? + + public var associatedPeerIds: [PeerId] { + if let peerId = self.peerId { + return [peerId] + } else { + return [] + } + } + + public init(peerId: PeerId?, title: String?) { + self.peerId = peerId + self.title = title + } + + required public init(decoder: PostboxDecoder) { + if let peerId = decoder.decodeOptionalInt64ForKey("i") { + self.peerId = PeerId(peerId) + } else { + self.peerId = nil + } + self.title = decoder.decodeOptionalStringForKey("t") + } + + public func encode(_ encoder: PostboxEncoder) { + if let peerId = self.peerId { + encoder.encodeInt64(peerId.toInt64(), forKey: "i") + } else { + encoder.encodeNil(forKey: "i") + } + if let title = self.title { + encoder.encodeString(title, forKey: "t") + } else { + encoder.encodeNil(forKey: "t") + } + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 958ac84482..afe17a9427 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -286,6 +286,7 @@ private enum PreferencesKeyValues: Int32 { case displaySavedChatsAsTopics = 35 case shortcutMessages = 37 case timezoneList = 38 + case botBiometricsState = 39 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -481,6 +482,13 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.timezoneList.rawValue) return key } + + public static func botBiometricsState(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 4 + 8) + key.setInt32(0, value: PreferencesKeyValues.botBiometricsState.rawValue) + key.setInt64(4, value: peerId.toInt64()) + return key + } } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift index ed6ca78314..d12b6e71cb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerStatusSettings.swift @@ -16,7 +16,20 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { public static let canReportIrrelevantGeoLocation = Flags(rawValue: 1 << 6) public static let autoArchived = Flags(rawValue: 1 << 7) public static let suggestAddMembers = Flags(rawValue: 1 << 8) - + } + + public struct ManagingBot: Codable, Equatable { + public var id: PeerId + public var manageUrl: String? + public var isPaused: Bool + public var canReply: Bool + + public init(id: PeerId, manageUrl: String?, isPaused: Bool, canReply: Bool) { + self.id = id + self.manageUrl = manageUrl + self.isPaused = isPaused + self.canReply = canReply + } } public var flags: PeerStatusSettings.Flags @@ -24,20 +37,23 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { public var requestChatTitle: String? public var requestChatDate: Int32? public var requestChatIsChannel: Bool? + public var managingBot: ManagingBot? public init() { self.flags = PeerStatusSettings.Flags() self.geoDistance = nil self.requestChatTitle = nil self.requestChatDate = nil + self.managingBot = nil } - public init(flags: PeerStatusSettings.Flags, geoDistance: Int32? = nil, requestChatTitle: String? = nil, requestChatDate: Int32? = nil, requestChatIsChannel: Bool? = nil) { + public init(flags: PeerStatusSettings.Flags, geoDistance: Int32? = nil, requestChatTitle: String? = nil, requestChatDate: Int32? = nil, requestChatIsChannel: Bool? = nil, managingBot: ManagingBot?) { self.flags = flags self.geoDistance = geoDistance self.requestChatTitle = requestChatTitle self.requestChatDate = requestChatDate self.requestChatIsChannel = requestChatIsChannel + self.managingBot = managingBot } public init(decoder: PostboxDecoder) { @@ -46,6 +62,7 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { self.requestChatTitle = decoder.decodeOptionalStringForKey("requestChatTitle") self.requestChatDate = decoder.decodeOptionalInt32ForKey("requestChatDate") self.requestChatIsChannel = decoder.decodeOptionalBoolForKey("requestChatIsChannel") + self.managingBot = decoder.decodeCodable(ManagingBot.self, forKey: "managingBot") } public func encode(_ encoder: PostboxEncoder) { @@ -70,6 +87,11 @@ public struct PeerStatusSettings: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "requestChatIsChannel") } + if let managingBot = self.managingBot { + encoder.encodeCodable(managingBot, forKey: "managingBot") + } else { + encoder.encodeNil(forKey: "managingBot") + } } public func contains(_ member: PeerStatusSettings.Flags) -> Bool { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift index 2abeda4fcd..1823edd5ff 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift @@ -44,6 +44,7 @@ public struct StickerPackCollectionInfoFlags: OptionSet { public static let isEmoji = StickerPackCollectionInfoFlags(rawValue: 1 << 4) public static let isAvailableAsChannelStatus = StickerPackCollectionInfoFlags(rawValue: 1 << 5) public static let isCustomTemplateEmoji = StickerPackCollectionInfoFlags(rawValue: 1 << 6) + public static let isCreator = StickerPackCollectionInfoFlags(rawValue: 1 << 7) } @@ -115,35 +116,27 @@ public final class StickerPackCollectionInfo: ItemCollectionInfo, Equatable { if lhs.id != rhs.id { return false } - if lhs.title != rhs.title { return false } - if lhs.shortName != rhs.shortName { return false } - if lhs.hash != rhs.hash { return false } - if lhs.immediateThumbnailData != rhs.immediateThumbnailData { return false } - if lhs.thumbnailFileId != rhs.thumbnailFileId { return false } - if lhs.flags != rhs.flags { return false } - if lhs.count != rhs.count { return false } - return true } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift index 78150dc1d7..be745fbed8 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramSecretChat.swift @@ -93,7 +93,7 @@ public final class CachedSecretChatData: CachedPeerData { public init(decoder: PostboxDecoder) { if let legacyValue = decoder.decodeOptionalInt32ForKey("pcs") { - self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil) + self.peerStatusSettings = PeerStatusSettings(flags: PeerStatusSettings.Flags(rawValue: legacyValue), geoDistance: nil, managingBot: nil) } else if let peerStatusSettings = decoder.decodeObjectForKey("pss", decoder: { PeerStatusSettings(decoder: $0) }) as? PeerStatusSettings { self.peerStatusSettings = peerStatusSettings } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index a036a33a41..9ed2063e1b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1584,5 +1584,33 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct BotBiometricsState: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = TelegramBotBiometricsState + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .preferences(keys: Set([PreferencesKeys.botBiometricsState(peerId: self.id)])) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PreferencesView else { + preconditionFailure() + } + if let state = view.values[PreferencesKeys.botBiometricsState(peerId: self.id)]?.get(TelegramBotBiometricsState.self) { + return state + } else { + return TelegramBotBiometricsState.default + } + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 7d2b62e30e..55e5779f21 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -319,3 +319,42 @@ func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId: |> castError(InvokeBotCustomMethodError.self) |> switchToLatest } + +public struct TelegramBotBiometricsState: Codable, Equatable { + public struct OpaqueToken: Codable, Equatable { + public let publicKey: Data + public let data: Data + + public init(publicKey: Data, data: Data) { + self.publicKey = publicKey + self.data = data + } + } + + public var accessRequested: Bool + public var accessGranted: Bool + public var opaqueToken: OpaqueToken? + + public static var `default`: TelegramBotBiometricsState { + return TelegramBotBiometricsState( + accessRequested: false, + accessGranted: false, + opaqueToken: nil + ) + } + + public init(accessRequested: Bool, accessGranted: Bool, opaqueToken: OpaqueToken?) { + self.accessRequested = accessRequested + self.accessGranted = accessGranted + self.opaqueToken = opaqueToken + } +} + +func _internal_updateBotBiometricsState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState) -> TelegramBotBiometricsState) -> Signal { + return account.postbox.transaction { transaction -> Void in + let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId))?.get(TelegramBotBiometricsState.self) ?? TelegramBotBiometricsState.default + + transaction.setPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId), value: PreferencesEntry(update(previousState))) + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index a7e321c872..fcc57b2281 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -388,7 +388,7 @@ func _internal_sendMessageShortcut(account: Account, peerId: PeerId, id: Int32) guard let peer, let inputPeer = apiInputPeer(peer) else { return .complete() } - return account.network.request(Api.functions.messages.sendQuickReplyMessages(peer: inputPeer, shortcutId: id)) + return account.network.request(Api.functions.messages.sendQuickReplyMessages(peer: inputPeer, shortcutId: id, id: [], randomId: [])) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift index 85899d79ae..05b6c9825c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift @@ -90,15 +90,19 @@ func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId: var items: [RenderedChannelParticipant] = [] switch result { case let .channelParticipants(_, participants, chats, users): + postboxLog("channel users insertion started, count: \(participants.count)") let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + postboxLog("channel users parsed") updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + postboxLog("channel users postbox updated, started mapping ids") var peers: [PeerId: Peer] = [:] for id in parsedPeers.allIds { if let peer = transaction.getPeer(id) { peers[peer.id] = peer } } - + postboxLog("channel users finish mapping, started updating participants") + for participant in CachedChannelParticipants(apiParticipants: participants).participants { if let peer = parsedPeers.get(participant.peerId) { var renderedPresences: [PeerId: PeerPresence] = [:] @@ -108,6 +112,7 @@ func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId: items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: renderedPresences)) } } + postboxLog("channel participants finish updating") case .channelParticipantsNotModified: return nil } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift index 1deae7bbed..04ffbec511 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift @@ -214,7 +214,7 @@ func _internal_dismissPeerStatusOptions(account: Account, peerId: PeerId) -> Sig if let current = current as? CachedUserData { var peerStatusSettings = current.peerStatusSettings ?? PeerStatusSettings() peerStatusSettings.flags = [] - return current.withUpdatedPeerStatusSettings(PeerStatusSettings(flags: [])) + return current.withUpdatedPeerStatusSettings(peerStatusSettings) } else if let current = current as? CachedGroupData { var peerStatusSettings = current.peerStatusSettings ?? PeerStatusSettings() peerStatusSettings.flags = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index d0bc67b725..6980bbab07 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -30,6 +30,59 @@ public final class OpaqueChatInterfaceState { } } +public final class TelegramCollectibleItemInfo: Equatable { + public enum Subject: Equatable { + case username(String) + case phoneNumber(String) + } + + public let subject: Subject + public let purchaseDate: Int32 + public let currency: String + public let currencyAmount: Int64 + public let cryptoCurrency: String + public let cryptoCurrencyAmount: Int64 + public let url: String + + public init(subject: Subject, purchaseDate: Int32, currency: String, currencyAmount: Int64, cryptoCurrency: String, cryptoCurrencyAmount: Int64, url: String) { + self.subject = subject + self.purchaseDate = purchaseDate + self.currency = currency + self.currencyAmount = currencyAmount + self.cryptoCurrency = cryptoCurrency + self.cryptoCurrencyAmount = cryptoCurrencyAmount + self.url = url + } + + public static func ==(lhs: TelegramCollectibleItemInfo, rhs: TelegramCollectibleItemInfo) -> Bool { + if lhs === rhs { + return true + } + if lhs.subject != rhs.subject { + return false + } + if lhs.purchaseDate != rhs.purchaseDate { + return false + } + if lhs.currency != rhs.currency { + return false + } + if lhs.currencyAmount != rhs.currencyAmount { + return false + } + if lhs.cryptoCurrency != rhs.cryptoCurrency { + return false + } + if lhs.cryptoCurrencyAmount != rhs.cryptoCurrencyAmount { + return false + } + if lhs.url != rhs.url { + return false + } + return true + } +} + public extension TelegramEngine { enum NextUnreadChannelLocation: Equatable { case same @@ -1387,6 +1440,60 @@ public extension TelegramEngine { }) }).start() } + + public func getCollectibleUsernameInfo(username: String) -> Signal { + return self.account.network.request(Api.functions.fragment.getCollectibleInfo(collectible: .inputCollectibleUsername(username: username))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> TelegramCollectibleItemInfo? in + guard let result else { + return nil + } + switch result { + case let .collectibleInfo(purchaseDate, currency, amount, cryptoCurrency, cryptoAmount, url): + return TelegramCollectibleItemInfo( + subject: .username(username), + purchaseDate: purchaseDate, + currency: currency, + currencyAmount: amount, + cryptoCurrency: cryptoCurrency, + cryptoCurrencyAmount: cryptoAmount, + url: url + ) + } + } + } + + public func getCollectiblePhoneNumberInfo(phoneNumber: String) -> Signal { + return self.account.network.request(Api.functions.fragment.getCollectibleInfo(collectible: .inputCollectiblePhone(phone: phoneNumber))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> TelegramCollectibleItemInfo? in + guard let result else { + return nil + } + switch result { + case let .collectibleInfo(purchaseDate, currency, amount, cryptoCurrency, cryptoAmount, url): + return TelegramCollectibleItemInfo( + subject: .phoneNumber(phoneNumber), + purchaseDate: purchaseDate, + currency: currency, + currencyAmount: amount, + cryptoCurrency: cryptoCurrency, + cryptoCurrencyAmount: cryptoAmount, + url: url + ) + } + } + } + + public func updateBotBiometricsState(peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState) -> TelegramBotBiometricsState) { + let _ = _internal_updateBotBiometricsState(account: self.account, peerId: peerId, update: update).startStandalone() + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 775b5d7c2e..df655e5377 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -56,12 +56,12 @@ func fetchAndUpdateSupplementalCachedPeerData(peerId rawPeerId: PeerId, accountP var peerStatusSettings: PeerStatusSettings if let peer = transaction.getPeer(peer.id), let associatedPeerId = peer.associatedPeerId, !transaction.isPeerContact(peerId: associatedPeerId) { if let peer = peer as? TelegramSecretChat, case .creator = peer.role { - peerStatusSettings = PeerStatusSettings(flags: []) + peerStatusSettings = PeerStatusSettings(flags: [], managingBot: nil) } else { - peerStatusSettings = PeerStatusSettings(flags: [.canReport]) + peerStatusSettings = PeerStatusSettings(flags: [.canReport], managingBot: nil) } } else { - peerStatusSettings = PeerStatusSettings(flags: []) + peerStatusSettings = PeerStatusSettings(flags: [], managingBot: nil) } transaction.updatePeerCachedData(peerIds: [peer.id], update: { peerId, current in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 2df16ae9a7..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 { @@ -209,66 +209,8 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri return .generic } |> mapToSignal { result -> Signal in - let info: StickerPackCollectionInfo - var items: [StickerPackItem] = [] - - switch result { - case .stickerSetNotModified: + guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else { return .complete() - case let .stickerSet(set, packs, keywords, documents): - let namespace: ItemCollectionId.Namespace - switch set { - case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _): - if (flags & (1 << 3)) != 0 { - namespace = Namespaces.ItemCollection.CloudMaskPacks - } else if (flags & (1 << 7)) != 0 { - namespace = Namespaces.ItemCollection.CloudEmojiPacks - } else { - namespace = Namespaces.ItemCollection.CloudStickerPacks - } - } - info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) - var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:] - for pack in packs { - switch pack { - case let .stickerPack(text, fileIds): - let key = ValueBoxKey(text).toMemoryBuffer() - for fileId in fileIds { - let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) - if indexKeysByFile[mediaId] == nil { - indexKeysByFile[mediaId] = [key] - } else { - indexKeysByFile[mediaId]!.append(key) - } - } - } - } - for keyword in keywords { - switch keyword { - case let .stickerKeyword(documentId, texts): - for text in texts { - let key = ValueBoxKey(text).toMemoryBuffer() - let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId) - if indexKeysByFile[mediaId] == nil { - indexKeysByFile[mediaId] = [key] - } else { - indexKeysByFile[mediaId]!.append(key) - } - } - } - } - - for apiDocument in documents { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { - let fileIndexKeys: [MemoryBuffer] - if let indexKeys = indexKeysByFile[id] { - fileIndexKeys = indexKeys - } else { - fileIndexKeys = [] - } - items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys)) - } - } } return .single(.complete(info, items)) } @@ -294,6 +236,333 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri } } +public enum RenameStickerSetError { + case generic +} + +func _internal_renameStickerSet(account: Account, packReference: StickerPackReference, title: String) -> Signal { + return account.network.request(Api.functions.stickers.renameStickerSet(stickerset: packReference.apiInputStickerSet, title: title)) + |> mapError { error -> RenameStickerSetError in + return .generic + } + |> mapToSignal { result -> Signal in + guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else { + return .complete() + } + return account.postbox.transaction { transaction -> Void in + transaction.replaceItemCollectionInfos(namespace: Namespaces.ItemCollection.CloudStickerPacks, itemCollectionInfos: [(info.id, info)]) + + cacheStickerPack(transaction: transaction, info: info, items: items) + } + |> castError(RenameStickerSetError.self) + |> ignoreValues + } +} + +public enum DeleteStickerSetError { + case generic +} + +func _internal_deleteStickerSet(account: Account, packReference: StickerPackReference) -> Signal { + return account.network.request(Api.functions.stickers.deleteStickerSet(stickerset: packReference.apiInputStickerSet)) + |> mapError { error -> DeleteStickerSetError in + return .generic + } + |> mapToSignal { _ in + return account.postbox.transaction { transaction in + if case let .id(id, _) = packReference { + transaction.removeItemCollection(collectionId: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id)) + } + } + |> castError(DeleteStickerSetError.self) + } + |> ignoreValues +} + +public enum AddStickerToSetError { + case generic +} + +func _internal_addStickerToStickerSet(account: Account, packReference: StickerPackReference, sticker: ImportSticker) -> Signal { + let uploadSticker: Signal + if let resource = sticker.resource as? CloudDocumentMediaResource { + uploadSticker = .single(.complete(resource, sticker.mimeType)) + } else { + uploadSticker = account.postbox.loadedPeerWithId(account.peerId) + |> castError(AddStickerToSetError.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 { _ -> AddStickerToSetError 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.addStickerToSet(stickerset: packReference.apiInputStickerSet, sticker: inputSticker)) + |> mapError { error -> AddStickerToSetError in + return .generic + } + |> mapToSignal { result -> Signal in + guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else { + return .complete() + } + return account.postbox.transaction { transaction -> Bool in + if transaction.getItemCollectionInfo(collectionId: info.id) != nil { + transaction.replaceItemCollectionItems(collectionId: info.id, items: items) + } + cacheStickerPack(transaction: transaction, info: info, items: items) + return true + } + |> castError(AddStickerToSetError.self) + } + } +} + +public enum ReorderStickerError { + case generic +} + +func _internal_reorderSticker(account: Account, sticker: FileMediaReference, position: Int) -> Signal { + guard let resource = sticker.media.resource as? CloudDocumentMediaResource else { + return .fail(.generic) + } + return account.network.request(Api.functions.stickers.changeStickerPosition(sticker: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), position: Int32(position))) + |> mapError { error -> ReorderStickerError 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(ReorderStickerError.self) + |> ignoreValues + } +} + + +public enum DeleteStickerError { + case generic +} + +func _internal_deleteStickerFromStickerSet(account: Account, sticker: FileMediaReference) -> Signal { + guard let resource = sticker.media.resource as? CloudDocumentMediaResource else { + return .fail(.generic) + } + return account.network.request(Api.functions.stickers.removeStickerFromSet(sticker: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)))) + |> mapError { error -> DeleteStickerError 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(DeleteStickerError.self) + |> ignoreValues + } +} + +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) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> [(StickerPackCollectionInfo, StickerPackItem?)] in + guard let result else { + return [] + } + var infos: [(StickerPackCollectionInfo, StickerPackItem?)] = [] + switch result { + case let .myStickers(_, sets): + for set in sets { + switch set { + case let .stickerSetCovered(set, cover): + let namespace: ItemCollectionId.Namespace + switch set { + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _): + if (flags & (1 << 3)) != 0 { + namespace = Namespaces.ItemCollection.CloudMaskPacks + } else if (flags & (1 << 7)) != 0 { + namespace = Namespaces.ItemCollection.CloudEmojiPacks + } else { + namespace = Namespaces.ItemCollection.CloudStickerPacks + } + } + let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + var firstItem: StickerPackItem? + if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []) + } + infos.append((info, firstItem)) + case let .stickerSetFullCovered(set, _, _, documents): + let namespace: ItemCollectionId.Namespace + switch set { + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _): + if (flags & (1 << 3)) != 0 { + namespace = Namespaces.ItemCollection.CloudMaskPacks + } else if (flags & (1 << 7)) != 0 { + namespace = Namespaces.ItemCollection.CloudEmojiPacks + } else { + namespace = Namespaces.ItemCollection.CloudStickerPacks + } + } + let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + var firstItem: StickerPackItem? + if let apiDocument = documents.first { + if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []) + } + } + infos.append((info, firstItem)) + default: + break + } + } + } + return infos + } +} + +private func parseStickerSetInfoAndItems(apiStickerSet: Api.messages.StickerSet) -> (StickerPackCollectionInfo, [StickerPackItem])? { + switch apiStickerSet { + case .stickerSetNotModified: + return nil + case let .stickerSet(set, packs, keywords, documents): + let namespace: ItemCollectionId.Namespace + switch set { + case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _): + if (flags & (1 << 3)) != 0 { + namespace = Namespaces.ItemCollection.CloudMaskPacks + } else if (flags & (1 << 7)) != 0 { + namespace = Namespaces.ItemCollection.CloudEmojiPacks + } else { + namespace = Namespaces.ItemCollection.CloudStickerPacks + } + } + let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:] + for pack in packs { + switch pack { + case let .stickerPack(text, fileIds): + let key = ValueBoxKey(text).toMemoryBuffer() + for fileId in fileIds { + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if indexKeysByFile[mediaId] == nil { + indexKeysByFile[mediaId] = [key] + } else { + indexKeysByFile[mediaId]!.append(key) + } + } + } + } + for keyword in keywords { + switch keyword { + case let .stickerKeyword(documentId, texts): + for text in texts { + let key = ValueBoxKey(text).toMemoryBuffer() + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId) + if indexKeysByFile[mediaId] == nil { + indexKeysByFile[mediaId] = [key] + } else { + indexKeysByFile[mediaId]!.append(key) + } + } + } + } + + var items: [StickerPackItem] = [] + for apiDocument in documents { + if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + let fileIndexKeys: [MemoryBuffer] + if let indexKeys = indexKeysByFile[id] { + fileIndexKeys = indexKeys + } else { + fileIndexKeys = [] + } + items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys)) + } + } + return (info, items) + } +} + func _internal_getStickerSetShortNameSuggestion(account: Account, title: String) -> Signal { return account.network.request(Api.functions.stickers.suggestShortName(title: title)) |> map (Optional.init) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift index de8ac8e496..a8e6444d67 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift @@ -55,6 +55,9 @@ extension StickerPackCollectionInfo { if (flags & (1 << 10)) != 0 { setFlags.insert(.isAvailableAsChannelStatus) } + if (flags & (1 << 11)) != 0 { + setFlags.insert(.isCreator) + } var thumbnailRepresentation: TelegramMediaImageRepresentation? var immediateThumbnailData: Data? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index acc8702807..01db7419d8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -86,6 +86,34 @@ public extension TelegramEngine { return _internal_createStickerSet(account: self.account, title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnail, type: type, software: software) } + public func renameStickerSet(packReference: StickerPackReference, title: String) -> Signal { + return _internal_renameStickerSet(account: self.account, packReference: packReference, title: title) + } + + public func deleteStickerSet(packReference: StickerPackReference) -> Signal { + return _internal_deleteStickerSet(account: self.account, packReference: packReference) + } + + public func addStickerToStickerSet(packReference: StickerPackReference, sticker: ImportSticker) -> Signal { + return _internal_addStickerToStickerSet(account: self.account, packReference: packReference, sticker: sticker) + } + + public func reorderSticker(sticker: FileMediaReference, position: Int) -> Signal { + return _internal_reorderSticker(account: self.account, sticker: sticker, position: position) + } + + public func deleteStickerFromStickerSet(sticker: FileMediaReference) -> Signal { + 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) + } + public func getStickerSetShortNameSuggestion(title: String) -> Signal { return _internal_getStickerSetShortNameSuggestion(account: self.account, title: title) } diff --git a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift index 2986a87c58..faf6979c99 100644 --- a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift @@ -1,15 +1,15 @@ import Foundation import AppBundle -private final class CurrencyFormatterEntry { - let symbol: String - let thousandsSeparator: String - let decimalSeparator: String - let symbolOnLeft: Bool - let spaceBetweenAmountAndSymbol: Bool - let decimalDigits: Int +public final class CurrencyFormatterEntry { + public let symbol: String + public let thousandsSeparator: String + public let decimalSeparator: String + public let symbolOnLeft: Bool + public let spaceBetweenAmountAndSymbol: Bool + public let decimalDigits: Int - init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { + public init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { self.symbol = symbol self.thousandsSeparator = thousandsSeparator self.decimalSeparator = decimalSeparator @@ -191,8 +191,8 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { } } -public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String, Bool) { - if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { +public func formatCurrencyAmountCustom(_ amount: Int64, currency: String, customFormat: CurrencyFormatterEntry? = nil) -> (String, String, Bool) { + if let entry = customFormat ?? currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { var result = "" if amount < 0 { result.append("-") diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index 8cdc962093..8483290226 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -143,7 +143,6 @@ public func stringForCompactDate(timestamp: Int32, strings: PresentationStrings, var timeinfo: tm = tm() localtime_r(&t, &timeinfo) - //TODO:localize return "\(shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)) \(timeinfo.tm_mday) \(monthAtIndex(Int(timeinfo.tm_mon), strings: strings))" } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index b8a93acb65..5892c6150a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -437,6 +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/StickerPickerScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 28692a32ca..cfab79f187 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -434,6 +434,7 @@ final class AvatarEditorScreenComponent: Component { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -455,6 +456,7 @@ final class AvatarEditorScreenComponent: Component { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -513,6 +515,7 @@ final class AvatarEditorScreenComponent: Component { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -547,6 +550,7 @@ final class AvatarEditorScreenComponent: Component { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -654,6 +658,7 @@ final class AvatarEditorScreenComponent: Component { context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) } }, + editAction: { _ in }, pushController: { c in }, presentController: { c in @@ -784,6 +789,7 @@ final class AvatarEditorScreenComponent: Component { } else if groupId == AnyHashable("peerSpecific") { } }, + editAction: { _ in }, pushController: { c in }, presentController: { c in 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/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index 69bc656b17..ef3ac7d6ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -156,6 +156,18 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess } } + if authorTitle == nil { + for attribute in message.attributes { + if let attribute = attribute as? InlineBusinessBotMessageAttribute { + if let title = attribute.title { + authorTitle = title + } else if let peerId = attribute.peerId, let peer = message.peers[peerId] { + authorTitle = peer.debugDisplayTitle + } + } + } + } + if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { authorTitle = nil } 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/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index d746fb87a8..5f8b66660a 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -804,6 +804,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }) } }, + editAction: { _ in }, pushController: { [weak interaction] controller in guard let interaction else { return @@ -969,6 +970,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1018,6 +1020,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: 3, displayPremiumBadges: false, headerItem: nil, @@ -1077,6 +1080,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1108,6 +1112,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1313,6 +1318,33 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else if groupId == AnyHashable("peerSpecific") { } }, + editAction: { [weak interaction] groupId in + guard let collectionId = groupId.base as? ItemCollectionId else { + return + } + let viewKey = PostboxViewKey.itemCollectionInfo(id: collectionId) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak interaction] views in + guard let interaction, let view = views.views[viewKey] as? ItemCollectionInfoView, let info = view.info as? StickerPackCollectionInfo else { + return + } + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + let controller = context.sharedContext.makeStickerPackScreen( + context: context, + updatedPresentationData: nil, + mainStickerPack: packReference, + stickerPacks: [packReference], + loadedStickerPacks: [], + isEditing: true, + parentNavigationController: nil, + sendSticker: { [weak interaction] fileReference, sourceView, sourceRect in + return interaction?.sendSticker(fileReference, false, false, nil, false, sourceView, sourceRect, nil, []) ?? false + } + ) + interaction.presentController(controller, nil) + }) + }, pushController: { [weak interaction] controller in guard let interaction else { return @@ -1382,6 +1414,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1413,6 +1446,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -2327,6 +2361,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi strongSelf.presentController?(actionSheet) } }, + editAction: { _ in }, pushController: { _ in }, presentController: { _ in @@ -2774,7 +2809,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { switch attribute { case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): if let packReference = packReference { - let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in + let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) return true }) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift index be655b579d..fe6a48cd38 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift @@ -124,6 +124,8 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer maybeFile = foundItem.file case let .pack(fileValue): maybeFile = fileValue + case .image: + break } } guard let file = maybeFile else { diff --git a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift index 1dc7209e00..bec3370cf2 100644 --- a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift +++ b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift @@ -145,6 +145,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject private var lastTimeStep: Double = 0.0 public var animationSpeed: Float = 1.0 + public var playsBackwards: Bool = false public var becameEmpty: (() -> Void)? diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index bce0b9310a..f8349a2818 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -462,6 +462,7 @@ public final class EmojiStatusSelectionController: ViewController { }, clearGroup: { groupId in }, + editAction: { _ in }, pushController: { c in }, presentController: { c in @@ -584,6 +585,7 @@ public final class EmojiStatusSelectionController: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -639,6 +641,7 @@ public final class EmojiStatusSelectionController: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -671,6 +674,7 @@ public final class EmojiStatusSelectionController: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 7c141f89ee..ffca635585 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -660,25 +660,44 @@ private final class PremiumBadgeView: UIView { } private final class GroupHeaderActionButton: UIButton { + override static var layerClass: AnyClass { + return PassthroughLayer.self + } + + let tintContainerLayer: SimpleLayer + private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)? private let backgroundLayer: SimpleLayer + private let tintBackgroundLayer: SimpleLayer private let textLayer: SimpleLayer + private let tintTextLayer: SimpleLayer private let pressed: () -> Void init(pressed: @escaping () -> Void) { self.pressed = pressed + self.tintContainerLayer = SimpleLayer() + self.backgroundLayer = SimpleLayer() self.backgroundLayer.masksToBounds = true + self.tintBackgroundLayer = SimpleLayer() + self.tintBackgroundLayer.masksToBounds = true + self.textLayer = SimpleLayer() + self.tintTextLayer = SimpleLayer() super.init(frame: CGRect()) + (self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer + self.layer.addSublayer(self.backgroundLayer) self.layer.addSublayer(self.textLayer) self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside) + + self.tintContainerLayer.addSublayer(self.tintBackgroundLayer) + self.tintContainerLayer.addSublayer(self.tintTextLayer) } required init(coder: NSCoder) { @@ -719,18 +738,34 @@ private final class GroupHeaderActionButton: UIButton { super.touchesCancelled(touches, with: event) } - func update(theme: PresentationTheme, title: String) -> CGSize { + func update(theme: PresentationTheme, title: String, compact: Bool) -> CGSize { let textConstrainedWidth: CGFloat = 100.0 - let color = theme.list.itemCheckColors.foregroundColor - self.backgroundLayer.backgroundColor = theme.list.itemCheckColors.fillColor.cgColor + let needsVibrancy = !theme.overallDarkAppearance && compact + + let foregroundColor: UIColor + let backgroundColor: UIColor + + if compact { + foregroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + backgroundColor = foregroundColor.withMultipliedAlpha(0.2) + } else { + foregroundColor = theme.list.itemCheckColors.foregroundColor + backgroundColor = theme.list.itemCheckColors.fillColor + } + + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + self.tintBackgroundLayer.backgroundColor = UIColor.white.withAlphaComponent(0.2).cgColor + + self.tintContainerLayer.isHidden = !needsVibrancy let textSize: CGSize - if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth { + if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == foregroundColor, currentTextLayout.constrainedWidth == textConstrainedWidth { textSize = currentTextLayout.size } else { - let font: UIFont = Font.semibold(15.0) - let string = NSAttributedString(string: title.uppercased(), font: font, textColor: color) + let font: UIFont = compact ? Font.medium(11.0) : Font.semibold(15.0) + let string = NSAttributedString(string: title.uppercased(), font: font, textColor: foregroundColor) + let tintString = NSAttributedString(string: title.uppercased(), font: font, textColor: .white) let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil) textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height)) self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in @@ -741,17 +776,29 @@ private final class GroupHeaderActionButton: UIButton { UIGraphicsPopContext() })?.cgImage - self.currentTextLayout = (title, color, textConstrainedWidth, textSize) + self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + + tintString.draw(in: stringBounds) + + UIGraphicsPopContext() + })?.cgImage + self.currentTextLayout = (title, foregroundColor, textConstrainedWidth, textSize) } - let size = CGSize(width: textSize.width + 16.0 * 2.0, height: 28.0) + let size = CGSize(width: textSize.width + (compact ? 6.0 : 16.0) * 2.0, height: compact ? 16.0 : 28.0) - let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize) + let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) self.textLayer.frame = textFrame + self.tintTextLayer.frame = textFrame self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size) self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0 + self.tintBackgroundLayer.frame = self.backgroundLayer.frame + self.tintBackgroundLayer.cornerRadius = self.backgroundLayer.cornerRadius + return size } } @@ -816,6 +863,7 @@ private final class GroupHeaderLayer: UIView { layoutType: EmojiPagerContentComponent.ItemLayoutType, hasTopSeparator: Bool, actionButtonTitle: String?, + actionButtonIsCompact: Bool, title: String, subtitle: String?, badge: String?, @@ -877,9 +925,10 @@ private final class GroupHeaderLayer: UIView { actionButton = GroupHeaderActionButton(pressed: self.actionPressed) self.actionButton = actionButton self.addSubview(actionButton) + self.tintContentLayer.addSublayer(actionButton.tintContainerLayer) } - actionButtonSize = actionButton.update(theme: theme, title: actionButtonTitle) + actionButtonSize = actionButton.update(theme: theme, title: actionButtonTitle, compact: actionButtonIsCompact) } else { if let actionButton = self.actionButton { self.actionButton = nil @@ -1210,7 +1259,9 @@ private final class GroupHeaderLayer: UIView { } if let actionButtonSize = actionButtonSize, let actionButton = self.actionButton { - actionButton.frame = CGRect(origin: CGPoint(x: size.width - actionButtonSize.width, y: textFrame.minY + 3.0), size: actionButtonSize) + let actionButtonFrame = CGRect(origin: CGPoint(x: size.width - actionButtonSize.width, y: textFrame.minY + (actionButtonIsCompact ? 0.0 : 3.0)), size: actionButtonSize) + actionButton.bounds = CGRect(origin: CGPoint(), size: actionButtonFrame.size) + actionButton.center = actionButtonFrame.center } if hasTopSeparator { @@ -2414,6 +2465,7 @@ public final class EmojiPagerContentComponent: Component { public let openSearch: () -> Void public let addGroupAction: (AnyHashable, Bool, Bool) -> Void public let clearGroup: (AnyHashable) -> Void + public let editAction: (AnyHashable) -> Void public let pushController: (ViewController) -> Void public let presentController: (ViewController) -> Void public let presentGlobalOverlayController: (ViewController) -> Void @@ -2443,6 +2495,7 @@ public final class EmojiPagerContentComponent: Component { openSearch: @escaping () -> Void, addGroupAction: @escaping (AnyHashable, Bool, Bool) -> Void, clearGroup: @escaping (AnyHashable) -> Void, + editAction: @escaping (AnyHashable) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, presentGlobalOverlayController: @escaping (ViewController) -> Void, @@ -2470,6 +2523,7 @@ public final class EmojiPagerContentComponent: Component { self.openSearch = openSearch self.addGroupAction = addGroupAction self.clearGroup = clearGroup + self.editAction = editAction self.pushController = pushController self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController @@ -2613,6 +2667,7 @@ public final class EmojiPagerContentComponent: Component { public let isPremiumLocked: Bool public let isEmbedded: Bool public let hasClear: Bool + public let hasEdit: Bool public let collapsedLineCount: Int? public let displayPremiumBadges: Bool public let headerItem: EntityKeyboardAnimationData? @@ -2631,6 +2686,7 @@ public final class EmojiPagerContentComponent: Component { isPremiumLocked: Bool, isEmbedded: Bool, hasClear: Bool, + hasEdit: Bool, collapsedLineCount: Int?, displayPremiumBadges: Bool, headerItem: EntityKeyboardAnimationData?, @@ -2648,6 +2704,7 @@ public final class EmojiPagerContentComponent: Component { self.isPremiumLocked = isPremiumLocked self.isEmbedded = isEmbedded self.hasClear = hasClear + self.hasEdit = hasEdit self.collapsedLineCount = collapsedLineCount self.displayPremiumBadges = displayPremiumBadges self.headerItem = headerItem @@ -3382,14 +3439,22 @@ public final class EmojiPagerContentComponent: Component { public let item: Item - private let content: ItemContent + private var content: ItemContent + private var theme: PresentationTheme? + private let placeholderColor: UIColor let pixelSize: CGSize + let pointSize: CGSize private let size: CGSize private var disposable: Disposable? private var fetchDisposable: Disposable? private var premiumBadgeView: PremiumBadgeView? + private var iconLayer: SimpleLayer? + private var tintIconLayer: SimpleLayer? + + private(set) var tintContentLayer: SimpleLayer? + private var badge: Badge? private var validSize: CGSize? @@ -3421,6 +3486,52 @@ public final class EmojiPagerContentComponent: Component { } } + override public var position: CGPoint { + get { + return super.position + } set(value) { + if let mirrorLayer = self.tintContentLayer { + mirrorLayer.position = value + } + super.position = value + } + } + + override public var bounds: CGRect { + get { + return super.bounds + } set(value) { + if let mirrorLayer = self.tintContentLayer { + mirrorLayer.bounds = value + } + super.bounds = value + } + } + + override public func add(_ animation: CAAnimation, forKey key: String?) { + if let mirrorLayer = self.tintContentLayer { + mirrorLayer.add(animation, forKey: key) + } + + super.add(animation, forKey: key) + } + + override public func removeAllAnimations() { + if let mirrorLayer = self.tintContentLayer { + mirrorLayer.removeAllAnimations() + } + + super.removeAllAnimations() + } + + override public func removeAnimation(forKey: String) { + if let mirrorLayer = self.tintContentLayer { + mirrorLayer.removeAnimation(forKey: forKey) + } + + super.removeAnimation(forKey: forKey) + } + public var onContentsUpdate: () -> Void = {} public var onLoop: () -> Void = {} @@ -3445,6 +3556,7 @@ public final class EmojiPagerContentComponent: Component { let scale = min(2.0, UIScreenScale) let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale) self.pixelSize = pixelSize + self.pointSize = pointSize self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale) super.init() @@ -3561,20 +3673,26 @@ public final class EmojiPagerContentComponent: Component { image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } case .add: - context.setFillColor(UIColor.black.withAlphaComponent(0.08).cgColor) - context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) - context.setFillColor(UIColor.black.withAlphaComponent(0.16).cgColor) - - let plusSize = CGSize(width: 4.5, height: 31.5) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath) - context.fillPath() + break } UIGraphicsPopContext() })?.withRenderingMode(icon == .stop ? .alwaysTemplate : .alwaysOriginal) self.contents = image?.cgImage } + + if case .icon(.add) = content { + let tintContentLayer = SimpleLayer() + self.tintContentLayer = tintContentLayer + + let iconLayer = SimpleLayer() + self.iconLayer = iconLayer + self.addSublayer(iconLayer) + + let tintIconLayer = SimpleLayer() + self.tintIconLayer = tintIconLayer + tintContentLayer.addSublayer(tintIconLayer) + } } override public init(layer: Any) { @@ -3588,6 +3706,7 @@ public final class EmojiPagerContentComponent: Component { self.placeholderColor = layer.placeholderColor self.size = layer.size self.pixelSize = layer.pixelSize + self.pointSize = layer.pointSize self.onUpdateDisplayPlaceholder = { _, _ in } @@ -3613,8 +3732,22 @@ public final class EmojiPagerContentComponent: Component { return nullAction } - func update(content: ItemContent) { + func update( + content: ItemContent, + theme: PresentationTheme + ) { + var themeUpdated = false + if self.theme !== theme { + self.theme = theme + themeUpdated = true + } + var contentUpdated = false if self.content != content { + self.content = content + contentUpdated = true + } + + if themeUpdated || contentUpdated { if case let .icon(icon) = content, case let .topic(title, color) = icon { let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -3630,15 +3763,56 @@ public final class EmojiPagerContentComponent: Component { UIGraphicsPopContext() }) self.contents = image?.cgImage + } else if case .icon(.add) = content { + guard let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer else { + return + } + func generateIcon(color: UIColor) -> UIImage? { + return generateImage(self.pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + UIGraphicsPushContext(context) + + context.setFillColor(color.withMultipliedAlpha(0.2).cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) + context.setFillColor(color.cgColor) + + let plusSize = CGSize(width: 4.5, height: 31.5) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath) + context.fillPath() + + UIGraphicsPopContext() + }) + } + + let needsVibrancy = !theme.overallDarkAppearance + let color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + + iconLayer.contents = generateIcon(color: color)?.cgImage + tintIconLayer.contents = generateIcon(color: .white)?.cgImage + + tintIconLayer.isHidden = !needsVibrancy } } } - func update(transition: Transition, size: CGSize, badge: Badge?, blurredBadgeColor: UIColor, blurredBadgeBackgroundColor: UIColor) { + func update( + transition: Transition, + size: CGSize, + badge: Badge?, + blurredBadgeColor: UIColor, + blurredBadgeBackgroundColor: UIColor + ) { if self.badge != badge || self.validSize != size { self.badge = badge self.validSize = size + if let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer { + transition.setFrame(layer: iconLayer, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(layer: tintIconLayer, frame: CGRect(origin: .zero, size: size)) + } + if let badge = badge { var badgeTransition = transition let premiumBadgeView: PremiumBadgeView @@ -5583,6 +5757,7 @@ public final class EmojiPagerContentComponent: Component { var headerCentralContentWidth: CGFloat? var headerSizeUpdated = false if let title = itemGroup.title { + let hasEdit = itemGroup.hasEdit validGroupHeaderIds.insert(itemGroup.groupId) let groupHeaderView: GroupHeaderLayer var groupHeaderTransition = transition @@ -5596,7 +5771,11 @@ public final class EmojiPagerContentComponent: Component { guard let strongSelf = self, let component = strongSelf.component else { return } - component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false, true) + if hasEdit { + component.inputInteractionHolder.inputInteraction?.editAction(groupId) + } else { + component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false, true) + } }, performItemAction: { [weak self] item, view, rect, layer in guard let strongSelf = self, let component = strongSelf.component else { @@ -5611,8 +5790,12 @@ public final class EmojiPagerContentComponent: Component { } var actionButtonTitle: String? + var actionButtonIsCompact = false if case .detailed = itemLayout.layoutType, itemGroup.isFeatured { actionButtonTitle = itemGroup.actionButtonTitle + } else if itemGroup.hasEdit { + actionButtonTitle = keyboardChildEnvironment.strings.Stickers_Edit + actionButtonIsCompact = true } let hasTopSeparator = false @@ -5624,6 +5807,7 @@ public final class EmojiPagerContentComponent: Component { layoutType: itemLayout.layoutType, hasTopSeparator: hasTopSeparator, actionButtonTitle: actionButtonTitle, + actionButtonIsCompact: actionButtonIsCompact, title: title, subtitle: itemGroup.subtitle, badge: itemGroup.badge, @@ -5956,8 +6140,11 @@ public final class EmojiPagerContentComponent: Component { } ) - self.scrollView.layer.addSublayer(itemLayer) self.visibleItemLayers[itemId] = itemLayer + self.scrollView.layer.addSublayer(itemLayer) + if let tintContentLayer = itemLayer.tintContentLayer { + self.mirrorContentScrollView.layer.addSublayer(tintContentLayer) + } } var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index) @@ -6002,10 +6189,16 @@ public final class EmojiPagerContentComponent: Component { } if case .icon = item.content { - itemLayer.update(content: item.content) + itemLayer.update(content: item.content, theme: keyboardChildEnvironment.theme) } - itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor) + itemLayer.update( + transition: transition, + size: itemFrame.size, + badge: badge, + blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), + blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor + ) switch item.tintMode { case let .custom(color): @@ -6166,6 +6359,7 @@ public final class EmojiPagerContentComponent: Component { itemLayer.opacity = 0.0 itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.16) itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemLayer] _ in + itemLayer?.tintContentLayer?.removeFromSuperlayer() itemLayer?.removeFromSuperlayer() }) @@ -6185,6 +6379,7 @@ public final class EmojiPagerContentComponent: Component { } } else if let position = updatedItemPositions?[.item(id: id)], transitionHintInstalledGroupId != id.groupId { transition.setPosition(layer: itemLayer, position: position, completion: { [weak itemLayer] _ in + itemLayer?.tintContentLayer?.removeFromSuperlayer() itemLayer?.removeFromSuperlayer() }) if let itemSelectionLayer = itemSelectionLayer { @@ -6198,6 +6393,7 @@ public final class EmojiPagerContentComponent: Component { itemLayer.opacity = 0.0 itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2) itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemLayer] _ in + itemLayer?.tintContentLayer?.removeFromSuperlayer() itemLayer?.removeFromSuperlayer() }) @@ -6217,7 +6413,9 @@ public final class EmojiPagerContentComponent: Component { } } } else { + itemLayer.tintContentLayer?.removeFromSuperlayer() itemLayer.removeFromSuperlayer() + if let itemSelectionLayer = itemSelectionLayer { itemSelectionLayer.removeFromSuperlayer() itemSelectionLayer.tintContainerLayer.removeFromSuperlayer() @@ -7340,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/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index 4fbf2390c1..1c05326830 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -39,6 +39,7 @@ public extension EmojiPagerContentComponent { case groupPhoto case backgroundIcon case reactionList + case stickerAlt } static func emojiInputData( @@ -58,6 +59,7 @@ public extension EmojiPagerContentComponent { topicColor: Int32? = nil, backgroundIconColor: UIColor? = nil, hasSearch: Bool = true, + hasRecent: Bool = true, forceHasPremium: Bool = false, premiumIfSavedMessages: Bool = true, hideBackground: Bool = false @@ -1158,6 +1160,50 @@ public extension EmojiPagerContentComponent { } } } + } else if case .stickerAlt = subject { + for reactionItem in topReactionItems { +// if existingIds.contains(reactionItem.reaction) { +// continue +// } +// existingIds.insert(reactionItem.reaction) + + let icon: EmojiPagerContentComponent.Item.Icon + if case .reaction(onlyTop: true) = subject { + icon = .none + } else if !hasPremium, case .custom = reactionItem.reaction { + icon = .locked + } else { + icon = .none + } + + var tintMode: Item.TintMode = .none + if reactionItem.file.isCustomTemplateEmoji { + tintMode = .primary + } + + let animationFile = reactionItem.file + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: animationFile, + subgroupId: nil, + icon: icon, + tintMode: tintMode + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + + if itemGroups[groupIndex].items.count >= 8 * 1000 { + break + } + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) + } + } } let hasRecentEmoji = ![.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction, .status, .profilePhoto, .groupPhoto, .topicIcon, .backgroundIcon, .reactionList, .messageTag].contains(subject) @@ -1515,6 +1561,7 @@ public extension EmojiPagerContentComponent { isPremiumLocked: group.isPremiumLocked, isEmbedded: isEmbedded, hasClear: hasClear, + hasEdit: false, collapsedLineCount: group.collapsedLineCount, displayPremiumBadges: false, headerItem: headerItem, @@ -1643,6 +1690,7 @@ public extension EmojiPagerContentComponent { var isPremiumLocked: Bool var isFeatured: Bool var displayPremiumBadges: Bool + var hasEdit: Bool var headerItem: EntityKeyboardAnimationData? var items: [EmojiPagerContentComponent.Item] } @@ -1741,6 +1789,7 @@ public extension EmojiPagerContentComponent { isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, + hasEdit: false, headerItem: nil, items: [resultItem] ) @@ -1778,7 +1827,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } } @@ -1812,9 +1861,21 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } + + if !forceHasPremium, let groupIndex = itemGroupIndexById[groupId] { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .icon(.add), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + itemGroups[groupIndex].items.insert(resultItem, at: 0) + } } var avatarPeer: EnginePeer? @@ -1851,7 +1912,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } } @@ -1883,9 +1944,11 @@ public extension EmojiPagerContentComponent { var title = "" var headerItem: EntityKeyboardAnimationData? + var hasEdit = false inner: for (id, info, _) in view.collectionInfos { if id == groupId, let info = info as? StickerPackCollectionInfo { title = info.title + hasEdit = info.flags.contains(.isCreator) if let thumbnail = info.thumbnail { let type: EntityKeyboardAnimationData.ItemType @@ -1911,7 +1974,7 @@ public extension EmojiPagerContentComponent { break inner } } - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, headerItem: headerItem, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, hasEdit: hasEdit, headerItem: headerItem, items: [resultItem])) } } @@ -1974,7 +2037,7 @@ public extension EmojiPagerContentComponent { ) } - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, headerItem: headerItem, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, hasEdit: false, headerItem: headerItem, items: [resultItem])) } } } @@ -1989,6 +2052,7 @@ public extension EmojiPagerContentComponent { } else if group.id == AnyHashable("featuredTop") { hasClear = true isEmbedded = true + } else if group.id == AnyHashable("saved") { } return EmojiPagerContentComponent.ItemGroup( @@ -2002,6 +2066,7 @@ public extension EmojiPagerContentComponent { isPremiumLocked: group.isPremiumLocked, isEmbedded: isEmbedded, hasClear: hasClear, + hasEdit: group.hasEdit, collapsedLineCount: nil, displayPremiumBadges: group.displayPremiumBadges, headerItem: group.headerItem, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift index 6307c7d3d0..68a150eb48 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -128,6 +128,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode isPremiumLocked: !self.hasPremiumForInstallation, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: 3, displayPremiumBadges: false, headerItem: nil, @@ -171,6 +172,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode }, clearGroup: { _ in }, + editAction: { _ in }, pushController: { _ in }, presentController: { _ in @@ -293,6 +295,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -348,6 +351,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -382,6 +386,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 59e48bdfc0..e7d812db0e 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -919,6 +919,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { }, clearGroup: { _ in }, + editAction: { _ in + }, pushController: { c in }, presentController: { c in diff --git a/submodules/TelegramUI/Components/MediaEditor/BUILD b/submodules/TelegramUI/Components/MediaEditor/BUILD index 54f2c6a7b9..ce3155b45d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/BUILD +++ b/submodules/TelegramUI/Components/MediaEditor/BUILD @@ -69,6 +69,8 @@ swift_library( "//submodules/YuvConversion:YuvConversion", "//submodules/FastBlur:FastBlur", "//submodules/WallpaperBackgroundNode", + "//submodules/ImageTransparency", + "//submodules/FFMpegBinding", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal index 730de12915..bb880e31b7 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal @@ -200,5 +200,5 @@ fragment half4 adjustmentsFragmentShader(RasterizerData in [[stage_in]], result.rgb = result.rgb + noise * adjustments.grain * 0.04; } - return result; + return half4(result.rgb * result.a, result.a); } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal index a4fa037c62..5a9790436c 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal @@ -39,12 +39,12 @@ fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], float aspectRatio = R.x / R.y; constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); - half3 color = texture.sample(samplr, in.texCoord).rgb; - float colorAlpha = min(1.0, adjustments.isOpaque + mask.sample(samplr, in.texCoord).r); + half4 color = texture.sample(samplr, in.texCoord); + float colorAlpha = min(1.0, adjustments.isOpaque * color.a + mask.sample(samplr, in.texCoord).r); float t = 1.0 / adjustments.dimensions.y; float side = 1.0 * aspectRatio; float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, adjustments.roundness)), side * adjustments.roundness)); - return mix(half4(color, 0.0), half4(color, colorAlpha * adjustments.alpha), distance); + return mix(half4(color.rgb, 0.0), half4(color.rgb, colorAlpha * adjustments.alpha), distance); } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift index 9ee76ac9cf..cd92007ef7 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift @@ -97,9 +97,9 @@ final class AdjustmentsRenderPass: DefaultRenderPass { } override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - guard self.adjustments.hasValues else { - return input - } +// guard self.adjustments.hasValues else { +// return input +// } self.setupVerticesBuffer(device: device) let width = input.width diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift index fe2f1100f0..09856f307f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift @@ -55,67 +55,126 @@ public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) -> } } -public enum CutoutResult { - case image(UIImage) - case pixelBuffer(CVPixelBuffer) +public struct CutoutResult { + public enum Image { + case image(UIImage, CIImage) + case pixelBuffer(CVPixelBuffer) + } + + public let index: Int + public let extractedImage: Image? + public let maskImage: Image? + public let backgroundImage: Image? } -public func cutoutImage(from image: UIImage, atPoint point: CGPoint?, asImage: Bool) -> Signal { - if #available(iOS 17.0, *) { - guard let cgImage = image.cgImage else { - return .single(nil) - } - return Signal { subscriber in - let ciContext = CIContext(options: nil) - let inputImage = CIImage(cgImage: cgImage) +public enum CutoutTarget { + case point(CGPoint?) + case index(Int) + case all +} + +public func cutoutImage( + from image: UIImage, + editedImage: UIImage? = nil, + values: MediaEditorValues?, + target: CutoutTarget, + includeExtracted: Bool = true, + completion: @escaping ([CutoutResult]) -> Void +) { + if #available(iOS 17.0, *), let cgImage = image.cgImage { + let ciContext = CIContext(options: nil) + let inputImage = CIImage(cgImage: cgImage) + + queue.async { let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else { - subscriber.putNext(nil) - subscriber.putCompletion() + completion([]) return } - let instances = IndexSet(instances(atPoint: point, inObservation: result).prefix(1)) - if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { - if asImage { - let filter = CIFilter.blendWithMask() - filter.inputImage = inputImage - filter.backgroundImage = CIImage(color: .clear) - filter.maskImage = CIImage(cvPixelBuffer: mask) - if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { - let image = UIImage(cgImage: cgImage) - subscriber.putNext(.image(image)) - subscriber.putCompletion() - return + let targetInstances: IndexSet + switch target { + case let .point(point): + targetInstances = instances(atPoint: point, inObservation: result) + case let .index(index): + targetInstances = IndexSet([index]) + case .all: + targetInstances = result.allInstances + } + + var results: [CutoutResult] = [] + for instance in targetInstances { + if let mask = try? result.generateScaledMaskForImage(forInstances: IndexSet(integer: instance), from: handler) { + let extractedImage: CutoutResult.Image? + if includeExtracted { + let filter = CIFilter.blendWithMask() + filter.backgroundImage = CIImage(color: .clear) + + let dimensions: CGSize + var maskImage = CIImage(cvPixelBuffer: mask) + if let editedImage = editedImage?.cgImage.flatMap({ CIImage(cgImage: $0) }) { + filter.inputImage = editedImage + dimensions = editedImage.extent.size + + if let values { + let initialScale: CGFloat + if maskImage.extent.height > maskImage.extent.width { + initialScale = dimensions.width / maskImage.extent.width + } else { + initialScale = dimensions.width / maskImage.extent.height + } + + let dimensions = editedImage.extent.size + maskImage = maskImage.transformed(by: CGAffineTransform(translationX: -maskImage.extent.width / 2.0, y: -maskImage.extent.height / 2.0)) + + var transform = CGAffineTransform.identity + let position = values.cropOffset + let rotation = values.cropRotation + let scale = values.cropScale + transform = transform.translatedBy(x: dimensions.width / 2.0 + position.x, y: dimensions.height / 2.0 + position.y * -1.0) + transform = transform.rotated(by: -rotation) + transform = transform.scaledBy(x: scale * initialScale, y: scale * initialScale) + maskImage = maskImage.transformed(by: transform) + } + } else { + filter.inputImage = inputImage + dimensions = inputImage.extent.size + } + filter.maskImage = maskImage + + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: CGRect(origin: .zero, size: dimensions)) { + extractedImage = .image(UIImage(cgImage: cgImage), output) + } else { + extractedImage = nil + } + } else { + extractedImage = nil } - } else { - let filter = CIFilter.blendWithMask() - filter.inputImage = CIImage(color: .white) - filter.backgroundImage = CIImage(color: .black) - filter.maskImage = CIImage(cvPixelBuffer: mask) - if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { - let image = UIImage(cgImage: cgImage) - subscriber.putNext(.image(image)) - subscriber.putCompletion() - return + + let maskFilter = CIFilter.blendWithMask() + maskFilter.inputImage = CIImage(color: .white) + maskFilter.backgroundImage = CIImage(color: .black) + maskFilter.maskImage = CIImage(cvPixelBuffer: mask) + let maskImage: CutoutResult.Image? + if let maskOutput = maskFilter.outputImage?.cropped(to: inputImage.extent), let maskCgImage = ciContext.createCGImage(maskOutput, from: inputImage.extent) { + maskImage = .image(UIImage(cgImage: maskCgImage), maskOutput) + } else { + maskImage = nil + } + + if extractedImage != nil || maskImage != nil { + results.append(CutoutResult(index: instance, extractedImage: extractedImage, maskImage: maskImage, backgroundImage: nil)) } -// subscriber.putNext(.pixelBuffer(mask)) -// subscriber.putCompletion() } } - subscriber.putNext(nil) - subscriber.putCompletion() + completion(results) } try? handler.perform([request]) - return ActionDisposable { - request.cancel() - } } - |> runOn(queue) } else { - return .single(nil) + completion([]) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 183b2405e3..4cf696489b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -11,6 +11,7 @@ import TelegramCore import TelegramPresentationData import FastBlur import AccountContext +import ImageTransparency public struct MediaEditorPlayerState { public struct Track: Equatable { @@ -105,6 +106,7 @@ public final class MediaEditor { case asset(PHAsset) case draft(MediaEditorDraft) case message(MessageId) + case sticker(TelegramMediaFile) var dimensions: PixelDimensions { switch self { @@ -116,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) } } } @@ -653,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 @@ -681,7 +698,7 @@ public final class MediaEditor { } - if case .sticker = self.mode { + if case .sticker = self.mode, let cgImage = image.cgImage, !imageHasTransparency(cgImage) { let _ = (cutoutStickerImage(from: image, onlyCheck: true) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self, result != nil else { @@ -1683,21 +1700,7 @@ public final class MediaEditor { self.renderer.renderFrame() } - public func getSeparatedImage(point: CGPoint?) -> Signal { - guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { - return .single(nil) - } - return cutoutImage(from: image, atPoint: point, asImage: true) - |> map { result in - if let result, case let .image(image) = result { - return image - } else { - return nil - } - } - } - - public func removeSeparationMask() { + public func removeSegmentationMask() { self.isCutoutUpdated(false) self.renderer.currentMainInputMask = nil @@ -1706,26 +1709,27 @@ public final class MediaEditor { } } - public func setSeparationMask(point: CGPoint?) { + public func setSegmentationMask(_ image: UIImage) { guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else { return } + + self.isCutoutUpdated(true) + + //TODO:replace with pixelbuffer + self.renderer.currentMainInputMask = loadTexture(image: image, device: device) + if !self.skipRendering { + self.updateRenderChain() + } + } + + public func processImage(with f: @escaping (UIImage, UIImage?) -> Void) { guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { return } - self.isCutoutUpdated(true) - - let _ = (cutoutImage(from: image, atPoint: point, asImage: false) - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self, let result, case let .image(image) = result else { - return - } - //TODO:replace with pixelbuffer - self.renderer.currentMainInputMask = loadTexture(image: image, device: device) - if !self.skipRendering { - self.updateRenderChain() - } - }) + Queue.concurrentDefaultQueue().async { + f(image, self.resultImage) + } } private func maybeGeneratePersonSegmentation(_ image: UIImage?) { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift index 0270ec0a7e..6277737481 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift @@ -78,10 +78,11 @@ func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? { let bytePerPixel = 4 let bytesPerRow = bytePerPixel * Int(width) let bitsPerComponent = 8 - let bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue - let context = CGContext.init(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) - context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height)) - + let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) + if let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) { + context.clear(CGRect(x: 0, y: 0, width: width, height: height)) + context.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height)) + } return rawData } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 3ac4dd34f3..d4e7c8512c 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -505,7 +505,7 @@ public final class MediaEditorVideoExport { result = trackTrimRange } if trackStart + result.duration > maxDuration { - result = CMTimeRange(start: result.start, end: maxDuration - trackStart) + result = CMTimeRange(start: result.start, duration: maxDuration - trackStart) } return result } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift index d6e3fcbe43..443390652e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift @@ -149,7 +149,7 @@ struct VideoEncodeParameters { var roundness: simd_float1 var alpha: simd_float1 var isOpaque: simd_float1 - var empty: simd_float1 + var empty: simd_float1 = 0.0 } final class VideoFinishPass: RenderPass { @@ -242,18 +242,6 @@ final class VideoFinishPass: RenderPass { empty: 0 ) encoder.setFragmentBytes(¶meters, length: MemoryLayout.size, index: 0) -// var resolution = simd_uint2(UInt32(size.width), UInt32(size.height)) -// encoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) -// -// var roundness = roundness -// encoder.setFragmentBytes(&roundness, length: MemoryLayout.size, index: 1) -// -// var alpha = alpha -// encoder.setFragmentBytes(&alpha, length: MemoryLayout.size, index: 2) -// -// var isOpaque = maskTexture == nil ? 1.0 : 0.0 -// encoder.setFragmentBytes(&isOpaque, length: MemoryLayout.size, index: 3) - encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) } @@ -645,9 +633,7 @@ final class VideoFinishPass: RenderPass { length: MemoryLayout.stride * vertices.count, options: []) encoder.setVertexBuffer(buffer, offset: 0, index: 0) - encoder.setFragmentBytes(&self.gradientColors, length: MemoryLayout.size, index: 0) - encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 88ab5ba5ef..c78b21af71 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -52,6 +52,11 @@ swift_library( "//submodules/TelegramUI/Components/MediaScrubberComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/TelegramUI/Components/DustEffect", + "//submodules/WebPBinding", + "//submodules/StickerResources", + "//submodules/StickerPeekUI", + "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController", + "//submodules/TelegramUI/Components/StickerPickerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index fd68d35fbb..7b33ae8f67 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -47,8 +47,8 @@ private final class MediaCutoutScreenComponent: Component { private let doneButton = ComponentView() private let fadeView = UIView() - private let separatedImageView = UIImageView() - + private var outlineViews: [StickerCutoutOutlineView] = [] + private var component: MediaCutoutScreenComponent? private weak var state: State? private var environment: ViewControllerComponentContainer.Environment? @@ -57,9 +57,7 @@ private final class MediaCutoutScreenComponent: Component { self.buttonsContainerView.clipsToBounds = true self.fadeView.alpha = 0.0 - self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) - - self.separatedImageView.contentMode = .scaleAspectFit + self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7) super.init(frame: frame) @@ -69,7 +67,6 @@ private final class MediaCutoutScreenComponent: Component { self.buttonsContainerView.addSubview(self.buttonsBackgroundView) self.addSubview(self.fadeView) - self.addSubview(self.separatedImageView) self.addSubview(self.previewContainerView) self.previewContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap(_:)))) @@ -89,9 +86,21 @@ private final class MediaCutoutScreenComponent: Component { x: location.x / self.previewContainerView.frame.width, y: location.y / self.previewContainerView.frame.height ) - component.mediaEditor.setSeparationMask(point: point) - self.playDissolveAnimation() + component.mediaEditor.processImage { [weak self] originalImage, _ in + cutoutImage(from: originalImage, values: nil, target: .point(point), includeExtracted: false, completion: { [weak self] results in + Queue.mainQueue().async { + if let self, let component = self.component, let result = results.first, let maskImage = result.maskImage { + if case let .image(mask, _) = maskImage { + self.playDissolveAnimation() + component.mediaEditor.setSegmentationMask(mask) + } + } + } + }) + } + + HapticFeedback().impact(.medium) } func animateInFromEditor() { @@ -106,6 +115,9 @@ private final class MediaCutoutScreenComponent: Component { self.cancelButton.view?.isHidden = true self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + for outlineView in self.outlineViews { + outlineView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + } self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completion() }) @@ -210,7 +222,7 @@ private final class MediaCutoutScreenComponent: Component { let labelSize = self.label.update( transition: transition, - component: AnyComponent(Text(text: "Tap an object to cut it out", font: Font.regular(17.0), color: .white)), + component: AnyComponent(Text(text: "Tap on an object to cut it out", font: Font.regular(17.0), color: .white)), environment: {}, containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0) ) @@ -229,7 +241,9 @@ private final class MediaCutoutScreenComponent: Component { transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) - transition.setFrame(view: self.separatedImageView, frame: previewContainerFrame) + for view in self.outlineViews { + transition.setFrame(view: view, frame: previewContainerFrame) + } let frameWidth = floor(previewContainerFrame.width * 0.97) @@ -237,20 +251,28 @@ private final class MediaCutoutScreenComponent: Component { self.fadeView.layer.cornerRadius = frameWidth / 8.0 if isFirstTime { - let _ = (component.mediaEditor.getSeparatedImage(point: nil) - |> deliverOnMainQueue).start(next: { [weak self] image in - guard let self else { - return - } - self.separatedImageView.image = image - self.state?.updated(transition: .easeInOut(duration: 0.2)) - }) - } else { - if let _ = self.separatedImageView.image { - transition.setAlpha(view: self.fadeView, alpha: 1.0) - } else { - transition.setAlpha(view: self.fadeView, alpha: 0.0) + let values = component.mediaEditor.values + component.mediaEditor.processImage { originalImage, editedImage in + cutoutImage(from: originalImage, editedImage: editedImage, values: values, target: .all, completion: { results in + Queue.mainQueue().async { + if !results.isEmpty { + for result in results { + if let extractedImage = result.extractedImage, let maskImage = result.maskImage { + if case let .image(image, _) = extractedImage, case let .image(_, mask) = maskImage { + let outlineView = StickerCutoutOutlineView(frame: self.previewContainerView.frame) + outlineView.update(image: image, maskImage: mask, size: self.previewContainerView.bounds.size, values: values) + self.insertSubview(outlineView, belowSubview: self.previewContainerView) + self.outlineViews.append(outlineView) + } + } + } + self.state?.updated(transition: .easeInOut(duration: 0.4)) + } + } + }) } + } else { + transition.setAlpha(view: self.fadeView, alpha: !self.outlineViews.isEmpty ? 1.0 : 0.0) } return availableSize } 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 cfd9e042df..de6ec17e31 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -41,6 +41,11 @@ import ForwardInfoPanelComponent import ContextReferenceButtonComponent import MediaScrubberComponent import BlurredBackgroundComponent +import WebPBinding +import StickerResources +import StickerPeekUI +import StickerPackEditTitleController +import StickerPickerScreen private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -2050,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 { @@ -2146,9 +2157,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let ciContext = CIContext(options: [.workingColorSpace : NSNull()]) - private let stickerPickerInputData = Promise() + private let stickerPickerInputData = Promise() - private var availableReactions: [ReactionItem] = [] + fileprivate var availableReactions: [ReactionItem] = [] private var availableReactionsDisposable: Disposable? private var panGestureRecognizer: UIPanGestureRecognizer? @@ -2207,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) } @@ -2312,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 { @@ -2491,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) } @@ -2626,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 @@ -4057,7 +4084,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate switch mode { case .sticker: self.mediaEditor?.maybePauseVideo() - let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji, hasGifs: true) + + var hasInteractiveStickers = true + if let controller = self.controller, case .stickerEditor = controller.mode { + hasInteractiveStickers = false + } + 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 { @@ -4227,7 +4259,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if self.entitiesView.hasSelection { self.entitiesView.selectEntity(nil) } - let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor, hiddenTools: !self.canEnhance ? [.enhance] : []) + var hiddenTools: [EditorToolKey] = [] + if !self.canEnhance { + hiddenTools.append(.enhance) + } + if let controller = self.controller, case .stickerEditor = controller.mode { + hiddenTools.append(.grain) + hiddenTools.append(.vignette) + } + let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor, hiddenTools: hiddenTools) controller.dismissed = { [weak self] in if let self { self.animateInFromTool() @@ -4237,8 +4277,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.animateOutToTool() } }, - openCutout: { [weak self] in - if let self, let mediaEditor = self.mediaEditor { + openCutout: { [weak self, weak controller] in + if let self, let controller, let mediaEditor = self.mediaEditor { if self.entitiesView.hasSelection { self.entitiesView.selectEntity(nil) } @@ -4251,17 +4291,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in snapshotView?.removeFromSuperview() }) - mediaEditor.removeSeparationMask() + mediaEditor.removeSegmentationMask() } else { - let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor, previewView: self.previewView) - controller.dismissed = { [weak self] in + let cutoutController = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor, previewView: self.previewView) + cutoutController.dismissed = { [weak self] in if let self { self.animateInFromTool(inPlace: true) } } - self.controller?.present(controller, in: .window(.root)) + controller.present(cutoutController, in: .window(.root)) self.animateOutToTool(inPlace: true) } + controller.hapticFeedback.impact(.medium) } } ) @@ -4411,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 { @@ -4422,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) } } @@ -4437,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) } } @@ -4456,6 +4502,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return draft.isVideo case .message: return false + case .sticker: + return false } } } @@ -4468,6 +4516,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } case image(image: UIImage, dimensions: PixelDimensions) case video(video: VideoResult, coverImage: UIImage?, values: MediaEditorValues, duration: Double, dimensions: PixelDimensions) + case sticker(file: TelegramMediaFile) } public struct Result { @@ -4477,6 +4526,31 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public let options: MediaEditorResultPrivacy public let stickers: [TelegramMediaFile] public let randomId: Int64 + + init() { + self.media = nil + self.mediaAreas = [] + self.caption = NSAttributedString() + self.options = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false) + self.stickers = [] + self.randomId = 0 + } + + init( + media: MediaResult?, + mediaAreas: [MediaArea], + caption: NSAttributedString, + options: MediaEditorResultPrivacy, + stickers: [TelegramMediaFile], + randomId: Int64 + ) { + self.media = media + self.mediaAreas = mediaAreas + self.caption = caption + self.options = options + self.stickers = stickers + self.randomId = randomId + } } let context: AccountContext @@ -4508,6 +4582,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var audioSessionDisposable: Disposable? private let postingAvailabilityPromise = Promise() private var postingAvailabilityDisposable: Disposable? + + fileprivate var myStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)] = [] + private var myStickerPacksDisposable: Disposable? public init( context: AccountContext, @@ -4581,6 +4658,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate updateStorySources(engine: self.context.engine) updateStoryDrafts(engine: self.context.engine) + + if case .stickerEditor = mode { + self.myStickerPacksDisposable = (self.context.engine.stickers.getMyStickerSets() + |> deliverOnMainQueue).start(next: { [weak self] packs in + guard let self else { + return + } + self.myStickerPacks = packs + }) + } } required public init(coder aDecoder: NSCoder) { @@ -4591,6 +4678,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.exportDisposable.dispose() self.audioSessionDisposable?.dispose() self.postingAvailabilityDisposable?.dispose() + self.myStickerPacksDisposable?.dispose() } fileprivate func setupAudioSessionIfNeeded() { @@ -5500,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) @@ -5567,22 +5667,20 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } func requestStickerCompletion(animated: Bool) { - guard let mediaEditor = self.node.mediaEditor, !self.didComplete else { + guard let mediaEditor = self.node.mediaEditor else { return } - - self.didComplete = true - + self.dismissAllTooltips() - mediaEditor.stop() - mediaEditor.invalidate() - self.node.entitiesView.invalidate() - if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } +// mediaEditor.stop() +// mediaEditor.invalidate() +// self.node.entitiesView.invalidate() + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) @@ -5592,7 +5690,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self, let resultImage { Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") - let scaledImage = generateImage(CGSize(width: 512, height: 512), contextGenerator: { size, context in + let dimensions = CGSize(width: 512, height: 512) + let scaledImage = generateImage(dimensions, contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.addPath(CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: size.width / 8.0, cornerHeight: size.width / 8.0, transform: nil)) @@ -5602,7 +5701,40 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate context.draw(resultImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize)) }, opaque: false, scale: 1.0)! - self.completion(MediaEditorScreen.Result(media: .image(image: scaledImage, dimensions: PixelDimensions(scaledImage.size)), 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.presentStickerPreview(image: scaledImage) + } + }) + } + } + + func presentStickerPreview(image: UIImage) { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + Queue.concurrentDefaultQueue().async { + if let data = try? WebP.convert(toWebP: image, quality: 97.0) { + 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)) + + 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 { @@ -5610,9 +5742,337 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) }) - } - }) + }))) + 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( + context: self.context, + theme: presentationData.theme, + strings: presentationData.strings, + item: .image(image), + isCreating: true, + menu: menuItems, + reactionItems: self.node.availableReactions, + openPremiumIntro: {} + ), + sourceView: { [weak self] in + if let self { + let size = CGSize(width: self.view.frame.width, height: self.view.frame.width) + return (self.view, CGRect(origin: CGPoint(x: (self.view.frame.width - size.width) / 2.0, y: (self.view.frame.height - size.height) / 2.0), size: size)) + } else { + return nil + } + }, + activateImmediately: true + ) + self.present(peekController, in: .window(.root)) + } + + private enum StickerAction { + case addToFavorites + case createStickerPack(title: String) + case addToStickerPack(pack: StickerPackReference, title: String) + case upload + } + + private func presentCreateStickerPack(file: TelegramMediaFile, completion: @escaping () -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + + var dismissImpl: (() -> Void)? + let controller = stickerPackEditTitleController(context: self.context, forceDark: true, title: "New Sticker Set", text: "Choose a name for your sticker set.", placeholder: presentationData.strings.ImportStickerPack_NamePlaceholder, actionTitle: presentationData.strings.Common_Done, value: nil, maxLength: 128, apply: { [weak self] title in + guard let self else { + return + } + dismissImpl?() + completion() + + self.updateEditProgress(0.0, cancel: { [weak self] in + self?.stickerUploadDisposable.set(nil) + }) + self.stickerUploadDisposable.set((self.context.engine.stickers.createStickerSet( + title: title ?? "", + shortName: "", + stickers: [ + ImportSticker( + resource: file.resource, + emojis: ["😀"], + dimensions: PixelDimensions(width: 512, height: 512), + mimeType: "image/webp", + keywords: "" + ) + ], + thumbnail: nil, + type: .stickers(content: .image), + software: nil + ) |> deliverOnMainQueue).startStandalone(next: { [weak self] status in + guard let self else { + return + } + switch status { + case let .progress(progress, _, _): + self.updateEditProgress(progress, cancel: { [weak self] in + self?.stickerUploadDisposable.set(nil) + }) + case let .complete(info, items): + self.completion(MediaEditorScreen.Result(), { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + guard let self else { + return + } + let navigationController = self.navigationController as? NavigationController + self.dismiss() + if let navigationController { + Queue.mainQueue().after(0.2) { + let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) + let controller = self.context.sharedContext.makeStickerPackScreen(context: self.context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [.result(info: info, items: items, installed: true)], isEditing: false, parentNavigationController: nil, sendSticker: nil) + (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) + } + } + }) + }) + } + })) + }, cancel: {}) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + self.present(controller, in: .window(.root)) + } + + private let stickerUploadDisposable = MetaDisposable() + private func uploadSticker(_ file: TelegramMediaFile, action: StickerAction) { + let context = self.context + let dimensions = PixelDimensions(width: 512, height: 512) + let mimeType = "image/webp" + + self.updateEditProgress(0.0, cancel: { [weak self] in + self?.stickerUploadDisposable.set(nil) + }) + + let signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> castError(UploadStickerError.self) + |> mapToSignal { peer -> Signal in + guard let peer else { + return .complete() + } + return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: file.resource, alt: "", dimensions: dimensions, mimeType: mimeType) + |> mapToSignal { status -> Signal in + switch status { + case .progress: + return .single(status) + case let .complete(resource, _): + let file = stickerFile(resource: resource, size: file.size ?? 0, dimensions: dimensions) + switch action { + case .addToFavorites: + return context.engine.stickers.toggleStickerSaved(file: file, saved: true) + |> `catch` { _ -> Signal in + return .fail(.generic) + } + |> map { _ in + return status + } + case let .createStickerPack(title): + let sticker = ImportSticker( + resource: resource, + emojis: ["😀"], + dimensions: dimensions, + mimeType: "image/webp", + keywords: "" + ) + return context.engine.stickers.createStickerSet(title: title, shortName: "", stickers: [sticker], thumbnail: nil, type: .stickers(content: .image), software: nil) + |> `catch` { _ -> Signal in + return .fail(.generic) + } + |> mapToSignal { innerStatus in + if case .complete = innerStatus { + return .single(status) + } else { + return .complete() + } + } + case let .addToStickerPack(pack, _): + let sticker = ImportSticker( + resource: resource, + emojis: ["😀"], + dimensions: dimensions, + mimeType: "image/webp", + keywords: "" + ) + return context.engine.stickers.addStickerToStickerSet(packReference: pack, sticker: sticker) + |> `catch` { _ -> Signal in + return .fail(.generic) + } + |> map { _ in + return status + } + case .upload: + return .single(status) + } + } + } + } + self.stickerUploadDisposable.set((signal + |> deliverOnMainQueue).startStandalone(next: { [weak self] status in + guard let self else { + return + } + + switch status { + case let .progress(progress): + self.updateEditProgress(progress, cancel: { [weak self] in + self?.stickerUploadDisposable.set(nil) + }) + case let .complete(resource, _): + let navigationController = self.navigationController as? NavigationController + + 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 + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.dismiss() + Queue.mainQueue().justDispatch { + finished() + + switch action { + case .addToFavorites: + if let parentController = navigationController?.viewControllers.last as? ViewController { + parentController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_StickerAddedToFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + } + case let .addToStickerPack(packReference, title): + let navigationController = self.navigationController as? NavigationController + 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) + (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) + + Queue.mainQueue().after(0.1) { + controller.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: "Sticker added to **\(title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + } + } + } + default: + break + } + } + }) + }) + } + })) } private var videoExport: MediaEditorVideoExport? @@ -5724,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 @@ -6441,3 +6906,12 @@ extension MediaScrubberComponent.Track { ) } } + +private func stickerFile(resource: TelegramMediaResource, size: Int64, dimensions: PixelDimensions) -> TelegramMediaFile { + var fileAttributes: [TelegramMediaFileAttribute] = [] + fileAttributes.append(.FileName(fileName: "sticker.webp")) + fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) + fileAttributes.append(.ImageSize(size: dimensions)) + + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: size, attributes: fileAttributes) +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index 11f3086dcb..4401869e98 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -665,7 +665,7 @@ private final class MediaToolsScreenComponent: Component { tools = tools.filter { !component.hiddenTools.contains($0.key) } - if !component.mediaEditor.sourceIsVideo { + if !component.mediaEditor.sourceIsVideo && !component.hiddenTools.contains(.grain) { tools.insert(AdjustmentTool( key: .grain, title: presentationData.strings.Story_Editor_Tool_Grain, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift new file mode 100644 index 0000000000..b0973cb1f9 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -0,0 +1,507 @@ +import Foundation +import UIKit +import Display +import CoreImage +import MediaEditor + +func createEmitterBehavior(type: String) -> NSObject { + let selector = ["behaviorWith", "Type:"].joined(separator: "") + let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type + let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))! + let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) + return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) +} + +private var previousBeginTime: Int = 3 + +final class StickerCutoutOutlineView: UIView { + let strokeLayer = SimpleShapeLayer() + let imageLayer = SimpleLayer() + var outlineLayer = CAEmitterLayer() + var glowLayer = CAEmitterLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.strokeLayer.fillColor = UIColor.clear.cgColor + self.strokeLayer.strokeColor = UIColor.clear.cgColor + self.strokeLayer.shadowColor = UIColor.white.cgColor + self.strokeLayer.shadowOpacity = 0.35 + self.strokeLayer.shadowRadius = 4.0 + + self.layer.allowsGroupOpacity = true + +// self.imageLayer.contentsGravity = .resizeAspect + + self.layer.addSublayer(self.strokeLayer) + self.layer.addSublayer(self.imageLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var hasContents: Bool { + self.imageLayer.contents != nil + } + + func update(image: UIImage, maskImage: CIImage, size: CGSize, values: MediaEditorValues) { + self.imageLayer.contents = image.cgImage + + if let path = getPathFromMaskImage(maskImage, size: size, values: values) { + self.strokeLayer.shadowPath = path.cgPath.expand(width: 1.5) + + self.setupAnimation(path: path) + } + } + + func setupAnimation(path: UIBezierPath) { + self.outlineLayer = CAEmitterLayer() + self.outlineLayer.opacity = 0.7 + self.glowLayer = CAEmitterLayer() + + self.layer.addSublayer(self.outlineLayer) + self.layer.addSublayer(self.glowLayer) + + let randomBeginTime = (previousBeginTime + 4) % 6 + previousBeginTime = randomBeginTime + + let outlineAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") + outlineAnimation.path = path.cgPath + outlineAnimation.duration = 5.0 + outlineAnimation.repeatCount = .infinity + outlineAnimation.calculationMode = .paced + outlineAnimation.beginTime = Double(randomBeginTime) + self.outlineLayer.add(outlineAnimation, forKey: "emitterPosition") + + let lineEmitterCell = CAEmitterCell() + lineEmitterCell.beginTime = CACurrentMediaTime() + let lineAlphaBehavior = createEmitterBehavior(type: "valueOverLife") + lineAlphaBehavior.setValue("color.alpha", forKey: "keyPath") + lineAlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values") + lineEmitterCell.setValue([lineAlphaBehavior], forKey: "emitterBehaviors") + lineEmitterCell.color = UIColor.white.cgColor + lineEmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage + lineEmitterCell.lifetime = 2.2 + lineEmitterCell.birthRate = 600 + lineEmitterCell.scale = 0.13 + lineEmitterCell.alphaSpeed = -0.4 + + self.outlineLayer.emitterCells = [lineEmitterCell] + self.outlineLayer.emitterMode = .points + self.outlineLayer.emitterSize = CGSize(width: 1.0, height: 1.0) + self.outlineLayer.emitterShape = .point + + let glowAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") + glowAnimation.path = path.cgPath + glowAnimation.duration = 5.0 + glowAnimation.repeatCount = .infinity + glowAnimation.calculationMode = .cubicPaced + glowAnimation.beginTime = Double(randomBeginTime) + self.glowLayer.add(glowAnimation, forKey: "emitterPosition") + + let glowEmitterCell = CAEmitterCell() + glowEmitterCell.beginTime = CACurrentMediaTime() + let glowAlphaBehavior = createEmitterBehavior(type: "valueOverLife") + glowAlphaBehavior.setValue("color.alpha", forKey: "keyPath") + glowAlphaBehavior.setValue([0.0, 0.32, 0.4, 0.2, 0.0], forKey: "values") + glowEmitterCell.setValue([glowAlphaBehavior], forKey: "emitterBehaviors") + glowEmitterCell.color = UIColor.white.cgColor + glowEmitterCell.contents = UIImage(named: "Media Editor/ParticleGlow")?.cgImage + glowEmitterCell.lifetime = 2.0 + glowEmitterCell.birthRate = 30 + glowEmitterCell.scale = 1.9 + glowEmitterCell.alphaSpeed = -0.1 + + self.glowLayer.emitterCells = [glowEmitterCell] + self.glowLayer.emitterMode = .points + self.glowLayer.emitterSize = CGSize(width: 1.0, height: 1.0) + self.glowLayer.emitterShape = .point + + self.strokeLayer.animateAlpha(from: 0.0, to: CGFloat(self.strokeLayer.opacity), duration: 0.4) + + self.outlineLayer.animateAlpha(from: 0.0, to: CGFloat(self.outlineLayer.opacity), duration: 0.4, delay: 0.0) + self.glowLayer.animateAlpha(from: 0.0, to: CGFloat(self.glowLayer.opacity), duration: 0.4, delay: 0.0) + + let values = [1.0, 1.07, 1.0] + let keyTimes = [0.0, 0.67, 1.0] + self.imageLayer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.4, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + override func layoutSubviews() { + self.strokeLayer.frame = self.bounds.offsetBy(dx: 0.0, dy: 1.0) + self.outlineLayer.frame = self.bounds + self.imageLayer.frame = self.bounds + self.glowLayer.frame = self.bounds + } +} + +private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaEditorValues) -> UIBezierPath? { + let edges = image.applyingFilter("CILineOverlay", parameters: ["inputEdgeIntensity": 0.1]) + + guard let pixelBuffer = getEdgesBitmap(edges) else { + return nil + } + let minSide = min(size.width, size.height) + let scaledImageSize = image.extent.size.aspectFilled(CGSize(width: minSide, height: minSide)) + let positionOffset = CGPoint( + x: (size.width - scaledImageSize.width) / 2.0, + y: (size.height - scaledImageSize.height) / 2.0 + ) + + var contour = findContours(pixelBuffer: pixelBuffer) + contour = simplify(contour, tolerance: 1.4) + let path = UIBezierPath(points: contour, close: true) + + let firstScale = min(size.width, size.height) / 256.0 + let secondScale = size.width / 1080.0 + + var transform = CGAffineTransform.identity + let position = values.cropOffset + let rotation = values.cropRotation + let scale = values.cropScale + + transform = transform.translatedBy(x: positionOffset.x + position.x * secondScale, y: positionOffset.y + position.y * secondScale) + transform = transform.rotated(by: rotation) + transform = transform.scaledBy(x: scale * firstScale, y: scale * firstScale) + + if !path.isEmpty { + path.apply(transform) + return path + } + return nil +} + +private func findContours(pixelBuffer: CVPixelBuffer) -> [CGPoint] { + struct Point: Hashable { + let x: Int + let y: Int + + var cgPoint: CGPoint { + return CGPoint(x: x, y: y) + } + } + + var contours = [[Point]]() + + CVPixelBufferLockBaseAddress(pixelBuffer, []) + defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, []) } + + let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) + + var visited: [Point: Bool] = [:] + func markVisited(_ point: Point) { + visited[point] = true + } + + func getPixelIntensity(_ point: Point) -> UInt8 { + let pixelOffset = point.y * bytesPerRow + point.x + let pixelPtr = baseAddress?.advanced(by: pixelOffset) + return pixelPtr?.load(as: UInt8.self) ?? 0 + } + + func isBlackPixel(_ point: Point) -> Bool { + if point.x >= 0 && point.x < width && point.y >= 0 && point.y < height { + let value = getPixelIntensity(point) + return value < 220 + } else { + return false + } + } + + func traceContour(startPoint: Point) -> [Point] { + var contour = [startPoint] + var currentPoint = startPoint + var previousDirection = 7 + + let dx = [1, 1, 0, -1, -1, -1, 0, 1] + let dy = [0, 1, 1, 1, 0, -1, -1, -1] + + repeat { + var found = false + for i in 0 ..< 8 { + let direction = (previousDirection + i) % 8 + let newX = currentPoint.x + dx[direction] + let newY = currentPoint.y + dy[direction] + let newPoint = Point(x: newX, y: newY) + + if isBlackPixel(newPoint) && !(visited[newPoint] == true) { + contour.append(newPoint) + previousDirection = (direction + 5) % 8 + currentPoint = newPoint + found = true + markVisited(newPoint) + break + } + } + if !found { + break + } + } while currentPoint != startPoint + + return contour + } + + for y in 0 ..< height { + for x in 0 ..< width { + let point = Point(x: x, y: y) + if visited[point] == true { + continue + } + if isBlackPixel(point) { + let contour = traceContour(startPoint: point) + if contour.count > 25 { + contours.append(contour) + } + } + } + } + + return (contours.sorted(by: { lhs, rhs in lhs.count > rhs.count }).first ?? []).map { $0.cgPoint } +} + +private func getEdgesBitmap(_ ciImage: CIImage) -> CVPixelBuffer? { + let context = CIContext(options: nil) + guard let contourCgImage = context.createCGImage(ciImage, from: ciImage.extent) else { + return nil + } + let image = UIImage(cgImage: contourCgImage) + + let size = image.size.aspectFilled(CGSize(width: 256, height: 256)) + let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, + kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary + var pixelBuffer: CVPixelBuffer? + let status = CVPixelBufferCreate(kCFAllocatorDefault, + Int(size.width), + Int(size.height), + kCVPixelFormatType_OneComponent8, + attrs, + &pixelBuffer) + guard status == kCVReturnSuccess, let buffer = pixelBuffer else { + return nil + } + + CVPixelBufferLockBaseAddress(buffer, []) + defer { CVPixelBufferUnlockBaseAddress(buffer, []) } + + let pixelData = CVPixelBufferGetBaseAddress(buffer) + let rgbColorSpace = CGColorSpaceCreateDeviceGray() + guard let context = CGContext(data: pixelData, + width: Int(size.width), + height: Int(size.height), + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), + space: rgbColorSpace, + bitmapInfo: 0) else { + return nil + } + + context.translateBy(x: 0, y: size.height) + context.scaleBy(x: 1.0, y: -1.0) + + UIGraphicsPushContext(context) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + image.draw(in: CGRect(origin: .zero, size: size)) + UIGraphicsPopContext() + + return buffer +} + +private extension CGPath { + func expand(width: CGFloat) -> CGPath { + let expandedPath = self.copy(strokingWithWidth: width * 2.0, lineCap: .round, lineJoin: .round, miterLimit: 0.0) + + class UserInfo { + let outputPath = CGMutablePath() + var passedFirst = false + } + var userInfo = UserInfo() + + withUnsafeMutablePointer(to: &userInfo) { userInfoPointer in + expandedPath.apply(info: userInfoPointer) { (userInfo, nextElementPointer) in + let element = nextElementPointer.pointee + let userInfoPointer = userInfo!.assumingMemoryBound(to: UserInfo.self) + let userInfo = userInfoPointer.pointee + + if !userInfo.passedFirst { + if case .closeSubpath = element.type { + userInfo.passedFirst = true + } + } else { + switch element.type { + case .moveToPoint: + userInfo.outputPath.move(to: element.points[0]) + case .addLineToPoint: + userInfo.outputPath.addLine(to: element.points[0]) + case .addQuadCurveToPoint: + userInfo.outputPath.addQuadCurve(to: element.points[1], control: element.points[0]) + case .addCurveToPoint: + userInfo.outputPath.addCurve(to: element.points[2], control1: element.points[0], control2: element.points[1]) + case .closeSubpath: + userInfo.outputPath.closeSubpath() + @unknown default: + userInfo.outputPath.closeSubpath() + } + } + } + } + return userInfo.outputPath + } +} + +private func simplify(_ points: [CGPoint], tolerance: CGFloat?) -> [CGPoint] { + guard points.count > 1 else { + return points + } + + let sqTolerance = tolerance != nil ? (tolerance! * tolerance!) : 1.0 + var result = simplifyRadialDistance(points, tolerance: sqTolerance) + result = simplifyDouglasPeucker(result, sqTolerance: sqTolerance) + + return result +} + +private func simplifyRadialDistance(_ points: [CGPoint], tolerance: CGFloat) -> [CGPoint] { + guard points.count > 2 else { + return points + } + + var prevPoint = points.first! + var newPoints = [prevPoint] + var currentPoint: CGPoint! + + for i in 1.. tolerance { + newPoints.append(currentPoint) + prevPoint = currentPoint + } + } + + if prevPoint.equalsTo(currentPoint) == false { + newPoints.append(currentPoint) + } + + return newPoints +} + +private func simplifyDPStep(_ points: [CGPoint], first: Int, last: Int, sqTolerance: CGFloat, simplified: inout [CGPoint]) { + guard last > first else { + return + } + var maxSqDistance = sqTolerance + var index = 0 + + for currentIndex in first+1.. maxSqDistance { + maxSqDistance = sqDistance + index = currentIndex + } + } + + if maxSqDistance > sqTolerance { + if (index - first) > 1 { + simplifyDPStep(points, first: first, last: index, sqTolerance: sqTolerance, simplified: &simplified) + } + simplified.append(points[index]) + if (last - index) > 1 { + simplifyDPStep(points, first: index, last: last, sqTolerance: sqTolerance, simplified: &simplified) + } + } +} + +private func simplifyDouglasPeucker(_ points: [CGPoint], sqTolerance: CGFloat) -> [CGPoint] { + guard points.count > 1 else { + return [] + } + + let last = (points.count - 1) + var simplied = [points.first!] + simplifyDPStep(points, first: 0, last: last, sqTolerance: sqTolerance, simplified: &simplied) + simplied.append(points.last!) + + return simplied +} + +private extension CGPoint { + func equalsTo(_ compare: CGPoint) -> Bool { + return self.x == compare.self.x && self.y == compare.y + } + + func distanceFrom(_ otherPoint: CGPoint) -> CGFloat { + let dx = self.x - otherPoint.x + let dy = self.y - otherPoint.y + return (dx * dx) + (dy * dy) + } + + func distanceToSegment(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat { + var x = p1.x + var y = p1.y + var dx = p2.x - x + var dy = p2.y - y + + if dx != 0 || dy != 0 { + let t = ((self.x - x) * dx + (self.y - y) * dy) / (dx * dx + dy * dy) + if t > 1 { + x = p2.x + y = p2.y + } else if t > 0 { + x += dx * t + y += dy * t + } + } + + dx = self.x - x + dy = self.y - y + + return dx * dx + dy * dy + } +} + +fileprivate extension Array { + subscript(circularIndex index: Int) -> Element { + get { + assert(self.count > 0) + let index = (index + self.count) % self.count + return self[index] + } + set { + assert(self.count > 0) + let index = (index + self.count) % self.count + return self[index] = newValue + } + } + func circularIndex(_ index: Int) -> Int { + return (index + self.count) % self.count + } +} +extension UIBezierPath { + convenience init(points: [CGPoint], close: Bool) { + self.init() + let K: CGFloat = 0.2 + var c1 = [Int: CGPoint]() + var c2 = [Int: CGPoint]() + let count = close ? points.count + 1 : points.count - 1 + for index in 1 ..< count { + let p = points[circularIndex: index] + let vP1 = points[circularIndex: index + 1] + let vP2 = points[index - 1] + let vP = CGPoint(x: vP1.x - vP2.x, y: vP1.y - vP2.y) + let v = CGPoint(x: vP.x * K, y: vP.y * K) + c2[(index + points.count - 1) % points.count] = CGPoint(x: p.x - v.x, y: p.y - v.y) //(p - v) + c1[(index + points.count) % points.count] = CGPoint(x: p.x + v.x, y: p.y + v.y) //(p + v) + } + self.move(to: points[0]) + for index in 0 ..< points.count - 1 { + let c1 = c1[index] ?? points[points.circularIndex(index)] + let c2 = c2[index] ?? points[points.circularIndex(index + 1)] + self.addCurve(to: points[circularIndex: index + 1], controlPoint1: c1, controlPoint2: c2) + } + self.close() + } +} diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 031539d733..b5b550cda8 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -473,6 +473,8 @@ final class PeerAllowedReactionsScreenComponent: Component { }, clearGroup: { _ in }, + editAction: { _ in + }, pushController: { c in }, presentController: { c in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 9b1fd4efd9..becde7514e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -140,6 +140,7 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/TextLoadingEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index 8093277652..0929d6f58f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -7,6 +7,8 @@ import UIKit import AppBundle import TelegramStringFormatting import ContextUI +import SwiftSignalKit +import TextLoadingEffect enum PeerInfoScreenLabeledValueTextColor { case primary @@ -22,6 +24,23 @@ enum PeerInfoScreenLabeledValueIcon { case qrCode } +private struct TextLinkItemSource: Equatable { + enum Target { + case primary + case additional + } + + let item: TextLinkItem + let target: Target + let range: NSRange? + + init(item: TextLinkItem, target: Target, range: NSRange?) { + self.item = item + self.target = target + self.range = range + } +} + final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let id: AnyHashable let label: String @@ -30,9 +49,9 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let textColor: PeerInfoScreenLabeledValueTextColor let textBehavior: PeerInfoScreenLabeledValueTextBehavior let icon: PeerInfoScreenLabeledValueIcon? - let action: ((ASDisplayNode) -> Void)? + let action: ((ASDisplayNode, Promise?) -> Void)? let longTapAction: ((ASDisplayNode) -> Void)? - let linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)? + let linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void)? let iconAction: (() -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? let requestLayout: () -> Void @@ -45,9 +64,9 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { textColor: PeerInfoScreenLabeledValueTextColor = .primary, textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, icon: PeerInfoScreenLabeledValueIcon? = nil, - action: ((ASDisplayNode) -> Void)?, + action: ((ASDisplayNode, Promise?) -> Void)?, longTapAction: ((ASDisplayNode) -> Void)? = nil, - linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)? = nil, + linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void)? = nil, iconAction: (() -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, requestLayout: @escaping () -> Void @@ -116,9 +135,14 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { private let activateArea: AccessibilityAreaNode + private var validLayout: (width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool)? private var item: PeerInfoScreenLabeledValueItem? private var theme: PresentationTheme? + private var linkProgressView: TextLoadingEffectView? + private var linkItemWithProgress: TextLinkItemSource? + private var linkItemProgressDisposable: Disposable? + private var isExpanded: Bool = false override init() { @@ -259,6 +283,10 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } } + deinit { + self.linkItemProgressDisposable?.dispose() + } + @objc private func expandPressed() { self.isExpanded = true self.item?.requestLayout() @@ -313,11 +341,57 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { case .tap, .longTap: if let item = self.item { if let linkItem = self.linkItemAtPoint(location) { - item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem, self.linkHighlightingNode ?? self, self.linkHighlightingNode?.rects.first) + self.linkItemProgressDisposable?.dispose() + let progressValue = Promise(false) + self.linkItemProgressDisposable = (progressValue.get() + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + var currentLinkItem: TextLinkItemSource? + if value { + currentLinkItem = linkItem + } + if self.linkItemWithProgress != currentLinkItem { + self.linkItemWithProgress = currentLinkItem + + if let validLayout = self.validLayout { + let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) + } + } + }) + + item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem.item, self.linkHighlightingNode ?? self, self.linkHighlightingNode?.rects.first, progressValue) } else if case .longTap = gesture { item.longTapAction?(self) } else if case .tap = gesture { - item.action?(self.contextSourceNode) + var linkItem: TextLinkItemSource? + if let attributedText = self.textNode.attributedText { + linkItem = TextLinkItemSource(item: .url(url: "", concealed: false), target: .primary, range: NSRange(location: 0, length: attributedText.length)) + } + self.linkItemProgressDisposable?.dispose() + let progressValue = Promise(false) + self.linkItemProgressDisposable = (progressValue.get() + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + var currentLinkItem: TextLinkItemSource? + if value { + currentLinkItem = linkItem + } + if self.linkItemWithProgress != currentLinkItem { + self.linkItemWithProgress = currentLinkItem + + if let validLayout = self.validLayout { + let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) + } + } + }) + + item.action?(self.contextSourceNode, progressValue) } } default: @@ -334,13 +408,15 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { return 10.0 } + self.validLayout = (width, safeInsets, presentationData, item, topItem, bottomItem, hasCorners) + self.item = item self.theme = presentationData.theme if let action = item.action { self.selectionNode.pressed = { [weak self] in if let strongSelf = self { - action(strongSelf.contextSourceNode) + action(strongSelf.contextSourceNode, nil) } } } else { @@ -553,30 +629,100 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } self.contextSourceNode.contentRect = extractedRect + if let linkItemWithProgress = self.linkItemWithProgress, let range = linkItemWithProgress.range { + let linkProgressView: TextLoadingEffectView + if let current = self.linkProgressView { + linkProgressView = current + } else { + linkProgressView = TextLoadingEffectView(frame: CGRect()) + self.linkProgressView = linkProgressView + self.contextSourceNode.contentNode.view.addSubview(linkProgressView) + } + + let progressColor: UIColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1) + + let targetTextNode: TextNode + switch linkItemWithProgress.target { + case .primary: + targetTextNode = self.textNode + case .additional: + targetTextNode = self.additionalTextNode + } + linkProgressView.frame = targetTextNode.frame + linkProgressView.update(color: progressColor, textNode: targetTextNode, range: range) + } else { + if let linkProgressView = self.linkProgressView { + self.linkProgressView = nil + linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in + linkProgressView?.removeFromSuperview() + }) + } + } + return height } - private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { + private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItemSource? { let textNodeFrame = self.textNode.frame - if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + var item: TextLinkItem? + var urlRange: NSRange? if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - return .url(url: url, concealed: false) + item = .url(url: url, concealed: false) + + if let (_, _, urlRangeValue) = self.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.URL, index: index) { + urlRange = urlRangeValue + } } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .mention(peerName) + item = .mention(peerName) + + if let (_, _, urlRangeValue) = self.textNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention).rawValue, index: index) { + urlRange = urlRangeValue + } } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + item = .hashtag(hashtag.peerName, hashtag.hashtag) + + if let (_, _, urlRangeValue) = self.textNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag).rawValue, index: index) { + urlRange = urlRangeValue + } + } else { + item = nil + } + if let item { + return TextLinkItemSource(item: item, target: .primary, range: urlRange) } else { return nil } } let additionalTextNodeFrame = self.additionalTextNode.frame - if let (_, attributes) = self.additionalTextNode.attributesAtPoint(CGPoint(x: point.x - additionalTextNodeFrame.minX, y: point.y - additionalTextNodeFrame.minY)) { + if let (index, attributes) = self.additionalTextNode.attributesAtPoint(CGPoint(x: point.x - additionalTextNodeFrame.minX, y: point.y - additionalTextNodeFrame.minY)) { + var item: TextLinkItem? + var urlRange: NSRange? + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - return .url(url: url, concealed: false) + item = .url(url: url, concealed: false) + + if let (_, _, urlRangeValue) = self.additionalTextNode.attributeSubstringWithRange(name: TelegramTextAttributes.URL, index: index) { + urlRange = urlRangeValue + } } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .mention(peerName) + item = .mention(peerName) + + if let (_, _, urlRangeValue) = self.additionalTextNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention).rawValue, index: index) { + urlRange = urlRangeValue + } } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + item = .hashtag(hashtag.peerName, hashtag.hashtag) + + if let (_, _, urlRangeValue) = self.additionalTextNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag).rawValue, index: index) { + urlRange = urlRangeValue + } + } else { + item = nil + } + + if let item { + return TextLinkItemSource(item: item, target: .additional, range: urlRange) } else { return nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 74439e4e9f..62f73db0c4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -531,7 +531,7 @@ private enum TopicsLimitedReason { private final class PeerInfoInteraction { let openChat: () -> Void - let openUsername: (String) -> Void + let openUsername: (String, Bool, Promise?) -> Void let openPhone: (String, ASDisplayNode, ContextGesture?) -> Void let editingOpenNotificationSettings: () -> Void let editingOpenSoundSettings: () -> Void @@ -585,7 +585,7 @@ private final class PeerInfoInteraction { let openEditing: () -> Void init( - openUsername: @escaping (String) -> Void, + openUsername: @escaping (String, Bool, Promise?) -> Void, openPhone: @escaping (String, ASDisplayNode, ContextGesture?) -> Void, editingOpenNotificationSettings: @escaping () -> Void, editingOpenSoundSettings: @escaping () -> Void, @@ -1092,7 +1092,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let bioContextAction: (ASDisplayNode) -> Void = { sourceNode in interaction.openPeerInfoContextMenu(.bio, sourceNode, nil) } - let bioLinkAction: (TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void = { action, item, _, _ in + let bioLinkAction: (TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void = { action, item, _, _, _ in interaction.performBioLinkAction(action, item) } @@ -1109,7 +1109,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { label = presentationData.strings.ContactInfo_PhoneLabelMobile } - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node in + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node, _ in interaction.openPhone(phone, node, nil) }, longTapAction: nil, contextAction: { node, gesture, _ in interaction.openPhone(phone, node, gesture) @@ -1132,14 +1132,14 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese additionalText: additionalUsernames, textColor: .accent, icon: .qrCode, - action: { _ in - interaction.openUsername(mainUsername) + action: { _, progress in + interaction.openUsername(mainUsername, true, progress) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil) - }, linkItemAction: { type, item, _, _ in + }, linkItemAction: { type, item, _, _, progress in if case .tap = type { if case let .mention(username) = item { - interaction.openUsername(String(username[username.index(username.startIndex, offsetBy: 1)...])) + interaction.openUsername(String(username[username.index(username.startIndex, offsetBy: 1)...]), false, progress) } } }, iconAction: { @@ -1307,14 +1307,14 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese text: linkText, textColor: .accent, icon: .qrCode, - action: { _ in - interaction.openUsername(linkText) + action: { _, progress in + interaction.openUsername(linkText, true, progress) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link(customLink: linkText), sourceNode, nil) - }, linkItemAction: { type, item, _, _ in + }, linkItemAction: { type, item, _, _, progress in if case .tap = type { if case let .mention(username) = item { - interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1)))) + interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1))), false, progress) } } }, iconAction: { @@ -1360,14 +1360,14 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese additionalText: additionalUsernames, textColor: .accent, icon: .qrCode, - action: { _ in - interaction.openUsername(mainUsername) + action: { _, progress in + interaction.openUsername(mainUsername, true, progress) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil) - }, linkItemAction: { type, item, sourceNode, sourceRect in + }, linkItemAction: { type, item, sourceNode, sourceRect, progress in if case .tap = type { if case let .mention(username) = item { - interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1)))) + interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1))), false, progress) } } else if case .longTap = type { if case let .mention(username) = item { @@ -2393,8 +2393,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.paneContainerNode.parentController = controller self._interaction = PeerInfoInteraction( - openUsername: { [weak self] value in - self?.openUsername(value: value) + openUsername: { [weak self] value, isMainUsername, progress in + self?.openUsername(value: value, isMainUsername: isMainUsername, progress: progress) }, openPhone: { [weak self] value, node, gesture in self?.openPhone(value: value, node: node, gesture: gesture) @@ -6416,7 +6416,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro contextController.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) } - private func openUsername(value: String) { + private func openUsername(value: String, isMainUsername: Bool, progress: Promise?) { let url: String if value.hasPrefix("https://") { url = value @@ -6424,70 +6424,101 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro url = "https://t.me/\(value)" } - let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData) - shareController.completed = { [weak self] peerIds in - guard let strongSelf = self else { + let openShare: (TelegramCollectibleItemInfo?) -> Void = { [weak self] collectibleItemInfo in + guard let self else { return } - let _ = (strongSelf.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData, collectibleItemInfo: collectibleItemInfo) + shareController.completed = { [weak self] peerIds in guard let strongSelf = self else { return } - - let peers = peerList.compactMap { $0 } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { - text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string - } else { - text = "" + let _ = (strongSelf.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + guard let strongSelf = self else { + return } + + let peers = peerList.compactMap { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + }) + } + shareController.actionCompleted = { [weak self] in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + } + self.view.endEditing(true) + self.controller?.present(shareController, in: .window(.root)) + } + + if let pathComponents = URL(string: url)?.pathComponents, pathComponents.count >= 2, !pathComponents[1].isEmpty { + let namePart = pathComponents[1] + progress?.set(.single(true)) + let _ = (self.context.sharedContext.makeCollectibleItemInfoScreenInitialData(context: self.context, peerId: self.peerId, subject: .username(namePart)) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self else { + return } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.controller?.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) + progress?.set(.single(false)) + + if let initialData { + if isMainUsername { + openShare(initialData.collectibleItemInfo) + } else { + self.view.endEditing(true) + self.controller?.push(self.context.sharedContext.makeCollectibleItemInfoScreen(context: self.context, initialData: initialData)) } - return false - }), in: .current) + } else { + openShare(nil) + } }) + } else { + openShare(nil) } - shareController.actionCompleted = { [weak self] in - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - } - self.view.endEditing(true) - self.controller?.present(shareController, in: .window(.root)) } private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) { @@ -6697,10 +6728,27 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } var actions = ContextController.Items(content: .list(items)) if isAnonymousNumber && !accountIsFromUS { + let collectibleInfo = Promise() + collectibleInfo.set(strongSelf.context.sharedContext.makeCollectibleItemInfoScreenInitialData(context: strongSelf.context, peerId: strongSelf.peerId, subject: .phoneNumber(value))) + actions.tip = .animatedEmoji(text: strongSelf.presentationData.strings.UserInfo_AnonymousNumberInfo, arguments: nil, file: nil, action: { [weak self] in - if let strongSelf = self { - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: "https://fragment.com/numbers", forceExternal: true, presentationData: strongSelf.presentationData, navigationController: nil, dismissInput: {}) + guard let self else { + return } + + let _ = (collectibleInfo.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self else { + return + } + if let initialData { + self.view.endEditing(true) + self.controller?.push(self.context.sharedContext.makeCollectibleItemInfoScreen(context: self.context, initialData: initialData)) + } else { + self.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: "https://fragment.com/numbers", forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: {}) + } + }) }) } let contextController = ContextController(presentationData: strongSelf.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) @@ -8744,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/Settings/CollectibleItemInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD new file mode 100644 index 0000000000..7a12d1fc72 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "CollectibleItemInfoScreen", + module_name = "CollectibleItemInfoScreen", + 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/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/Components/SheetComponent", + "//submodules/PresentationDataUtils", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Markdown", + "//submodules/TelegramStringFormatting", + "//submodules/AvatarNode", + "//submodules/PhoneNumberFormat", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift new file mode 100644 index 0000000000..df6c8248b0 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift @@ -0,0 +1,772 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import SheetComponent +import ButtonComponent +import PlainButtonComponent +import TelegramCore +import SwiftSignalKit +import MultilineTextComponent +import BalancedTextComponent +import TelegramStringFormatting +import AvatarNode +import TelegramPresentationData +import PhoneNumberFormat +import BundleIconComponent + +private final class PeerBadgeComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + } + + static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let background = ComponentView() + private let title = ComponentView() + private var avatarNode: AvatarNode? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let height: CGFloat = 32.0 + let avatarPadding: CGFloat = 1.0 + + let avatarDiameter = height - avatarPadding * 2.0 + let avatarTextSpacing: CGFloat = 4.0 + let rightTextInset: CGFloat = 12.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarPadding - avatarDiameter - avatarTextSpacing - rightTextInset, height: height) + ) + let titleFrame = CGRect(origin: CGPoint(x: avatarPadding + avatarDiameter + avatarTextSpacing, y: floorToScreenPixels((height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + let avatarFrame = CGRect(origin: CGPoint(x: avatarPadding, y: avatarPadding), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + avatarNode.frame = avatarFrame + avatarNode.updateSize(size: avatarFrame.size) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + + let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + titleSize.width + rightTextInset, height: height) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(RoundedRectangle(color: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: nil)), + environment: {}, + containerSize: size + ) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), 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) + } +} + +private final class CollectibleItemInfoScreenContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: CollectibleItemInfoScreen.InitialData + let dismiss: () -> Void + + init( + context: AccountContext, + initialData: CollectibleItemInfoScreen.InitialData, + dismiss: @escaping () -> Void + ) { + self.context = context + self.initialData = initialData + self.dismiss = dismiss + } + + static func ==(lhs: CollectibleItemInfoScreenContentComponent, rhs: CollectibleItemInfoScreenContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class View: UIView { + private let iconBackground = ComponentView() + private let icon = ComponentView() + private let title = ComponentView() + private let peerBadge = ComponentView() + private let text = ComponentView() + private let button = ComponentView() + private let copyButton = ComponentView() + + private var component: CollectibleItemInfoScreenContentComponent? + + private var currencySymbolIcon: UIImage? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[EnvironmentType.self].value + + let sideInset: CGFloat = 16.0 + let contentSideInset: CGFloat = sideInset + 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += 30.0 + + let iconBackgroundSize = self.iconBackground.update( + transition: transition, + component: AnyComponent(RoundedRectangle(color: environment.theme.list.itemCheckColors.fillColor, cornerRadius: nil)), + environment: {}, + containerSize: CGSize(width: 90.0, height: 90.0) + ) + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconBackgroundSize.width) * 0.5), y: contentHeight), size: iconBackgroundSize) + if let iconBackgroundView = self.iconBackground.view { + if iconBackgroundView.superview == nil { + self.addSubview(iconBackgroundView) + } + transition.setFrame(view: iconBackgroundView, frame: iconBackgroundFrame) + } + contentHeight += iconBackgroundSize.height + contentHeight += 16.0 + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Peer Info/CollectibleUsernameInfoTitleIcon", + tintColor: environment.theme.list.itemCheckColors.foregroundColor + )), + environment: {}, + containerSize: iconBackgroundFrame.size + ) + let iconFrame = CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - iconSize.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + + let titleText = NSMutableAttributedString() + let textText = NSMutableAttributedString() + switch component.initialData.subject { + case let .username(username): + let rawTitleString = environment.strings.CollectibleItemInfo_UsernameTitle("@\(username.username)") + titleText.append(NSAttributedString(string: rawTitleString.string, font: Font.semibold(16.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTitleString.ranges { + titleText.addAttributes([ + .foregroundColor: environment.theme.list.itemAccentColor, + NSAttributedString.Key(rawValue: "URL"): "" + ], range: range.range) + } + + let dateText = stringForDate(timestamp: username.info.purchaseDate, strings: environment.strings) + + let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(username.info.cryptoCurrencyAmount, currency: username.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( + symbol: "~", + thousandsSeparator: ",", + decimalSeparator: ".", + symbolOnLeft: true, + spaceBetweenAmountAndSymbol: false, + decimalDigits: 9 + )) + var cryptoCurrencyText = rawCryptoCurrencyText + while cryptoCurrencyText.hasSuffix("0") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + if cryptoCurrencyText.hasSuffix(".") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + + let (currencyText, currencySign, _) = formatCurrencyAmountCustom(username.info.currencyAmount, currency: username.info.currency) + + let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTextString.ranges { + switch range.index { + case 0: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 1: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 3: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + default: + break + } + } + case let .phoneNumber(phoneNumber): + let formattedPhoneNumber = formatPhoneNumber(context: component.context, number: phoneNumber.phoneNumber) + + let rawTitleString = environment.strings.CollectibleItemInfo_PhoneTitle("\(formattedPhoneNumber)") + titleText.append(NSAttributedString(string: rawTitleString.string, font: Font.semibold(16.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTitleString.ranges { + titleText.addAttributes([ + .foregroundColor: environment.theme.list.itemAccentColor, + NSAttributedString.Key(rawValue: "URL"): "" + ], range: range.range) + } + + let dateText = stringForDate(timestamp: phoneNumber.info.purchaseDate, strings: environment.strings) + + let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.cryptoCurrencyAmount, currency: phoneNumber.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( + symbol: "~", + thousandsSeparator: ",", + decimalSeparator: ".", + symbolOnLeft: true, + spaceBetweenAmountAndSymbol: false, + decimalDigits: 9 + )) + var cryptoCurrencyText = rawCryptoCurrencyText + while cryptoCurrencyText.hasSuffix("0") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + if cryptoCurrencyText.hasSuffix(".") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + + let (currencyText, currencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.currencyAmount, currency: phoneNumber.info.currency) + + let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTextString.ranges { + switch range.index { + case 0: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 1: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 3: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + default: + break + } + } + } + + let currencySymbolRange = (textText.string as NSString).range(of: "~") + + if self.currencySymbolIcon == nil { + if let templateImage = UIImage(bundleImageName: "Peer Info/CollectibleTonSymbolInline") { + self.currencySymbolIcon = generateImage(CGSize(width: templateImage.size.width, height: templateImage.size.height + 2.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let cgImage = templateImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: templateImage.size)) + } + })?.withRenderingMode(.alwaysTemplate) + } + } + + if currencySymbolRange.location != NSNotFound, let currencySymbolIcon = self.currencySymbolIcon { + textText.replaceCharacters(in: currencySymbolRange, with: "$") + textText.addAttribute(.attachment, value: currencySymbolIcon, range: currencySymbolRange) + + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + let font = Font.semibold(15.0) + let runDelegateData = RunDelegateData( + ascent: font.ascender, + descent: font.descender, + width: currencySymbolIcon.size.width + 4.0 + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + textText.addAttribute(NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String), value: runDelegate, range: currencySymbolRange) + } + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(titleText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.185 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - contentSideInset * 2.0, height: 1000.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + contentHeight += titleSize.height + contentHeight += 7.0 + + if let peer = component.initialData.peer { + let peerBadgeSize = self.peerBadge.update( + transition: transition, + component: AnyComponent(PeerBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: peer + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - contentSideInset * 2.0, height: 1000.0) + ) + let peerBadgeFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - peerBadgeSize.width) * 0.5), y: contentHeight), size: peerBadgeSize) + if let peerBadgeView = self.peerBadge.view { + if peerBadgeView.superview == nil { + self.addSubview(peerBadgeView) + } + transition.setFrame(view: peerBadgeView, frame: peerBadgeFrame) + } + contentHeight += peerBadgeSize.height + contentHeight += 23.0 + } + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(textText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.185 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - contentSideInset * 2.0, height: 1000.0) + ) + let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentHeight), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + transition.setPosition(view: textView, position: textFrame.center) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + contentHeight += textSize.height + contentHeight += 21.0 + + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: environment.strings.CollectibleItemInfo_ButtonOpenInfo, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + + switch component.initialData.subject { + case let .username(username): + component.context.sharedContext.applicationBindings.openUrl(username.info.url) + case let .phoneNumber(phoneNumber): + component.context.sharedContext.applicationBindings.openUrl(phoneNumber.info.url) + } + + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + contentHeight += 5.0 + + let copyButtonTitle: String + switch component.initialData.subject { + case .username: + copyButtonTitle = environment.strings.CollectibleItemInfo_ButtonCopyUsername + case .phoneNumber: + copyButtonTitle = environment.strings.CollectibleItemInfo_ButtonCopyPhone + } + + + let copyButtonSize = self.copyButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: copyButtonTitle, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)) + )), + background: nil, + effectAlignment: .center, + minSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + + switch component.initialData.subject { + case let .username(username): + UIPasteboard.general.string = "https://t.me/\(username.username)" + case let .phoneNumber(phoneNumber): + let formattedPhoneNumber = formatPhoneNumber(context: component.context, number: phoneNumber.phoneNumber) + UIPasteboard.general.string = formattedPhoneNumber + } + + component.dismiss() + }, + isEnabled: true, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let copyButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: copyButtonSize) + if let copyButtonView = self.copyButton.view { + if copyButtonView.superview == nil { + self.addSubview(copyButtonView) + } + transition.setFrame(view: copyButtonView, frame: copyButtonFrame) + } + contentHeight += copyButtonSize.height - 9.0 + + if environment.safeInsets.bottom.isZero { + contentHeight += 16.0 + } else { + contentHeight += environment.safeInsets.bottom + 14.0 + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + 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) + } +} + +private final class CollectibleItemInfoScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: CollectibleItemInfoScreen.InitialData + + init( + context: AccountContext, + initialData: CollectibleItemInfoScreen.InitialData + ) { + self.context = context + self.initialData = initialData + } + + static func ==(lhs: CollectibleItemInfoScreenComponent, rhs: CollectibleItemInfoScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: CollectibleItemInfoScreenComponent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: CollectibleItemInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.sheetAnimateOut.invoke(Action { _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(CollectibleItemInfoScreenContentComponent( + context: component.context, + initialData: component.initialData, + dismiss: { [weak self] in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + + guard let self else { + return + } + //TODO:open info + let _ = self + }) + } + )), + backgroundColor: .color(environment.theme.list.plainBackgroundColor), + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } + } + + 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 class CollectibleItemInfoScreen: ViewControllerComponentContainer { + fileprivate enum ResolvedSubject { + struct Username { + var username: String + var info: TelegramCollectibleItemInfo + + init(username: String, info: TelegramCollectibleItemInfo) { + self.username = username + self.info = info + } + } + + struct PhoneNumber { + var phoneNumber: String + var info: TelegramCollectibleItemInfo + + init(phoneNumber: String, info: TelegramCollectibleItemInfo) { + self.phoneNumber = phoneNumber + self.info = info + } + } + + case username(Username) + case phoneNumber(PhoneNumber) + } + + public final class InitialData: CollectibleItemInfoScreenInitialData { + fileprivate let peer: EnginePeer? + fileprivate let subject: ResolvedSubject + + fileprivate init(peer: EnginePeer?, subject: ResolvedSubject) { + self.peer = peer + self.subject = subject + } + + public var collectibleItemInfo: TelegramCollectibleItemInfo { + switch self.subject { + case let .username(username): + return username.info + case let .phoneNumber(phoneNumber): + return phoneNumber.info + } + } + } + + public init(context: AccountContext, initialData: InitialData) { + super.init(context: context, component: CollectibleItemInfoScreenComponent( + context: context, + initialData: initialData + ), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal { + switch subject { + case let .username(username): + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ), + context.engine.peers.getCollectibleUsernameInfo(username: username) + ) + |> map { peer, result -> CollectibleItemInfoScreenInitialData? in + guard let result else { + return nil + } + return InitialData(peer: peer, subject: .username(ResolvedSubject.Username( + username: username, + info: result + ))) + } + case let .phoneNumber(phoneNumber): + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ), + context.engine.peers.getCollectiblePhoneNumberInfo(phoneNumber: phoneNumber) + ) + |> map { peer, result -> CollectibleItemInfoScreenInitialData? in + guard let result else { + return nil + } + return InitialData(peer: peer, subject: .phoneNumber(ResolvedSubject.PhoneNumber( + phoneNumber: phoneNumber, + info: result + ))) + } + } + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift new file mode 100644 index 0000000000..288cbffad0 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift @@ -0,0 +1,333 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import AppBundle +import BundleIconComponent +import Markdown +import TelegramCore + +/*public final class CollectibleItemInfoScreenContentComponent: Component { + public let theme: PresentationTheme + public let strings: PresentationStrings + public let settings: GlobalPrivacySettings + public let openSettings: () -> Void + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + settings: GlobalPrivacySettings, + openSettings: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.settings = settings + self.openSettings = openSettings + } + + public static func ==(lhs: CollectibleItemInfoScreenContentComponent, rhs: CollectibleItemInfoScreenContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.settings != rhs.settings { + return false + } + return true + } + + private final class Item { + let icon = ComponentView() + let title = ComponentView() + let text = ComponentView() + + init() { + } + } + + public final class View: UIView { + private let scrollView: UIScrollView + private let iconBackground: UIImageView + private let iconForeground: UIImageView + + private let title = ComponentView() + private let mainText = ComponentView() + + private var chevronImage: UIImage? + + private var items: [Item] = [] + + private var component: CollectibleItemInfoScreenContentComponent? + + public override init(frame: CGRect) { + self.scrollView = UIScrollView() + + self.iconBackground = UIImageView() + self.iconForeground = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.scrollView) + + self.scrollView.delaysContentTouches = false + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = false + + self.scrollView.addSubview(self.iconBackground) + self.scrollView.addSubview(self.iconForeground) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = super.hitTest(point, with: event) { + return result + } else { + return nil + } + } + + func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 16.0 + let sideIconInset: CGFloat = 40.0 + + var contentHeight: CGFloat = 0.0 + + let iconSize: CGFloat = 90.0 + if self.iconBackground.image == nil { + let backgroundColors = component.theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors + let colors: NSArray = [backgroundColors.1.cgColor, backgroundColors.0.cgColor] + self.iconBackground.image = generateGradientFilledCircleImage(diameter: iconSize, colors: colors) + } + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize) * 0.5), y: contentHeight), size: CGSize(width: iconSize, height: iconSize)) + transition.setFrame(view: self.iconBackground, frame: iconBackgroundFrame) + + if self.iconForeground.image == nil { + self.iconForeground.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/ArchiveIconLarge"), color: .white) + } + if let image = self.iconForeground.image { + transition.setFrame(view: self.iconForeground, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - image.size.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - image.size.height) * 0.5)), size: image.size)) + } + + contentHeight += iconSize + contentHeight += 15.0 + + let titleString = NSMutableAttributedString() + titleString.append(NSAttributedString(string: component.strings.ArchiveInfo_Title, font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor)) + let imageAttachment = NSTextAttachment() + imageAttachment.image = self.iconBackground.image + titleString.append(NSAttributedString(attachment: imageAttachment)) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleString), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 16.0 + + let text: String + if component.settings.keepArchivedUnmuted { + text = component.strings.ArchiveInfo_TextKeepArchivedUnmuted + } else { + text = component.strings.ArchiveInfo_TextKeepArchivedDefault + } + + let mainText = NSMutableAttributedString() + mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + bold: MarkdownAttributeSet( + font: Font.semibold(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + link: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemAccentColor, + additionalAttributes: [:] + ), + linkAttribute: { attributes in + return ("URL", "") + } + ))) + if self.chevronImage == nil { + self.chevronImage = UIImage(bundleImageName: "Settings/TextArrowRight") + } + if let range = mainText.string.range(of: ">"), let chevronImage = self.chevronImage { + mainText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: mainText.string)) + } + + let mainTextSize = self.mainText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(mainText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + component.openSettings() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let mainTextView = self.mainText.view { + if mainTextView.superview == nil { + self.scrollView.addSubview(mainTextView) + } + transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize)) + } + contentHeight += mainTextSize.height + + contentHeight += 24.0 + + struct ItemDesc { + var icon: String + var title: String + var text: String + } + let itemDescs: [ItemDesc] = [ + ItemDesc( + icon: "Chat List/Archive/IconArchived", + title: component.strings.ArchiveInfo_ChatsTitle, + text: component.strings.ArchiveInfo_ChatsText + ), + ItemDesc( + icon: "Chat List/Archive/IconHide", + title: component.strings.ArchiveInfo_HideTitle, + text: component.strings.ArchiveInfo_HideText + ), + ItemDesc( + icon: "Chat List/Archive/IconStories", + title: component.strings.ArchiveInfo_StoriesTitle, + text: component.strings.ArchiveInfo_StoriesText + ) + ] + for i in 0 ..< itemDescs.count { + if i != 0 { + contentHeight += 24.0 + } + + let item: Item + if self.items.count > i { + item = self.items[i] + } else { + item = Item() + self.items.append(item) + } + + let itemDesc = itemDescs[i] + + let iconSize = item.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: itemDesc.icon, + tintColor: component.theme.list.itemAccentColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let titleSize = item.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + let textSize = item.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: itemDesc.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + + if let iconView = item.icon.view { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 4.0), size: iconSize)) + } + + if let titleView = item.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 2.0 + + if let textView = item.text.view { + if textView.superview == nil { + self.scrollView.addSubview(textView) + } + transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: textSize)) + } + contentHeight += textSize.height + } + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + let size = CGSize(width: availableSize.width, height: min(availableSize.height, contentSize.height)) + if self.scrollView.bounds.size != size || self.scrollView.contentSize != contentSize { + self.scrollView.frame = CGRect(origin: CGPoint(), size: size) + self.scrollView.contentSize = contentSize + } + + 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/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift index 95cfb9911d..1ee373526b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift @@ -407,7 +407,7 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { let rowsCount = ceil(CGFloat(numItems) / CGFloat(itemsPerRow)) - contentSize = CGSize(width: params.width, height: 48.0 * rowsCount) + contentSize = CGSize(width: params.width, height: 10.0 + 42.0 * rowsCount) insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -518,6 +518,7 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { var origin = CGPoint(x: sideInset, y: 10.0) i = 0 + var validIds = Set() for item in items { let iconItemNode: PeerNameColorIconItemNode let indexKey: Int32 @@ -545,6 +546,20 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { origin.x = sideInset origin.y += iconSize.height + 10.0 } + + validIds.insert(indexKey) + } + + var removeKeys: [Int32] = [] + for (id, _) in strongSelf.itemNodes { + if !validIds.contains(id) { + removeKeys.append(id) + } + } + for id in removeKeys { + if let itemNode = strongSelf.itemNodes.removeValue(forKey: id) { + itemNode.removeFromSupernode() + } } } }) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index 72cfb99bc2..c233426027 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -663,6 +663,8 @@ public func PeerNameColorScreen( }, clearGroup: { _ in }, + editAction: { _ in + }, pushController: { c in }, presentController: { c in 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 270874cdbc..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 @@ -991,6 +979,7 @@ public class StickerPickerScreen: ViewController { context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) } }, + editAction: { _ in }, pushController: { c in }, presentController: { c in @@ -1122,6 +1111,7 @@ public class StickerPickerScreen: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1177,6 +1167,7 @@ public class StickerPickerScreen: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1208,6 +1199,7 @@ public class StickerPickerScreen: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1386,6 +1378,7 @@ public class StickerPickerScreen: ViewController { } else if groupId == AnyHashable("peerSpecific") { } }, + editAction: { _ in }, pushController: { c in }, presentController: { c in @@ -1450,6 +1443,7 @@ public class StickerPickerScreen: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1481,6 +1475,7 @@ public class StickerPickerScreen: ViewController { isPremiumLocked: false, isEmbedded: false, hasClear: false, + hasEdit: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, @@ -1507,7 +1502,7 @@ public class StickerPickerScreen: ViewController { customLayout: nil, externalBackground: nil, externalExpansionView: nil, - customContentView: controller.hasGifs ? self.storyStickersContentView : nil, + customContentView: controller.hasInteractiveStickers ? self.storyStickersContentView : nil, useOpaqueTheme: false, hideBackground: true, stateContext: nil, @@ -1537,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) { @@ -1548,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 @@ -1572,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 @@ -1636,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 @@ -1746,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 { @@ -1782,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 @@ -1963,9 +1978,12 @@ 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 private var currentLayout: ContainerViewLayout? @@ -1980,16 +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) { + 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) { @@ -2005,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/Stickers/StickerPackEditTitleController/BUILD b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/BUILD new file mode 100644 index 0000000000..0b594f8af3 --- /dev/null +++ b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StickerPackEditTitleController", + module_name = "StickerPackEditTitleController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/UrlEscaping", + "//submodules/ActivityIndicator", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift similarity index 96% rename from submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift rename to submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift index c8f34868f9..c94161c26b 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift +++ b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift @@ -275,6 +275,7 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, UITextF self.textInputNode.returnKeyType = returnKeyType self.textInputNode.autocorrectionType = .default self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + self.textInputNode.textColor = theme.actionSheet.inputTextColor self.clearButton = HighlightableButtonNode() self.clearButton.imageNode.displaysAsynchronously = false @@ -308,7 +309,9 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, UITextF self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor - self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) + self.textInputNode.textColor = self.theme.actionSheet.inputTextColor + self.textInputNode.typingAttributes = [NSAttributedString.Key.font: Font.regular(14.0), NSAttributedString.Key.foregroundColor: self.theme.actionSheet.inputTextColor] + self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: self.theme.actionSheet.inputClearButtonColor), for: []) } func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -725,15 +728,18 @@ private final class ImportStickerPackTitleAlertContentNode: AlertContentNode { } } -func importStickerPackTitleController(context: AccountContext, title: String, text: String, placeholder: String, value: String?, maxLength: Int, apply: @escaping (String?) -> Void, cancel: @escaping () -> Void) -> AlertController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } +public func stickerPackEditTitleController(context: AccountContext, forceDark: Bool = false, title: String, text: String, placeholder: String, actionTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void, cancel: @escaping () -> Void) -> AlertController { + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } var dismissImpl: ((Bool) -> Void)? var applyImpl: (() -> Void)? let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?(true) cancel() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Next, action: { + }), TextAlertAction(type: .defaultAction, title: actionTitle ?? presentationData.strings.Common_Next, action: { applyImpl?() })] @@ -759,6 +765,10 @@ func importStickerPackTitleController(context: AccountContext, title: String, te let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + var presentationData = presentationData + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } controller?.theme = AlertControllerTheme(presentationData: presentationData) contentNode?.inputFieldNode.updateTheme(presentationData.theme) }) @@ -784,7 +794,7 @@ func importStickerPackTitleController(context: AccountContext, title: String, te } -func importStickerPackShortNameController(context: AccountContext, title: String, text: String, placeholder: String, value: String?, maxLength: Int, existingAlertController: AlertController?, apply: @escaping (String?) -> Void) -> AlertController { +public func importStickerPackShortNameController(context: AccountContext, title: String, text: String, placeholder: String, value: String?, maxLength: Int, existingAlertController: AlertController?, apply: @escaping (String?) -> Void) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var dismissImpl: ((Bool) -> Void)? var applyImpl: (() -> Void)? diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 42c75a2a00..7bc8d4a842 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -209,8 +209,31 @@ final class StoryContentCaptionComponent: Component { override init(frame: CGRect) { self.shadowGradientView = UIImageView() - if let image = StoryContentCaptionComponent.View.shadowImage { - self.shadowGradientView.image = image.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(image.size.height - 1.0)) + if let _ = StoryContentCaptionComponent.View.shadowImage { + let height: CGFloat = 128.0 + let baseGradientAlpha: CGFloat = 0.8 + let numSteps = 8 + let firstStep = 0 + let firstLocation = 0.0 + let colors = (0 ..< numSteps).map { i -> UIColor in + if i < firstStep { + return UIColor(white: 1.0, alpha: 1.0) + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + return UIColor(white: 0.0, alpha: baseGradientAlpha * value) + } + } + let locations = (0 ..< numSteps).map { i -> CGFloat in + if i < firstStep { + return 0.0 + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + return (firstLocation + (1.0 - firstLocation) * step) + } + } + + self.shadowGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0)) } self.scrollViewContainer = UIView() @@ -386,7 +409,8 @@ final class StoryContentCaptionComponent: Component { transition.setBounds(view: self.textSelectionKnobContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.scrollView.bounds.minY), size: CGSize())) - let shadowOverflow: CGFloat = 58.0 + let shadowHeight: CGFloat = self.shadowGradientView.image?.size.height ?? 100.0 + let shadowOverflow: CGFloat = floor(shadowHeight * 0.6) let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow)) let shadowGradientFrame = CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.minY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 412e4c50ff..ab245eef4b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2690,7 +2690,7 @@ public final class StoryItemSetContainerComponent: Component { self.bottomContentGradientLayer.colors = colors self.bottomContentGradientLayer.type = .axial - self.contentDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.3) + self.contentDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.8) } let wasPanning = self.component?.isPanning ?? false @@ -5583,6 +5583,8 @@ public final class StoryItemSetContainerComponent: Component { } })) } + default: + break } } else if updatedText != nil { let _ = (context.engine.messages.editStory(peerId: peerId, id: id, media: nil, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil) 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/Chat/Context Menu/AddCircle.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddCircle.imageset/Contents.json new file mode 100644 index 0000000000..2f71e3c395 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddCircle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pluscircle_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddCircle.imageset/pluscircle_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddCircle.imageset/pluscircle_24.pdf new file mode 100644 index 0000000000..f91e560299 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddCircle.imageset/pluscircle_24.pdf @@ -0,0 +1,95 @@ +%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 3.334961 3.334961 cm +0.000000 0.000000 0.000000 scn +1.330000 8.665078 m +1.330000 12.716087 4.613991 16.000078 8.665000 16.000078 c +12.716008 16.000078 16.000000 12.716087 16.000000 8.665078 c +16.000000 4.614070 12.716008 1.330078 8.665000 1.330078 c +4.613991 1.330078 1.330000 4.614070 1.330000 8.665078 c +h +8.665000 17.330078 m +3.879453 17.330078 0.000000 13.450625 0.000000 8.665078 c +0.000000 3.879531 3.879453 0.000076 8.665000 0.000076 c +13.450547 0.000076 17.330002 3.879531 17.330002 8.665078 c +17.330002 13.450625 13.450547 17.330078 8.665000 17.330078 c +h +8.664985 13.330078 m +9.032254 13.330078 9.329985 13.032348 9.329985 12.665078 c +9.329985 9.330078 l +12.664985 9.330078 l +13.032254 9.330078 13.329985 9.032348 13.329985 8.665078 c +13.329985 8.297809 13.032254 8.000078 12.664985 8.000078 c +9.329985 8.000078 l +9.329985 4.665078 l +9.329985 4.297809 9.032254 4.000078 8.664985 4.000078 c +8.297715 4.000078 7.999985 4.297809 7.999985 4.665078 c +7.999985 8.000078 l +4.664985 8.000078 l +4.297715 8.000078 3.999985 8.297809 3.999985 8.665078 c +3.999985 9.032348 4.297715 9.330078 4.664985 9.330078 c +7.999985 9.330078 l +7.999985 12.665078 l +7.999985 13.032348 8.297715 13.330078 8.664985 13.330078 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1284 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.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 +0000001374 00000 n +0000001397 00000 n +0000001570 00000 n +0000001644 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1703 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddSticker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddSticker.imageset/Contents.json new file mode 100644 index 0000000000..29df4dfe49 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddSticker.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addsticker_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddSticker.imageset/addsticker_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddSticker.imageset/addsticker_24.pdf new file mode 100644 index 0000000000..4c92cf0842 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddSticker.imageset/addsticker_24.pdf @@ -0,0 +1,229 @@ +%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 3.834961 3.834961 cm +0.000000 0.000000 0.000000 scn +5.465001 16.330078 m +5.436179 16.330078 l +5.436174 16.330078 l +4.620535 16.330084 3.967873 16.330088 3.440454 16.286997 c +2.899074 16.242764 2.431364 16.149834 2.001125 15.930616 c +1.311511 15.579241 0.750837 15.018567 0.399462 14.328953 c +0.180244 13.898714 0.087314 13.431005 0.043082 12.889624 c +-0.000010 12.362206 -0.000006 11.709543 0.000000 10.893904 c +0.000000 10.893900 l +0.000000 10.865078 l +0.000000 8.165078 l +0.000000 7.797809 0.297731 7.500078 0.665000 7.500078 c +1.032270 7.500078 1.330001 7.797809 1.330001 8.165078 c +1.330001 10.865078 l +1.330001 11.716129 1.330518 12.314425 1.368665 12.781320 c +1.406177 13.240452 1.476737 13.513649 1.584500 13.725145 c +1.808365 14.164504 2.165574 14.521713 2.604933 14.745578 c +2.816429 14.853341 3.089627 14.923901 3.548759 14.961413 c +4.015654 14.999560 4.613949 15.000077 5.465001 15.000077 c +10.865000 15.000077 l +11.716052 15.000077 12.314346 14.999560 12.781242 14.961413 c +13.240374 14.923901 13.513572 14.853341 13.725068 14.745578 c +14.164426 14.521713 14.521636 14.164504 14.745501 13.725145 c +14.853263 13.513649 14.923823 13.240452 14.961336 12.781320 c +14.999483 12.314425 15.000000 11.716129 15.000000 10.865078 c +15.000001 10.165077 l +15.000000 8.165078 l +15.000000 7.949642 14.999989 7.745213 14.999686 7.550866 c +14.998948 7.380232 14.996512 7.259462 14.988585 7.162432 c +14.983507 7.100278 14.977125 7.062618 14.971725 7.039696 c +14.969157 7.028788 14.966998 7.022051 14.965687 7.018376 c +14.964426 7.014845 14.963658 7.013326 14.963488 7.012991 c +14.931371 6.949957 14.880122 6.898708 14.817087 6.866590 c +14.816753 6.866420 14.815233 6.865652 14.811703 6.864392 c +14.808028 6.863081 14.801291 6.860921 14.790383 6.858353 c +14.767460 6.852954 14.729801 6.846571 14.667646 6.841494 c +14.534256 6.830595 14.356001 6.830078 14.065001 6.830078 c +13.365002 6.830078 l +13.337530 6.830078 l +12.800831 6.830087 12.357981 6.830093 11.997252 6.800621 c +11.622624 6.770012 11.278399 6.704330 10.955116 6.539610 c +10.453665 6.284107 10.045971 5.876414 9.790468 5.374962 c +9.757796 5.310840 9.729023 5.245891 9.703645 5.180046 c +9.294682 4.955009 8.789659 4.830078 8.165001 4.830078 c +7.341558 4.830078 6.728102 5.136502 6.314001 5.447078 c +6.105952 5.603114 5.950729 5.758654 5.849528 5.872505 c +5.799157 5.929173 5.762889 5.974701 5.740838 6.003644 c +5.729833 6.018087 5.722438 6.028310 5.718665 6.033616 c +5.715932 6.037502 l +5.511385 6.340178 5.100525 6.421325 4.796125 6.218391 c +4.490538 6.014668 4.407963 5.601789 4.611687 5.296203 c +5.165000 5.665078 l +4.611687 5.296203 4.611857 5.295947 4.612031 5.295686 c +4.612401 5.295135 l +4.613211 5.293926 l +4.615123 5.291091 l +4.620114 5.283784 l +4.634774 5.262849 l +4.646626 5.246182 4.662668 5.224178 4.682913 5.197606 c +4.723361 5.144518 4.780844 5.072859 4.855473 4.988901 c +5.004272 4.821503 5.224049 4.602041 5.516000 4.383078 c +6.101899 3.943654 6.988443 3.500078 8.165001 3.500078 c +8.637335 3.500078 9.084862 3.556022 9.502834 3.672907 c +9.499994 3.465117 9.499997 3.238721 9.500001 2.992584 c +9.500001 2.992556 l +9.500001 2.965078 l +9.500001 2.265078 l +9.500001 1.974079 9.499484 1.795822 9.488585 1.662432 c +9.483507 1.600277 9.477124 1.562618 9.471726 1.539696 c +9.469157 1.528788 9.466998 1.522051 9.465687 1.518375 c +9.464426 1.514845 9.463658 1.513325 9.463488 1.512991 c +9.431371 1.449957 9.380122 1.398708 9.317088 1.366590 c +9.316753 1.366421 9.315233 1.365652 9.311703 1.364391 c +9.308028 1.363081 9.301291 1.360922 9.290383 1.358353 c +9.267460 1.352955 9.229801 1.346571 9.167646 1.341494 c +9.070562 1.333561 8.949712 1.331128 8.778924 1.330392 c +8.584664 1.330091 8.380329 1.330078 8.165000 1.330078 c +7.797731 1.330078 7.500001 1.032347 7.500001 0.665077 c +7.500001 0.297808 7.797731 0.000078 8.165000 0.000078 c +8.189899 0.000078 l +8.207291 0.000078 8.224624 0.000078 8.241899 0.000078 c +8.565001 0.000078 l +8.589002 0.000078 l +8.589076 0.000078 l +8.658995 0.000074 8.727019 0.000071 8.792893 0.000378 c +9.654441 0.001719 10.357803 0.009060 10.946046 0.049196 c +11.677188 0.099081 12.278939 0.201841 12.832902 0.431300 c +14.220985 1.006264 15.323814 2.109093 15.898778 3.497176 c +16.128237 4.051139 16.230997 4.652890 16.280882 5.384032 c +16.321011 5.972157 16.328358 6.675352 16.329700 7.536662 c +16.330009 7.602722 16.330006 7.670947 16.330002 7.741076 c +16.330002 7.765078 l +16.330000 8.140180 l +16.330000 8.165078 l +16.330002 10.165077 l +16.330002 10.165615 16.330000 10.166151 16.330000 10.166687 c +16.330000 10.865078 l +16.330000 10.893884 l +16.330000 10.893948 l +16.330006 11.709566 16.330009 12.362215 16.286919 12.889624 c +16.242687 13.431005 16.149757 13.898714 15.930539 14.328953 c +15.579163 15.018567 15.018489 15.579241 14.328876 15.930616 c +13.898637 16.149834 13.430927 16.242764 12.889546 16.286997 c +12.362127 16.330088 11.709462 16.330084 10.893822 16.330078 c +10.865000 16.330078 l +5.465001 16.330078 l +h +14.958070 5.537087 m +14.896574 5.527458 14.835731 5.520794 14.775951 5.515910 c +14.581818 5.500050 14.348466 5.500062 14.089046 5.500076 c +14.089024 5.500076 l +14.065001 5.500078 l +13.365002 5.500078 l +12.793976 5.500078 12.405701 5.499560 12.105556 5.475039 c +11.813177 5.451149 11.663464 5.407837 11.558924 5.354571 c +11.307728 5.226581 11.103498 5.022351 10.975508 4.771154 c +10.922241 4.666615 10.878929 4.516901 10.855041 4.224522 c +10.830518 3.924378 10.830001 3.536103 10.830001 2.965078 c +10.830001 2.265078 l +10.830002 2.241066 l +10.830017 1.981632 10.830030 1.748270 10.814168 1.554127 c +10.809284 1.494348 10.802620 1.433504 10.792991 1.372008 c +10.814003 1.373334 10.834842 1.374701 10.855511 1.376111 c +11.518936 1.421376 11.959040 1.508917 12.323933 1.660061 c +13.386129 2.100037 14.230042 2.943949 14.670017 4.006145 c +14.821161 4.371038 14.908702 4.811142 14.953967 5.474567 c +14.955378 5.495236 14.956744 5.516075 14.958070 5.537087 c +h +6.665000 9.915077 m +6.665000 9.224722 6.217285 8.665077 5.665000 8.665077 c +5.112716 8.665077 4.665000 9.224722 4.665000 9.915077 c +4.665000 10.605433 5.112716 11.165077 5.665000 11.165077 c +6.217285 11.165077 6.665000 10.605433 6.665000 9.915077 c +h +11.665001 9.915077 m +11.665001 9.224722 11.217285 8.665077 10.665001 8.665077 c +10.112716 8.665077 9.665001 9.224722 9.665001 9.915077 c +9.665001 10.605433 10.112716 11.165077 10.665001 11.165077 c +11.217285 11.165077 11.665001 10.605433 11.665001 9.915077 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 0.830078 0.839844 cm +0.000000 0.000000 0.000000 scn +5.330000 8.665078 m +5.330000 9.032348 5.032269 9.330078 4.665000 9.330078 c +4.297730 9.330078 4.000000 9.032348 4.000000 8.665078 c +4.000000 5.330078 l +0.665000 5.330078 l +0.297731 5.330078 0.000000 5.032348 0.000000 4.665078 c +0.000000 4.297809 0.297731 4.000078 0.665000 4.000078 c +4.000000 4.000078 l +4.000000 0.665078 l +4.000000 0.297809 4.297730 0.000078 4.665000 0.000078 c +5.032269 0.000078 5.330000 0.297809 5.330000 0.665078 c +5.330000 4.000078 l +8.665000 4.000078 l +9.032269 4.000078 9.330000 4.297809 9.330000 4.665078 c +9.330000 5.032348 9.032269 5.330078 8.665000 5.330078 c +5.330000 5.330078 l +5.330000 8.665078 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 7178 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.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 +0000007268 00000 n +0000007291 00000 n +0000007464 00000 n +0000007538 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +7597 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/Contents.json new file mode 100644 index 0000000000..9d3f9124cb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "more.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/more.pdf b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/more.pdf new file mode 100644 index 0000000000..82da70ea58 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/InlineTextRightArrow.imageset/more.pdf @@ -0,0 +1,79 @@ +%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 1.500000 1.335754 cm +0.000000 0.000000 0.000000 scn +5.252930 4.662109 m +5.252930 4.527832 5.199219 4.409668 5.097168 4.307617 c +0.843262 0.145020 l +0.746582 0.048340 0.628418 0.000000 0.488770 0.000000 c +0.214844 0.000000 0.000000 0.209473 0.000000 0.488770 c +0.000000 0.628418 0.053711 0.746582 0.139648 0.837891 c +4.049805 4.662109 l +0.139648 8.486328 l +0.053711 8.577637 0.000000 8.701172 0.000000 8.835449 c +0.000000 9.114746 0.214844 9.324219 0.488770 9.324219 c +0.628418 9.324219 0.746582 9.275879 0.843262 9.184570 c +5.097168 5.016602 l +5.199219 4.919922 5.252930 4.796387 5.252930 4.662109 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 675 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 8.000000 12.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 +0000000765 00000 n +0000000787 00000 n +0000000959 00000 n +0000001033 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1092 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleDot.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleDot.imageset/Contents.json new file mode 100644 index 0000000000..29572eb880 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleDot.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "dot.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleDot.imageset/dot.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleDot.imageset/dot.png new file mode 100644 index 0000000000..3ac5dcbcb5 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleDot.imageset/dot.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleGlow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleGlow.imageset/Contents.json new file mode 100644 index 0000000000..6c8dfa9d38 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleGlow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "glow.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleGlow.imageset/glow.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleGlow.imageset/glow.png new file mode 100644 index 0000000000..d3be506a28 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/ParticleGlow.imageset/glow.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/Contents.json new file mode 100644 index 0000000000..4488755ba2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ton_12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/ton_12.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/ton_12.pdf new file mode 100644 index 0000000000..238337b6c9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/ton_12.pdf @@ -0,0 +1,83 @@ +%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 0.143555 0.130127 cm +0.000000 0.000000 0.000000 scn +5.156074 3.095078 m +1.539963 9.669825 l +5.156074 9.669825 l +5.156074 3.095078 l +h +6.556074 3.095079 m +10.172184 9.669825 l +6.556074 9.669825 l +6.556074 3.095079 l +h +1.201709 11.069824 m +0.288984 11.069824 -0.289608 10.091264 0.150250 9.291522 c +4.804615 0.829041 l +5.260527 0.000110 6.451622 0.000113 6.907533 0.829041 c +11.561897 9.291523 l +12.001758 10.091268 11.423159 11.069824 10.510438 11.069824 c +1.201709 11.069824 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 553 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 12.000000 12.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 +0000000643 00000 n +0000000665 00000 n +0000000838 00000 n +0000000912 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +971 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json new file mode 100644 index 0000000000..6a062695f3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "username.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf new file mode 100644 index 0000000000..548f72b27c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf @@ -0,0 +1,100 @@ +%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 19.254883 19.005127 cm +0.000000 0.000000 0.000000 scn +0.000000 25.994989 m +0.000000 40.351631 11.638357 51.989990 25.995001 51.989990 c +40.351643 51.989990 51.989998 40.351631 51.989998 25.994989 c +51.989998 19.994989 l +51.989998 15.579475 48.410515 11.999992 43.994999 11.999992 c +40.680130 11.999992 37.836452 14.017384 36.624741 16.891380 c +34.058002 13.897156 30.248056 11.999989 25.995001 11.999989 c +18.265776 11.999989 12.000000 18.265766 12.000000 25.994989 c +12.000000 33.724213 18.265776 39.989990 25.995001 39.989990 c +29.919403 39.989990 33.466534 38.374702 36.007809 35.772713 c +36.097725 36.791267 36.953087 37.589989 37.994999 37.589989 c +39.096806 37.589989 39.989998 36.696800 39.989998 35.594986 c +39.989998 26.005434 l +39.990002 25.994989 l +39.989998 25.984545 l +39.989998 19.994995 l +39.989998 17.783092 41.783100 15.989990 43.994999 15.989990 c +46.206898 15.989990 48.000000 17.783092 48.000000 19.994989 c +48.000000 25.994989 l +48.000000 38.148018 38.148026 47.999989 25.995001 47.999989 c +13.841973 47.999989 3.990000 38.148018 3.990000 25.994989 c +3.990000 25.793646 3.992699 25.592970 3.998061 25.392994 c +4.317001 13.518284 14.043282 3.989990 25.995001 3.989990 c +29.449270 3.989990 32.712318 4.784481 35.616467 6.198845 c +36.607044 6.681271 37.801151 6.269333 38.283577 5.278755 c +38.766006 4.288174 38.354069 3.094070 37.363487 2.611641 c +33.926983 0.938011 30.067709 -0.000008 25.995001 -0.000008 c +11.638357 -0.000008 0.000000 11.638348 0.000000 25.994989 c +h +25.995001 35.999992 m +31.520609 35.999992 35.999996 31.520597 35.999996 25.994989 c +35.999996 20.469381 31.520609 15.989994 25.995001 15.989994 c +20.469393 15.989994 15.990000 20.469381 15.990000 25.994989 c +15.990000 31.520597 20.469393 35.999992 25.995001 35.999992 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1836 +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 +0000001926 00000 n +0000001949 00000 n +0000002122 00000 n +0000002196 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2255 +%%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/Resources/Animations/ToastCollectibleUsernameEmoji.tgs b/submodules/TelegramUI/Resources/Animations/ToastCollectibleUsernameEmoji.tgs new file mode 100644 index 0000000000..f93fd6d746 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/ToastCollectibleUsernameEmoji.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_speed_low.tgs b/submodules/TelegramUI/Resources/Animations/anim_speed_low.tgs new file mode 100644 index 0000000000..bb9b6f86c6 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/anim_speed_low.tgs differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c0160b6d7c..aaf9c723e9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -585,6 +585,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)? var performOpenURL: ((Message?, String, Promise?) -> Void)? + var networkSpeedEventsDisposable: Disposable? + public var alwaysShowSearchResultsAsList: Bool = false { didSet { self.presentationInterfaceState = self.presentationInterfaceState.updatedDisplayHistoryFilterAsList(self.alwaysShowSearchResultsAsList) @@ -714,11 +716,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) { return false } - - if strongSelf.presentRecordedVoiceMessageDiscardAlert(action: action, performAction: false) { - return false - } - + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject { switch customChatContents.kind { case let .quickReplyMessageInput(_, shortcutType): @@ -4910,6 +4908,43 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } + let managingBot: Signal + if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudUser, !"".isEmpty { + managingBot = self.context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.BusinessConnectedBot(id: self.context.account.peerId) + ) + |> mapToSignal { result -> Signal in + guard let result else { + return .single(nil) + } + return context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: result.id) + ) + |> map { botPeer -> ChatManagingBot? in + guard let botPeer else { + return nil + } + + var isPaused = false + if result.recipients.exclude { + isPaused = result.recipients.additionalPeers.contains(peerId) + } else { + isPaused = !result.recipients.additionalPeers.contains(peerId) + } + + var settingsUrl: String? + if let username = botPeer.addressName { + settingsUrl = "https://t.me/\(username)" + } + + return ChatManagingBot(bot: botPeer, isPaused: isPaused, canReply: result.canReply, settingsUrl: settingsUrl) + } + } + |> distinctUntilChanged + } else { + managingBot = .single(nil) + } + do { let peerId = chatLocationPeerId if case let .peer(peerView) = self.chatLocationInfoData, let peerId = peerId { @@ -5298,10 +5333,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G threadInfo, hasSearchTags, hasSavedChats, - isPremiumRequiredForMessaging - ).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging in + isPremiumRequiredForMessaging, + managingBot + ).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in if let strongSelf = self { - if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging { + if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot { return } @@ -5396,7 +5432,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var contactStatus: ChatContactStatus? if let peer = peerView.peers[peerView.peerId] { if let cachedData = peerView.cachedData as? CachedUserData { - contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil) + contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot) } else if let cachedData = peerView.cachedData as? CachedGroupData { var invitedBy: Peer? if let invitedByPeerId = cachedData.invitedBy { @@ -5404,7 +5440,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G invitedBy = peer } } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy) + contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) } else if let cachedData = peerView.cachedData as? CachedChannelData { var canReportIrrelevantLocation = true if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member { @@ -5419,7 +5455,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G invitedBy = peer } } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy) + contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) } var peers = SimpleDictionary() @@ -5522,6 +5558,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil { + didDisplayActionsPanel = true + } if strongSelf.presentationInterfaceState.search != nil && strongSelf.presentationInterfaceState.hasSearchTags { didDisplayActionsPanel = true } @@ -5542,6 +5581,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + if let contactStatus, contactStatus.managingBot != nil { + displayActionsPanel = true + } if strongSelf.presentationInterfaceState.search != nil && hasSearchTags { displayActionsPanel = true } @@ -5875,9 +5917,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G hasScheduledMessages, hasSearchTags, hasSavedChats, - isPremiumRequiredForMessaging + isPremiumRequiredForMessaging, + managingBot ) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging in + |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in if let strongSelf = self { strongSelf.hasScheduledMessages = hasScheduledMessages @@ -5887,7 +5930,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let peer = peerView.peers[peerView.peerId] { copyProtectionEnabled = peer.isCopyProtectionEnabled if let cachedData = peerView.cachedData as? CachedUserData { - contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil) + contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot) } else if let cachedData = peerView.cachedData as? CachedGroupData { var invitedBy: Peer? if let invitedByPeerId = cachedData.invitedBy { @@ -5895,7 +5938,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G invitedBy = peer } } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy) + contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) } else if let cachedData = peerView.cachedData as? CachedChannelData { var canReportIrrelevantLocation = true if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member { @@ -5908,7 +5951,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G invitedBy = peer } } - contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy) + contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot) } var peers = SimpleDictionary() @@ -6112,6 +6155,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, contactStatus.managingBot != nil { + didDisplayActionsPanel = true + } var displayActionsPanel = false if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { @@ -6129,6 +6175,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + if let contactStatus, contactStatus.managingBot != nil { + displayActionsPanel = true + } if displayActionsPanel != didDisplayActionsPanel { animated = true @@ -6800,6 +6849,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.preloadSavedMessagesChatsDisposable?.dispose() self.recorderDataDisposable.dispose() self.displaySendWhenOnlineTipDisposable.dispose() + self.networkSpeedEventsDisposable?.dispose() } deallocate() } @@ -11689,6 +11739,57 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + var lastEventTimestamp: Double = 0.0 + self.networkSpeedEventsDisposable = (self.context.account.network.networkSpeedLimitedEvents + |> deliverOnMainQueue).start(next: { [weak self] event in + guard let self else { + return + } + + let timestamp = CFAbsoluteTimeGetCurrent() + if lastEventTimestamp + 10.0 < timestamp { + lastEventTimestamp = timestamp + } else { + return + } + + //TODO:localize + let title: String + let text: String + switch event { + case .download: + var speedIncreaseFactor = 10 + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["upload_premium_speedup_download"] as? Double { + speedIncreaseFactor = Int(value) + } + title = "Download speed limited" + text = "Subscribe to [Telegram Premium]() and increase download speeds \(speedIncreaseFactor) times." + case .upload: + var speedIncreaseFactor = 10 + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["upload_premium_speedup_upload"] as? Double { + speedIncreaseFactor = Int(value) + } + title = "Upload speed limited" + text = "Subscribe to [Telegram Premium]() and increase upload speeds \(speedIncreaseFactor) times." + } + let content: UndoOverlayContent = .universal(animation: "anim_speed_low", scale: 0.066, colors: [:], title: title, text: text, customUndoText: nil, timeout: 5.0) + + self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .top, action: { [weak self] action in + guard let self else { + return false + } + switch action { + case .info: + let controller = context.sharedContext.makePremiumIntroController(context: self.context, source: .reactions, forceDark: false, dismissed: nil) + self.push(controller) + return true + default: + break + } + return false + }), in: .current) + }) + self.displayNodeDidLoad() } @@ -14110,6 +14211,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } + func enqueueStickerFile(_ file: TelegramMediaFile) { + let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + + let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, nil) + self.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) + + Queue.mainQueue().after(3.0) { + if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage(), let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isSticker { + self.context.engine.stickers.addRecentlyUsedSticker(file: file) + } + } + } + func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { if !canSendMessagesToChat(self.presentationInterfaceState) { return @@ -14471,13 +14594,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G frames: data.frames, framesUpdateTimestamp: data.framesUpdateTimestamp, trimRange: data.trimRange -// control: ChatRecordedMediaPreview.Video.Control( -// updateTrimRange: { [weak self] start, end, updatedEnd, apply in -// if let self, let videoRecorderValue = self.videoRecorderValue { -// videoRecorderValue.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply) -// } -// } -// ) ) )) }.updatedInputTextPanelState { panelState in @@ -14807,25 +14923,41 @@ 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 + if threadId != nil { + viewControllers.remove(at: viewControllers.count - 2) + lastController.navigationPresentation = .modal + } viewControllers.insert(chatController, at: viewControllers.count - 1) 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 8772e663cc..fd8e11c61a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -1715,66 +1715,52 @@ extension ChatControllerImpl { } 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 { + var dismissImpl: (() -> Void)? + let mainController = self.context.sharedContext.makeStickerMediaPickerScreen( + context: self.context, + getSourceRect: { return .zero }, + completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in + guard let self, let asset = result as? PHAsset 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() - - Queue.mainQueue().after(0.1) { - commit({}) - if let mediaResult = result.media, case let .image(image, _) = mediaResult { - self?.enqueueStickerImage(image, isMemoji: false) - } - } - } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void + let editorController = MediaEditorScreen( + context: self.context, + mode: .stickerEditor(mode: .generic), + subject: .single(.asset(asset)), + transitionIn: .gallery( + MediaEditorScreen.TransitionIn.GalleryTransitionIn( + sourceView: transitionView, + sourceRect: transitionRect, + sourceImage: transitionImage ) - editorController.dismissed = { [weak controller] in - controller?.updateHiddenMediaId(nil) + ), + transitionOut: { finished, isNew in + if !finished { + return MediaEditorScreen.TransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0 + ) } - self.push(editorController) - -// completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in -// controller?.updateHiddenMediaId(nil) -// }) - } - } - } - present(mediaPickerController, mediaPickerController.mediaPickerContext) + return nil + }, completion: { [weak self] result, commit in + dismissImpl?() + 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 + ) + self.push(editorController) + }, + dismissed: {} + ) + dismissImpl = { [weak mainController] in + mainController?.dismiss() } mainController.navigationPresentation = .flatModal mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index d90cc96063..694a5266a2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -117,32 +117,44 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat } var displayActionsPanel = false - if !chatPresentationInterfaceState.peerIsBlocked && !inhibitTitlePanelDisplay, let contactStatus = chatPresentationInterfaceState.contactStatus, let peerStatusSettings = contactStatus.peerStatusSettings { - if !peerStatusSettings.flags.isEmpty { - if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.autoArchived) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canShareContact) { - displayActionsPanel = true - } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.suggestAddMembers) { + if !chatPresentationInterfaceState.peerIsBlocked && !inhibitTitlePanelDisplay, let contactStatus = chatPresentationInterfaceState.contactStatus { + if let peerStatusSettings = contactStatus.peerStatusSettings { + if !peerStatusSettings.flags.isEmpty { + if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { + displayActionsPanel = true + } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) || peerStatusSettings.contains(.autoArchived) { + displayActionsPanel = true + } else if peerStatusSettings.contains(.canShareContact) { + displayActionsPanel = true + } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { + displayActionsPanel = true + } else if peerStatusSettings.contains(.suggestAddMembers) { + displayActionsPanel = true + } + } + if peerStatusSettings.requestChatTitle != nil { displayActionsPanel = true } } - if peerStatusSettings.requestChatTitle != nil { - displayActionsPanel = true - } } - if displayActionsPanel && (selectedContext == nil || selectedContext! <= .pinnedMessage) { - if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode { - return currentPanel - } else if let controllerInteraction = controllerInteraction { - let panel = ChatReportPeerTitlePanelNode(context: context, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer) - panel.interfaceInteraction = interfaceInteraction - return panel + if (selectedContext == nil || selectedContext! <= .pinnedMessage) { + if displayActionsPanel { + if let currentPanel = currentPanel as? ChatReportPeerTitlePanelNode { + return currentPanel + } else if let controllerInteraction = controllerInteraction { + let panel = ChatReportPeerTitlePanelNode(context: context, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer) + panel.interfaceInteraction = interfaceInteraction + return panel + } + } else if !chatPresentationInterfaceState.peerIsBlocked && !inhibitTitlePanelDisplay, let contactStatus = chatPresentationInterfaceState.contactStatus, contactStatus.managingBot != nil { + if let currentPanel = currentPanel as? ChatManagingBotTitlePanelNode { + return currentPanel + } else { + let panel = ChatManagingBotTitlePanelNode(context: context) + panel.interfaceInteraction = interfaceInteraction + return panel + } } } diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift new file mode 100644 index 0000000000..73e852e12d --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -0,0 +1,472 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import ChatPresentationInterfaceState +import ComponentFlow +import AvatarNode +import MultilineTextComponent +import PlainButtonComponent +import ComponentDisplayAdapters +import AccountContext +import TelegramCore +import BundleIconComponent +import ContextUI +import SwiftSignalKit + +private final class ChatManagingBotTitlePanelComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let insets: UIEdgeInsets + let peer: EnginePeer + let managesChat: Bool + let isPaused: Bool + let toggleIsPaused: () -> Void + let openSettings: (UIView) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + insets: UIEdgeInsets, + peer: EnginePeer, + managesChat: Bool, + isPaused: Bool, + toggleIsPaused: @escaping () -> Void, + openSettings: @escaping (UIView) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.insets = insets + self.peer = peer + self.managesChat = managesChat + self.isPaused = isPaused + self.toggleIsPaused = toggleIsPaused + self.openSettings = openSettings + } + + static func ==(lhs: ChatManagingBotTitlePanelComponent, rhs: ChatManagingBotTitlePanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.managesChat != rhs.managesChat { + return false + } + if lhs.isPaused != rhs.isPaused { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let text = ComponentView() + private var avatarNode: AvatarNode? + private let actionButton = ComponentView() + private let settingsButton = ComponentView() + + private var component: ChatManagingBotTitlePanelComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ChatManagingBotTitlePanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let topInset: CGFloat = 6.0 + let bottomInset: CGFloat = 6.0 + let avatarDiameter: CGFloat = 36.0 + let avatarTextSpacing: CGFloat = 10.0 + let titleTextSpacing: CGFloat = 1.0 + let leftInset: CGFloat = component.insets.left + 12.0 + let rightInset: CGFloat = component.insets.right + 10.0 + let actionAndSettingsButtonsSpacing: CGFloat = 8.0 + + //TODO:localize + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.isPaused ? "START" : "STOP", font: Font.semibold(15.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + )), + background: AnyComponent(RoundedRectangle( + color: component.theme.list.itemCheckColors.fillColor, + cornerRadius: nil + )), + effectAlignment: .center, + contentInsets: UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.toggleIsPaused() + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: 150.0, height: 100.0) + ) + + let settingsButtonSize = self.settingsButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Chat/Context Menu/Customize", + tintColor: component.theme.rootController.navigationBar.controlColor + )), + effectAlignment: .center, + minSize: CGSize(width: 1.0, height: 40.0), + contentInsets: UIEdgeInsets(top: 0.0, left: 2.0, bottom: 0.0, right: 2.0), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + guard let settingsButtonView = self.settingsButton.view else { + return + } + component.openSettings(settingsButtonView) + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: 150.0, height: 100.0) + ) + + let maxTextWidth: CGFloat = availableSize.width - leftInset - avatarDiameter - avatarTextSpacing - rightInset - actionButtonSize.width - actionAndSettingsButtonsSpacing - settingsButtonSize.width - 8.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.semibold(16.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + //TODO:localize + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "bot has access to this chat", font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextWidth, height: 100.0) + ) + + let size = CGSize(width: availableSize.width, height: topInset + titleSize.height + titleTextSpacing + textSize.height + bottomInset) + + let titleFrame = CGRect(origin: CGPoint(x: leftInset + avatarDiameter + avatarTextSpacing, y: topInset), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint() + self.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setPosition(view: titleView, position: titleFrame.origin) + } + + let textFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleTextSpacing), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.layer.anchorPoint = CGPoint() + self.addSubview(textView) + } + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + transition.setPosition(view: textView, position: textFrame.origin) + } + + let avatarFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - avatarDiameter) * 0.5)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + avatarNode.frame = avatarFrame + avatarNode.updateSize(size: avatarFrame.size) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + + let settingsButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - settingsButtonSize.width, y: floor((size.height - settingsButtonSize.height) * 0.5)), size: settingsButtonSize) + if let settingsButtonView = self.settingsButton.view { + if settingsButtonView.superview == nil { + self.addSubview(settingsButtonView) + } + transition.setFrame(view: settingsButtonView, frame: settingsButtonFrame) + } + + let actionButtonFrame = CGRect(origin: CGPoint(x: settingsButtonFrame.minX - actionAndSettingsButtonsSpacing - actionButtonSize.width, y: floor((size.height - actionButtonSize.height) * 0.5)), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + 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) + } +} + +final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode { + private let context: AccountContext + private let separatorNode: ASDisplayNode + private let content = ComponentView() + + private var chatLocation: ChatLocation? + private var theme: PresentationTheme? + private var managingBot: ChatManagingBot? + + init(context: AccountContext) { + self.context = context + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.separatorNode) + } + + private func toggleIsPaused() { + guard let chatPeerId = self.chatLocation?.peerId else { + return + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessConnectedBot(id: self.context.account.peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] bot in + guard let self else { + return + } + guard let bot else { + return + } + + var recipients = bot.recipients + var additionalPeers = recipients.additionalPeers + if additionalPeers.contains(chatPeerId) { + additionalPeers.remove(chatPeerId) + } else { + additionalPeers.insert(chatPeerId) + } + recipients = TelegramBusinessRecipients( + categories: recipients.categories, + additionalPeers: additionalPeers, + exclude: recipients.exclude + ) + + let _ = self.context.engine.accountData.setAccountConnectedBot(bot: TelegramAccountConnectedBot( + id: bot.id, + recipients: recipients, + canReply: bot.canReply + )).startStandalone() + }) + } + + private func openSettingsMenu(sourceView: UIView) { + guard let interfaceInteraction = self.interfaceInteraction else { + return + } + guard let chatController = interfaceInteraction.chatController() else { + return + } + guard let managingBot = self.managingBot else { + return + } + let _ = managingBot + + let strings = self.context.sharedContext.currentPresentationData.with { $0 }.strings + let _ = strings + + var items: [ContextMenuItem] = [] + + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Remove bot from this chat", textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + let _ = self + }))) + if let url = managingBot.settingsUrl { + items.append(.action(ContextMenuActionItem(text: "Manage Bot", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + let _ = (self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: url, skipUrlAuth: false) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + guard let chatController = interfaceInteraction.chatController() else { + return + } + self.context.sharedContext.openResolvedUrl( + result, + context: self.context, + urlContext: .generic, + navigationController: chatController.navigationController as? NavigationController, + forceExternal: false, + openPeer: { [weak self] peer, navigation in + guard let self, let chatController = interfaceInteraction.chatController() else { + return + } + guard let navigationController = chatController.navigationController as? NavigationController else { + return + } + switch navigation { + case let .chat(_, subject, peekData): + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: subject, peekData: peekData)) + case let .withBotStartPayload(botStart): + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botStart: botStart, keepStack: .always)) + case let .withAttachBot(attachBotStart): + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) + case let .withBotApp(botAppStart): + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botAppStart: botAppStart)) + case .info: + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer, let chatController = interfaceInteraction.chatController() else { + return + } + guard let navigationController = chatController.navigationController as? NavigationController else { + return + } + if let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + navigationController.pushViewController(controller) + } + }) + default: + break + } + }, + sendFile: nil, + sendSticker: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: nil, + present: { [weak chatController] c, a in + chatController?.present(c, in: .window(.root), with: a) + }, + dismissInput: { + }, + contentContext: nil, + progress: nil, + completion: nil + ) + }) + }))) + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: chatController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + interfaceInteraction.presentController(contextController, nil) + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { + self.chatLocation = interfaceState.chatLocation + self.managingBot = interfaceState.contactStatus?.managingBot + + if interfaceState.theme !== self.theme { + self.theme = interfaceState.theme + + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + } + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + if let managingBot = interfaceState.contactStatus?.managingBot { + let contentSize = self.content.update( + transition: Transition(transition), + component: AnyComponent(ChatManagingBotTitlePanelComponent( + context: self.context, + theme: interfaceState.theme, + strings: interfaceState.strings, + insets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: 0.0, right: rightInset), + peer: managingBot.bot, + managesChat: managingBot.canReply, + isPaused: managingBot.isPaused, + toggleIsPaused: { [weak self] in + guard let self else { + return + } + self.toggleIsPaused() + }, + openSettings: { [weak self] sourceView in + guard let self else { + return + } + self.openSettingsMenu(sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: width, height: 1000.0) + ) + if let contentView = self.content.view { + if contentView.superview == nil { + self.view.addSubview(contentView) + } + transition.updateFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: contentSize)) + } + + return LayoutResult(backgroundHeight: contentSize.height, insetHeight: contentSize.height, hitTestSlop: 0.0) + } else { + return LayoutResult(backgroundHeight: 0.0, insetHeight: 0.0, hitTestSlop: 0.0) + } + + } +} + +private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} + diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 9dc0e6f4c2..c6fc539229 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -159,9 +159,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.presentThemeSelection() } if let botStart = params.botStart { - controller.updateChatPresentationInterfaceState(interactive: false, { state -> ChatPresentationInterfaceState in - return state.updatedBotStartPayload(botStart.payload) - }) + controller.startBot(botStart.payload) } if let attachBotStart = params.attachBotStart { controller.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index c820caf77a..15720f5ebc 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -32,6 +32,7 @@ import AuthorizationUI import ChatFolderLinkPreviewScreen import StoryContainerScreen import WallpaperGalleryScreen +import TelegramStringFormatting private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -986,7 +987,15 @@ func openResolvedUrlImpl( forceDark: forceDark, action: { [weak navigationController] in let _ = (context.engine.payments.applyPremiumGiftCode(slug: slug) - |> deliverOnMainQueue).startStandalone(completed: { + |> deliverOnMainQueue).startStandalone(error: { error in + dismissImpl?() + + if case let .waitForExpiration(date) = error { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let dateText = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + (navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Title, text: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Text(dateText).string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, action: { _ in return true }), in: .window(.root)) + } + }, completed: { dismissImpl?() let controller = PremiumIntroScreen(context: context, source: .settings, forceDark: forceDark, forceHasPremium: true) @@ -1030,7 +1039,7 @@ func openResolvedUrlImpl( (navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages) - |> deliverOnMainQueue).startStandalone() + |> deliverOnMainQueue).startStandalone() if let peerSelectionController = peerSelectionController { peerSelectionController.dismiss() } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index f454f44991..48b0935967 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -55,6 +55,10 @@ import ChatbotSetupScreen import BusinessLocationSetupScreen import BusinessHoursSetupScreen import AutomaticBusinessMessageSetupScreen +import CollectibleItemInfoScreen +import StickerPickerScreen +import MediaEditor +import MediaEditorScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1923,6 +1927,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { return QuickReplySetupScreen.initialData(context: context) } + public func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController { + return CollectibleItemInfoScreen(context: context, initialData: initialData as! CollectibleItemInfoScreen.InitialData) + } + + public func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal { + return CollectibleItemInfoScreen.initialData(context: context, peerId: peerId, subject: subject) + } + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { var modal = true let mappedSource: PremiumSource @@ -2255,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)? @@ -2263,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, @@ -2293,8 +2305,50 @@ public final class SharedAccountContextImpl: SharedAccountContext { return controller } - public func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController { - return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker) + public 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 { + return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, isEditing: isEditing, parentNavigationController: parentNavigationController, sendSticker: sendSticker) + } + + 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 { @@ -2304,6 +2358,21 @@ public final class SharedAccountContextImpl: SharedAccountContext { public 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 { return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented) } + + 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/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 2c6b97fd5b..aefb623683 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -618,6 +618,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers) } + default: + break } } else if let existingMedia { media = .existing(media: existingMedia._asMedia()) 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; diff --git a/submodules/WebPBinding/Sources/UIImage+WebP.m b/submodules/WebPBinding/Sources/UIImage+WebP.m index c4a07eed72..c6a1d9d371 100644 --- a/submodules/WebPBinding/Sources/UIImage+WebP.m +++ b/submodules/WebPBinding/Sources/UIImage+WebP.m @@ -87,6 +87,7 @@ CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGColorSpaceRelease(colorSpace); + CGContextClearRect(context, CGRectMake(0, 0, width, height)); CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGContextRelease(context); diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 878e6bcb76..3973bdc9fc 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/CheckNode:CheckNode", "//submodules/Markdown:Markdown", "//submodules/TextFormat:TextFormat", + "//submodules/LocalAuth", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 642b3d682d..5894b1e08b 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -23,6 +23,7 @@ import PromptUI import PhoneNumberFormat import QrCodeUI import InstantPageUI +import LocalAuth private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -1064,6 +1065,18 @@ public final class WebAppController: ViewController, AttachmentContainable { if let json = json, let isVisible = json["is_visible"] as? Bool { self.controller?.hasSettings = isVisible } + case "web_app_biometry_get_info": + self.sendBiometryInfoReceivedEvent() + case "web_app_biometry_request_access": + self.requestBiometryAccess() + case "web_app_biometry_request_auth": + self.requestBiometryAuth() + case "web_app_biometry_update_token": + var tokenData: Data? + if let json, let tokenDataValue = json["token"] as? String, !tokenDataValue.isEmpty { + tokenData = tokenDataValue.data(using: .utf8) + } + self.requestBiometryUpdateToken(tokenData: tokenData) default: break } @@ -1392,6 +1405,257 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "custom_method_invoked", data: paramsString) }) } + + fileprivate func sendBiometryInfoReceivedEvent() { + guard let controller = self.controller else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId) + ) + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + + var data: [String: Any] = [:] + if let biometricAuthentication = LocalAuth.biometricAuthentication { + data["available"] = true + switch biometricAuthentication { + case .faceId: + data["type"] = "face" + case .touchId: + data["type"] = "finger" + } + data["access_requested"] = state.accessRequested + data["access_granted"] = state.accessGranted + data["token_saved"] = state.opaqueToken != nil + } else { + data["available"] = false + } + + guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else { + return + } + guard let jsonDataString = String(data: jsonData, encoding: .utf8) else { + return + } + self.webView?.sendEvent(name: "biometry_info_received", data: jsonDataString) + }) + } + + fileprivate func requestBiometryAccess() { + guard let controller = self.controller else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId), + TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId) + ) + |> deliverOnMainQueue).start(next: { [weak self] botPeer, state in + guard let self, let botPeer, let controller = self.controller else { + return + } + + if state.accessRequested { + self.sendBiometryInfoReceivedEvent() + return + } + + let updateAccessGranted: (Bool) -> Void = { [weak self] granted in + guard let self else { + return + } + + self.context.engine.peers.updateBotBiometricsState(peerId: botPeer.id, update: { state in + var state = state + state.accessRequested = true + state.accessGranted = granted + return state + }) + + self.sendBiometryInfoReceivedEvent() + } + + //TODO:localize + let alertText = "Do you want to allow \(botPeer.compactDisplayTitle) to use Face ID?" + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: alertText, actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_No, action: { + updateAccessGranted(false) + }), + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Yes, action: { + updateAccessGranted(true) + }) + ], parseMarkdown: false), in: .window(.root)) + }) + } + + fileprivate func requestBiometryAuth() { + guard let controller = self.controller else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId), + TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId) + ) + |> deliverOnMainQueue).start(next: { [weak self] botPeer, state in + guard let self else { + return + } + + if state.accessRequested && state.accessGranted { + guard let controller = self.controller else { + return + } + guard let keyId = "A\(UInt64(bitPattern: self.context.account.id.int64))WebBot\(UInt64(bitPattern: controller.botId.toInt64()))".data(using: .utf8) else { + return + } + let appBundleId = self.context.sharedContext.applicationBindings.appBundleId + + Thread { [weak self] in + let key = LocalAuth.getOrCreatePrivateKey(baseAppBundleId: appBundleId, keyId: keyId) + + let decryptedData: LocalAuth.DecryptionResult + if let key { + if let encryptedData = state.opaqueToken { + if encryptedData.publicKey == key.publicKeyRepresentation { + decryptedData = key.decrypt(data: encryptedData.data) + } else { + // The local keychain has been reset + if let emptyEncryptedData = key.encrypt(data: Data()) { + decryptedData = key.decrypt(data: emptyEncryptedData) + } else { + decryptedData = .error(.generic) + } + } + } else { + if let emptyEncryptedData = key.encrypt(data: Data()) { + decryptedData = key.decrypt(data: emptyEncryptedData) + } else { + decryptedData = .error(.generic) + } + } + } else { + decryptedData = .error(.generic) + } + + DispatchQueue.main.async { + guard let self else { + return + } + + switch decryptedData { + case let .result(token): + self.sendBiometryAuthResult(isAuthorized: true, tokenData: state.opaqueToken != nil ? token : nil) + case .error: + self.sendBiometryAuthResult(isAuthorized: false, tokenData: nil) + } + } + }.start() + } else { + self.sendBiometryAuthResult(isAuthorized: false, tokenData: nil) + } + }) + } + + fileprivate func sendBiometryAuthResult(isAuthorized: Bool, tokenData: Data?) { + var data: [String: Any] = [:] + data["status"] = isAuthorized ? "authorized" : "failed" + if isAuthorized { + if let tokenData { + data["token"] = String(data: tokenData, encoding: .utf8) ?? "" + } else { + data["token"] = "" + } + } + + guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else { + return + } + guard let jsonDataString = String(data: jsonData, encoding: .utf8) else { + return + } + self.webView?.sendEvent(name: "biometry_auth_requested", data: jsonDataString) + } + + fileprivate func requestBiometryUpdateToken(tokenData: Data?) { + guard let controller = self.controller else { + return + } + guard let keyId = "A\(UInt64(bitPattern: self.context.account.id.int64))WebBot\(UInt64(bitPattern: controller.botId.toInt64()))".data(using: .utf8) else { + return + } + + if let tokenData { + let appBundleId = self.context.sharedContext.applicationBindings.appBundleId + Thread { [weak self] in + let key = LocalAuth.getOrCreatePrivateKey(baseAppBundleId: appBundleId, keyId: keyId) + + var encryptedData: TelegramBotBiometricsState.OpaqueToken? + if let key { + if let result = key.encrypt(data: tokenData) { + encryptedData = TelegramBotBiometricsState.OpaqueToken( + publicKey: key.publicKeyRepresentation, + data: result + ) + } + } + + DispatchQueue.main.async { + guard let self else { + return + } + + if let encryptedData { + self.context.engine.peers.updateBotBiometricsState(peerId: controller.botId, update: { state in + var state = state + state.opaqueToken = encryptedData + return state + }) + + var data: [String: Any] = [:] + data["status"] = "updated" + + guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else { + return + } + guard let jsonDataString = String(data: jsonData, encoding: .utf8) else { + return + } + self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString) + } else { + var data: [String: Any] = [:] + data["status"] = "failed" + + guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else { + return + } + guard let jsonDataString = String(data: jsonData, encoding: .utf8) else { + return + } + self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString) + } + } + }.start() + } else { + self.context.engine.peers.updateBotBiometricsState(peerId: controller.botId, update: { state in + var state = state + state.opaqueToken = nil + return state + }) + + var data: [String: Any] = [:] + data["status"] = "removed" + + guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else { + return + } + guard let jsonDataString = String(data: jsonData, encoding: .utf8) else { + return + } + self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString) + } + } } fileprivate var controllerNode: Node {